countdown-cli 2.0.0__tar.gz → 2.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. countdown_cli-2.1.0/.uv-cache/.gitignore +1 -0
  2. countdown_cli-2.1.0/.uv-cache/.lock +0 -0
  3. countdown_cli-2.1.0/.uv-cache/CACHEDIR.TAG +1 -0
  4. countdown_cli-2.1.0/.uv-cache/interpreter-v4/3ae53c40791d22fd/16472b94e5618f48.msgpack +0 -0
  5. countdown_cli-2.1.0/.uv-cache/sdists-v9/.git +0 -0
  6. countdown_cli-2.1.0/.uv-cache/sdists-v9/.gitignore +0 -0
  7. countdown_cli-2.1.0/AGENTS.md +12 -0
  8. countdown_cli-2.1.0/CONTRIBUTING.rst +75 -0
  9. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/PKG-INFO +9 -11
  10. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/README.rst +7 -9
  11. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/pyproject.toml +2 -10
  12. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/src/countdown/__main__.py +5 -5
  13. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/src/countdown/digits.py +7 -2
  14. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/src/countdown/display.py +26 -13
  15. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/src/countdown/terminal.py +2 -2
  16. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/src/countdown/timer.py +2 -2
  17. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/tests/test_display.py +92 -29
  18. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/tests/test_keys.py +9 -5
  19. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/tests/test_main.py +93 -40
  20. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/tests/test_timer.py +16 -0
  21. countdown_cli-2.1.0/uv.lock +319 -0
  22. countdown_cli-2.0.0/CONTRIBUTING.rst +0 -121
  23. countdown_cli-2.0.0/uv.lock +0 -319
  24. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/.claude/settings.local.json +0 -0
  25. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/.envrc +0 -0
  26. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/.gitattributes +0 -0
  27. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/.github/labels.yml +0 -0
  28. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/.github/release-drafter.yml +0 -0
  29. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/.github/workflows/labeler.yml +0 -0
  30. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/.github/workflows/release.yml +0 -0
  31. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/.github/workflows/tests.yml +0 -0
  32. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/.gitignore +0 -0
  33. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/.pre-commit-config.yaml +0 -0
  34. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/.python-version-default +0 -0
  35. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/CODE_OF_CONDUCT.rst +0 -0
  36. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/LICENSE.rst +0 -0
  37. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/codecov.yml +0 -0
  38. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/.gitignore +0 -0
  39. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/class_index.html +0 -0
  40. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/coverage_html_cb_6fb7b396.js +0 -0
  41. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/coverage_html_cb_da166b87.js +0 -0
  42. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/favicon_32_cb_58284776.png +0 -0
  43. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/function_index.html +0 -0
  44. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/index.html +0 -0
  45. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/keybd_closed_cb_ce680311.png +0 -0
  46. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/status.json +0 -0
  47. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/style_cb_6b508a39.css +0 -0
  48. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/style_cb_8e611ae1.css +0 -0
  49. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/z_55719c21e2af63da___init___py.html +0 -0
  50. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/z_55719c21e2af63da___main___py.html +0 -0
  51. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/z_55719c21e2af63da_digits_py.html +0 -0
  52. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/z_55719c21e2af63da_display_py.html +0 -0
  53. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/z_55719c21e2af63da_keys_py.html +0 -0
  54. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/z_55719c21e2af63da_terminal_py.html +0 -0
  55. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/z_55719c21e2af63da_timer_py.html +0 -0
  56. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/z_a44f0ac069e85531___init___py.html +0 -0
  57. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/z_a44f0ac069e85531_conftest_py.html +0 -0
  58. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/z_a44f0ac069e85531_test_display_py.html +0 -0
  59. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/z_a44f0ac069e85531_test_keys_py.html +0 -0
  60. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/z_a44f0ac069e85531_test_main_py.html +0 -0
  61. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/z_a44f0ac069e85531_test_terminal_py.html +0 -0
  62. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/z_a44f0ac069e85531_test_timer_py.html +0 -0
  63. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/images/1457.png +0 -0
  64. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/images/3253.png +0 -0
  65. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/images/python-morsels-logo.png +0 -0
  66. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/justfile +0 -0
  67. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/noxfile.py +0 -0
  68. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/src/countdown/__init__.py +0 -0
  69. /countdown_cli-2.0.0/src/countdown/numbers.txt → /countdown_cli-2.1.0/src/countdown/glyphs.txt +0 -0
  70. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/src/countdown/keys.py +0 -0
  71. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/tests/__init__.py +0 -0
  72. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/tests/conftest.py +0 -0
  73. {countdown_cli-2.0.0 → countdown_cli-2.1.0}/tests/test_terminal.py +0 -0
@@ -0,0 +1 @@
1
+ *
File without changes
@@ -0,0 +1 @@
1
+ Signature: 8a477f597d28d172789f06886806bc55
File without changes
File without changes
@@ -0,0 +1,12 @@
1
+ # Repository Guidelines
2
+
3
+ ## Structure
4
+ - `src/countdown/` hosts the CLI (`__main__.py`, `timer.py`, `display.py`, `terminal.py`, `keys.py`, `glyphs.txt`); `tests/` mirrors each module.
5
+ - Tooling lives in `justfile`, `pyproject.toml`, and `noxfile.py`; see `CONTRIBUTING.rst` if you need the long-form tour.
6
+
7
+ ## Workflow
8
+ - `just check` runs format + lint + pytest; treat it as the pre-push gate.
9
+ - `just test [-- -k expr]`, `just test-cov`, and `just test-all` cover targeted, coverage, and multi-Python runs.
10
+
11
+ ## Style
12
+ - Use `just format` instead of manual tweaks; keep docstrings concise and only add type hints when they clarify intent.
@@ -0,0 +1,75 @@
1
+ Contributor Guide
2
+ =================
3
+
4
+ How to report a bug
5
+ -------------------
6
+
7
+ Report bugs on the `Issue Tracker`_.
8
+
9
+ When filing an issue, make sure to answer these questions:
10
+
11
+ - Which operating system and Python version are you using?
12
+ - Which version of this project are you using?
13
+ - What did you do?
14
+ - What did you expect to see?
15
+ - What did you see instead?
16
+
17
+ The best way to get your bug fixed is to provide a test case,
18
+ and/or steps to reproduce the issue.
19
+
20
+ .. _Issue Tracker: https://github.com/treyhunner/countdown-cli/issues
21
+
22
+
23
+ How to request a feature
24
+ ------------------------
25
+
26
+ Request features on the `Issue Tracker`_.
27
+
28
+
29
+ How to set up your development environment
30
+ ------------------------------------------
31
+
32
+ You need Python, uv_, and just_ installed locally.
33
+ Nox_ is optional but required for the multi-version test matrix.
34
+
35
+ The CLI is exposed through the ``countdown`` script.
36
+ Run it directly from the synced environment:
37
+
38
+ .. code:: console
39
+
40
+ uv run countdown 6m30s
41
+
42
+ .. _uv: https://docs.astral.sh/uv/
43
+ .. _just: https://github.com/casey/just
44
+ .. _Nox: https://nox.thea.codes/
45
+
46
+
47
+ How to test the project
48
+ -----------------------
49
+
50
+ This project uses pytest_ and Ruff_ orchestrated through ``just`` tasks.
51
+ Before opening a pull request, run the aggregated check:
52
+
53
+ .. code:: console
54
+
55
+ just check
56
+
57
+ Useful individual commands:
58
+
59
+ .. code:: console
60
+
61
+ just test -- -k timer
62
+ just test-cov # pytest with coverage (fail_under=100)
63
+
64
+ Unit tests are located in the ``tests`` directory,
65
+ and are written using the pytest_ testing framework.
66
+ Open ``htmlcov/index.html`` after ``just test-cov`` to debug coverage issues.
67
+
68
+ If you need to validate across every supported Python version, run:
69
+
70
+ .. code:: console
71
+
72
+ just test-all
73
+
74
+ .. _pytest: https://pytest.readthedocs.io/
75
+ .. _Ruff: https://docs.astral.sh/ruff/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: countdown-cli
3
- Version: 2.0.0
3
+ Version: 2.1.0
4
4
  Summary: Terminal program to display countdown timer
5
5
  Project-URL: homepage, https://github.com/treyhunner/countdown-cli
6
6
  Project-URL: repository, https://github.com/treyhunner/countdown-cli
@@ -16,7 +16,7 @@ Classifier: Programming Language :: Python :: 3.13
16
16
  Classifier: Programming Language :: Python :: 3.14
17
17
  Classifier: Programming Language :: Python :: 3.15
18
18
  Requires-Python: >=3.10
19
- Requires-Dist: click>=8.0.1
19
+ Requires-Dist: click>=8.3.0
20
20
  Description-Content-Type: text/x-rst
21
21
 
22
22
  countdown-cli
@@ -26,8 +26,6 @@ countdown-cli
26
26
 
27
27
  |Tests| |Codecov|
28
28
 
29
- |pre-commit| |Black|
30
-
31
29
  .. |PyPI| image:: https://img.shields.io/pypi/v/countdown-cli.svg
32
30
  :target: https://pypi.org/project/countdown-cli/
33
31
  :alt: PyPI
@@ -46,12 +44,6 @@ countdown-cli
46
44
  .. |Codecov| image:: https://codecov.io/gh/treyhunner/countdown-cli/branch/main/graph/badge.svg
47
45
  :target: https://codecov.io/gh/treyhunner/countdown-cli
48
46
  :alt: Codecov
49
- .. |pre-commit| image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white
50
- :target: https://github.com/pre-commit/pre-commit
51
- :alt: pre-commit
52
- .. |Black| image:: https://img.shields.io/badge/code%20style-black-000000.svg
53
- :target: https://github.com/psf/black
54
- :alt: Black
55
47
 
56
48
  This project is based on a `Python Morsels`_ exercise for a command-line countdown timer.
57
49
  If you're working on that exercise right now, please **don't look at the source code** for this. 😉
@@ -77,6 +69,12 @@ Features
77
69
  - Pause/resume with ``p``, ``k``, ``Space``, or ``Enter``
78
70
  - Add / remove time with ``+`` or ``-``
79
71
 
72
+ To start a timer:
73
+
74
+ .. code:: console
75
+
76
+ countdown 6m30s
77
+
80
78
  |32:53|
81
79
 
82
80
  |14:57|
@@ -103,7 +101,7 @@ You can install **countdown-cli** via uv_ from PyPI_:
103
101
 
104
102
  .. code:: console
105
103
 
106
- $ uv tool install countdown-cli
104
+ uv tool install countdown-cli
107
105
 
108
106
 
109
107
  Contributing
@@ -5,8 +5,6 @@ countdown-cli
5
5
 
6
6
  |Tests| |Codecov|
7
7
 
8
- |pre-commit| |Black|
9
-
10
8
  .. |PyPI| image:: https://img.shields.io/pypi/v/countdown-cli.svg
11
9
  :target: https://pypi.org/project/countdown-cli/
12
10
  :alt: PyPI
@@ -25,12 +23,6 @@ countdown-cli
25
23
  .. |Codecov| image:: https://codecov.io/gh/treyhunner/countdown-cli/branch/main/graph/badge.svg
26
24
  :target: https://codecov.io/gh/treyhunner/countdown-cli
27
25
  :alt: Codecov
28
- .. |pre-commit| image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white
29
- :target: https://github.com/pre-commit/pre-commit
30
- :alt: pre-commit
31
- .. |Black| image:: https://img.shields.io/badge/code%20style-black-000000.svg
32
- :target: https://github.com/psf/black
33
- :alt: Black
34
26
 
35
27
  This project is based on a `Python Morsels`_ exercise for a command-line countdown timer.
36
28
  If you're working on that exercise right now, please **don't look at the source code** for this. 😉
@@ -56,6 +48,12 @@ Features
56
48
  - Pause/resume with ``p``, ``k``, ``Space``, or ``Enter``
57
49
  - Add / remove time with ``+`` or ``-``
58
50
 
51
+ To start a timer:
52
+
53
+ .. code:: console
54
+
55
+ countdown 6m30s
56
+
59
57
  |32:53|
60
58
 
61
59
  |14:57|
@@ -82,7 +80,7 @@ You can install **countdown-cli** via uv_ from PyPI_:
82
80
 
83
81
  .. code:: console
84
82
 
85
- $ uv tool install countdown-cli
83
+ uv tool install countdown-cli
86
84
 
87
85
 
88
86
  Contributing
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "countdown-cli"
3
- version = "2.0.0"
3
+ version = "2.1.0"
4
4
  description = "Terminal program to display countdown timer"
5
5
  readme = "README.rst"
6
6
  license = { text = "MIT" }
@@ -18,7 +18,7 @@ classifiers = [
18
18
  "Programming Language :: Python :: 3.15",
19
19
  ]
20
20
  dependencies = [
21
- "click>=8.0.1",
21
+ "click>=8.3.0",
22
22
  ]
23
23
 
24
24
  [project.urls]
@@ -49,14 +49,6 @@ source = ["countdown", "tests"]
49
49
  show_missing = true
50
50
  fail_under = 100
51
51
 
52
- [tool.mypy]
53
- strict = true
54
- warn_unreachable = true
55
- pretty = true
56
- show_column_numbers = true
57
- show_error_codes = true
58
- show_error_context = true
59
-
60
52
  [build-system]
61
53
  requires = ["hatchling"]
62
54
  build-backend = "hatchling.build"
@@ -1,6 +1,6 @@
1
1
  """Command-line interface."""
2
2
 
3
- import time
3
+ from time import sleep
4
4
 
5
5
  import click
6
6
 
@@ -26,7 +26,7 @@ from .terminal import (
26
26
 
27
27
  def get_number_lines(seconds):
28
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())
29
+ return timer.get_number_lines(seconds, get_chars_for_terminal(seconds))
30
30
 
31
31
 
32
32
  def run_countdown(total_seconds):
@@ -69,13 +69,13 @@ def run_countdown(total_seconds):
69
69
  if not paused:
70
70
  # Sleep in small chunks to check for keypresses more frequently
71
71
  for _ in range(20): # 20 x 0.05 = 1 second
72
- time.sleep(0.05)
72
+ sleep(0.05)
73
73
  if check_for_keypress():
74
74
  break # Exit sleep early if key is pressed
75
75
  n -= 1
76
76
  else:
77
77
  # Short sleep when paused for responsive keypress checking
78
- time.sleep(0.05)
78
+ sleep(0.05)
79
79
  except KeyboardInterrupt:
80
80
  pass
81
81
  finally:
@@ -94,7 +94,7 @@ def main(ctx, duration):
94
94
 
95
95
  Examples of DURATION:
96
96
 
97
- \\b
97
+ \b
98
98
  - 5m (5 minutes)
99
99
  - 45s (45 seconds)
100
100
  - 2m30s (2 minutes and 30 seconds)
@@ -30,8 +30,13 @@ def center(text, width):
30
30
 
31
31
 
32
32
  def populate_constants():
33
- """Populate CHARS_BY_SIZE and DIGIT_SIZES to contain the numbers in numbers.txt."""
34
- lines = files("countdown").joinpath("numbers.txt").read_text(encoding="utf-8").splitlines()
33
+ """Populate CHARS_BY_SIZE and DIGIT_SIZES from glyphs.txt."""
34
+ lines = (
35
+ files("countdown")
36
+ .joinpath("glyphs.txt")
37
+ .read_text(encoding="utf-8")
38
+ .splitlines()
39
+ )
35
40
  number_types = list(paragraphs(lines))
36
41
  for group in number_types:
37
42
  columns = transpose(group)
@@ -1,7 +1,7 @@
1
1
  """Visual rendering and ANSI terminal control."""
2
2
 
3
- import shutil
4
3
  import sys
4
+ from shutil import get_terminal_size
5
5
 
6
6
  from .digits import CHARS_BY_SIZE, DIGIT_SIZES
7
7
 
@@ -35,21 +35,34 @@ def enable_ansi_escape_codes(): # pragma: no cover
35
35
  )
36
36
 
37
37
 
38
- def 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
38
+ def _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}"
45
43
 
46
44
 
47
- def get_chars_for_terminal():
48
- """Return the largest CHARS dictionary that fits in the current terminal."""
49
- width, height = shutil.get_terminal_size()
45
+ def 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
+
55
+ def 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)
50
63
  for size in DIGIT_SIZES:
51
64
  chars = CHARS_BY_SIZE[size]
52
- required_width = get_required_width(chars)
65
+ required_width = get_required_width(chars, time_string)
53
66
  # For size 3 (smallest multi-line), allow it without padding
54
67
  # For larger sizes, require 1 line of padding on top and bottom (2 total)
55
68
  padding_needed = 0 if size == 3 else 2
@@ -61,7 +74,7 @@ def get_chars_for_terminal():
61
74
 
62
75
  def print_full_screen(lines, paused=False):
63
76
  """Print the given lines centered in the middle of the terminal window."""
64
- term_width, term_height = shutil.get_terminal_size()
77
+ term_width, term_height = get_terminal_size()
65
78
 
66
79
  # Calculate total content height
67
80
  content_height = len(lines)
@@ -6,9 +6,9 @@ import sys
6
6
  if sys.platform == "win32": # pragma: no cover
7
7
  import msvcrt
8
8
  else: # pragma: no cover
9
- import select
10
9
  import termios
11
10
  import tty
11
+ from select import select
12
12
 
13
13
 
14
14
  def check_for_keypress(): # pragma: no cover
@@ -18,7 +18,7 @@ def check_for_keypress(): # pragma: no cover
18
18
  if sys.platform == "win32":
19
19
  return msvcrt.kbhit()
20
20
  else:
21
- return select.select([sys.stdin], [], [], 0)[0]
21
+ return select([sys.stdin], [], [], 0)[0]
22
22
 
23
23
 
24
24
  def read_key(): # pragma: no cover
@@ -6,11 +6,11 @@ DURATION_RE = re.compile(
6
6
  r"""
7
7
  ^
8
8
  (?: # Optional minutes
9
- ( \d{1,2} ) # D or DD
9
+ ( \d+ ) # one or more digits
10
10
  m # "m"
11
11
  )?
12
12
  (?: # Optional seconds
13
- ( \d{1,2} ) # D or DD
13
+ ( \d+ ) # one or more digits
14
14
  s # "s"
15
15
  )?
16
16
  $
@@ -17,7 +17,10 @@ def fake_size(columns, lines):
17
17
 
18
18
 
19
19
  def test_print_full_screen_tiny_terminal(capsys, monkeypatch):
20
- monkeypatch.setattr("shutil.get_terminal_size", fake_size(40, 10))
20
+ monkeypatch.setattr(
21
+ "countdown.display.get_terminal_size",
22
+ fake_size(40, 10),
23
+ )
21
24
  display.print_full_screen(["hello world"])
22
25
  out, err = capsys.readouterr()
23
26
  assert out[:6] == "\x1b[H\x1b[J"
@@ -25,7 +28,10 @@ def test_print_full_screen_tiny_terminal(capsys, monkeypatch):
25
28
 
26
29
 
27
30
  def test_print_full_screen_larger_terminal(capsys, monkeypatch):
28
- monkeypatch.setattr("shutil.get_terminal_size", fake_size(80, 24))
31
+ monkeypatch.setattr(
32
+ "countdown.display.get_terminal_size",
33
+ fake_size(80, 24),
34
+ )
29
35
  display.print_full_screen(["hello world"])
30
36
  out, err = capsys.readouterr()
31
37
  assert out[:6] == "\x1b[H\x1b[J"
@@ -35,7 +41,10 @@ def test_print_full_screen_larger_terminal(capsys, monkeypatch):
35
41
 
36
42
 
37
43
  def test_print_full_screen_multiline_text(capsys, monkeypatch):
38
- monkeypatch.setattr("shutil.get_terminal_size", fake_size(100, 30))
44
+ monkeypatch.setattr(
45
+ "countdown.display.get_terminal_size",
46
+ fake_size(100, 30),
47
+ )
39
48
  display.print_full_screen(
40
49
  dedent(
41
50
  """\
@@ -61,14 +70,17 @@ def test_print_full_screen_multiline_text(capsys, monkeypatch):
61
70
 
62
71
  def test_print_full_screen_paused_shows_red_and_message(capsys, monkeypatch):
63
72
  """Test that paused=True shows colored timer and PAUSED message."""
64
- monkeypatch.setattr("shutil.get_terminal_size", fake_size(80, 24))
73
+ monkeypatch.setattr(
74
+ "countdown.display.get_terminal_size",
75
+ fake_size(80, 24),
76
+ )
65
77
  lines = ["00:05"]
66
78
  display.print_full_screen(lines, paused=True)
67
79
  out, err = capsys.readouterr()
68
80
  # Should contain intense magenta color code
69
- assert (
70
- "\x1b[95m" in out
71
- ), "Should contain intense magenta color code when paused"
81
+ assert "\x1b[95m" in out, (
82
+ "Should contain intense magenta color code when paused"
83
+ )
72
84
  # Should contain reset code
73
85
  assert "\033[0m" in out, "Should contain color reset code"
74
86
  # Should contain PAUSED message
@@ -77,7 +89,10 @@ def test_print_full_screen_paused_shows_red_and_message(capsys, monkeypatch):
77
89
 
78
90
  def test_print_full_screen_not_paused_no_red_or_message(capsys, monkeypatch):
79
91
  """Test that paused=False shows normal timer without PAUSED message."""
80
- monkeypatch.setattr("shutil.get_terminal_size", fake_size(80, 24))
92
+ monkeypatch.setattr(
93
+ "countdown.display.get_terminal_size",
94
+ fake_size(80, 24),
95
+ )
81
96
  lines = ["00:05"]
82
97
  display.print_full_screen(lines, paused=False)
83
98
  out, err = capsys.readouterr()
@@ -90,16 +105,18 @@ def test_print_full_screen_not_paused_no_red_or_message(capsys, monkeypatch):
90
105
  def test_print_full_screen_paused_tiny_terminal_no_message(capsys, monkeypatch):
91
106
  """Test that PAUSED message is hidden in tiny terminals with no room."""
92
107
  # Create a 3-line terminal with 3-line timer (no room for PAUSED text)
93
- monkeypatch.setattr("shutil.get_terminal_size", fake_size(20, 3))
108
+ monkeypatch.setattr("countdown.display.get_terminal_size", fake_size(20, 3))
94
109
  lines = ["line1", "line2", "line3"]
95
110
  display.print_full_screen(lines, paused=True)
96
111
  out, err = capsys.readouterr()
97
112
  # Should still show intense magenta color
98
- assert (
99
- "\x1b[95m" in out
100
- ), "Should contain intense magenta color code when paused"
113
+ assert "\x1b[95m" in out, (
114
+ "Should contain intense magenta color code when paused"
115
+ )
101
116
  # Should NOT show PAUSED message (no room)
102
- assert "PAUSED" not in out, "PAUSED message should not appear in tiny terminal"
117
+ assert "PAUSED" not in out, (
118
+ "PAUSED message should not appear in tiny terminal"
119
+ )
103
120
 
104
121
 
105
122
  def test_digit_sizes_available():
@@ -138,40 +155,53 @@ def test_char_heights_match_size():
138
155
 
139
156
  def test_get_chars_for_terminal_selects_largest_that_fits(monkeypatch):
140
157
  """Test that get_chars_for_terminal selects the largest size that fits both dimensions."""
141
- # Size requirements: 16(93w), 7(57w), 5(33w), 3(20w), 1(10w)
158
+ # Size requirements for displaying 00:00:
159
+ # 16(93w), 7(57w), 5(33w), 3(20w), 1(10w)
142
160
 
143
161
  # 80x24 terminal - size 7 fits (57w <= 80, 7h <= 24)
144
- monkeypatch.setattr("shutil.get_terminal_size", fake_size(80, 24))
162
+ monkeypatch.setattr(
163
+ "countdown.display.get_terminal_size",
164
+ fake_size(80, 24),
165
+ )
145
166
  chars = display.get_chars_for_terminal()
146
167
  height = len(chars["0"].splitlines())
147
168
  assert height == 7, "80x24 terminal should select size 7"
148
169
 
149
170
  # 100x24 terminal - size 16 fits (93w <= 100, 16h <= 24)
150
- monkeypatch.setattr("shutil.get_terminal_size", fake_size(100, 24))
171
+ monkeypatch.setattr(
172
+ "countdown.display.get_terminal_size",
173
+ fake_size(100, 24),
174
+ )
151
175
  chars = display.get_chars_for_terminal()
152
176
  height = len(chars["0"].splitlines())
153
177
  assert height == 16, "100x24 terminal should select size 16"
154
178
 
155
179
  # 60x20 terminal - size 7 fits (57w <= 60, 7h <= 20)
156
- monkeypatch.setattr("shutil.get_terminal_size", fake_size(60, 20))
180
+ monkeypatch.setattr(
181
+ "countdown.display.get_terminal_size",
182
+ fake_size(60, 20),
183
+ )
157
184
  chars = display.get_chars_for_terminal()
158
185
  height = len(chars["0"].splitlines())
159
186
  assert height == 7, "60x20 terminal should select size 7"
160
187
 
161
188
  # 32x10 terminal - size 3 fits (20w <= 32, 3h <= 10)
162
- monkeypatch.setattr("shutil.get_terminal_size", fake_size(32, 10))
189
+ monkeypatch.setattr(
190
+ "countdown.display.get_terminal_size",
191
+ fake_size(32, 10),
192
+ )
163
193
  chars = display.get_chars_for_terminal()
164
194
  height = len(chars["0"].splitlines())
165
195
  assert height == 3, "32x10 terminal should select size 3"
166
196
 
167
197
  # 15x5 terminal - size 1 fits (10w <= 15, 1h <= 5)
168
- monkeypatch.setattr("shutil.get_terminal_size", fake_size(15, 5))
198
+ monkeypatch.setattr("countdown.display.get_terminal_size", fake_size(15, 5))
169
199
  chars = display.get_chars_for_terminal()
170
200
  height = len(chars["0"].splitlines())
171
201
  assert height == 1, "15x5 terminal should select size 1"
172
202
 
173
203
  # Very small terminal - falls back to smallest
174
- monkeypatch.setattr("shutil.get_terminal_size", fake_size(5, 1))
204
+ monkeypatch.setattr("countdown.display.get_terminal_size", fake_size(5, 1))
175
205
  chars = display.get_chars_for_terminal()
176
206
  height = len(chars["0"].splitlines())
177
207
  assert height == 1, "5x1 terminal should fall back to size 1"
@@ -182,19 +212,25 @@ def test_different_sizes_render_correctly(monkeypatch):
182
212
  from countdown import timer
183
213
 
184
214
  # Test size 7 rendering (80x24 selects size 7)
185
- monkeypatch.setattr("shutil.get_terminal_size", fake_size(80, 24))
215
+ monkeypatch.setattr(
216
+ "countdown.display.get_terminal_size",
217
+ fake_size(80, 24),
218
+ )
186
219
  chars = display.get_chars_for_terminal()
187
220
  lines = timer.get_number_lines(0, chars) # 00:00
188
221
  assert len(lines) == 7, "80x24 terminal should render 7 lines"
189
222
 
190
223
  # Test size 3 rendering (32x10 selects size 3)
191
- monkeypatch.setattr("shutil.get_terminal_size", fake_size(32, 10))
224
+ monkeypatch.setattr(
225
+ "countdown.display.get_terminal_size",
226
+ fake_size(32, 10),
227
+ )
192
228
  chars = display.get_chars_for_terminal()
193
229
  lines = timer.get_number_lines(0, chars) # 00:00
194
230
  assert len(lines) == 3, "32x10 terminal should render 3 lines"
195
231
 
196
232
  # Test size 1 rendering (15x5 selects size 1)
197
- monkeypatch.setattr("shutil.get_terminal_size", fake_size(15, 5))
233
+ monkeypatch.setattr("countdown.display.get_terminal_size", fake_size(15, 5))
198
234
  chars = display.get_chars_for_terminal()
199
235
  lines = timer.get_number_lines(0, chars) # 00:00
200
236
  assert len(lines) == 1, "15x5 terminal should render 1 line"
@@ -203,19 +239,46 @@ def test_different_sizes_render_correctly(monkeypatch):
203
239
  def test_width_constraints_force_smaller_size(monkeypatch):
204
240
  """Test that narrow terminal widths force selection of smaller digit sizes."""
205
241
  # Size 7 requires 57 width - a 56x20 terminal should select size 5 instead
206
- monkeypatch.setattr("shutil.get_terminal_size", fake_size(56, 20))
242
+ monkeypatch.setattr(
243
+ "countdown.display.get_terminal_size",
244
+ fake_size(56, 20),
245
+ )
207
246
  chars = display.get_chars_for_terminal()
208
247
  height = len(chars["0"].splitlines())
209
- assert height == 5, "56x20 terminal too narrow for size 7, should select size 5"
248
+ assert height == 5, (
249
+ "56x20 terminal too narrow for size 7, should select size 5"
250
+ )
210
251
 
211
252
  # Size 5 requires 33 width - a 32x10 terminal should select size 3 instead
212
- monkeypatch.setattr("shutil.get_terminal_size", fake_size(32, 10))
253
+ monkeypatch.setattr(
254
+ "countdown.display.get_terminal_size",
255
+ fake_size(32, 10),
256
+ )
213
257
  chars = display.get_chars_for_terminal()
214
258
  height = len(chars["0"].splitlines())
215
- assert height == 3, "32x10 terminal too narrow for size 5, should select size 3"
259
+ assert height == 3, (
260
+ "32x10 terminal too narrow for size 5, should select size 3"
261
+ )
216
262
 
217
263
  # Size 3 requires 20 width - a 19x5 terminal should select size 1 instead
218
- monkeypatch.setattr("shutil.get_terminal_size", fake_size(19, 5))
264
+ monkeypatch.setattr("countdown.display.get_terminal_size", fake_size(19, 5))
219
265
  chars = display.get_chars_for_terminal()
220
266
  height = len(chars["0"].splitlines())
221
- assert height == 1, "19x5 terminal too narrow for size 3, should select size 1"
267
+ assert height == 1, (
268
+ "19x5 terminal too narrow for size 3, should select size 1"
269
+ )
270
+
271
+
272
+ def test_three_digit_minutes_force_smaller_chars(monkeypatch):
273
+ """Wide minute values should fall back to smaller glyphs if needed."""
274
+ # 60x20 terminal can show size 7 for two-digit minutes
275
+ monkeypatch.setattr(
276
+ "countdown.display.get_terminal_size",
277
+ fake_size(60, 20),
278
+ )
279
+ chars = display.get_chars_for_terminal(0)
280
+ assert len(chars["0"].splitlines()) == 7
281
+
282
+ # But 100 minutes needs 70 columns at size 7, so we should drop to size 5
283
+ chars = display.get_chars_for_terminal(6000) # 100 minutes
284
+ assert len(chars["0"].splitlines()) == 5
@@ -8,7 +8,9 @@ def test_is_pause_key_with_strings():
8
8
  assert keys.is_pause_key(" ") is True, "Space should be a pause key"
9
9
  assert keys.is_pause_key("p") is True, "p should be a pause key"
10
10
  assert keys.is_pause_key("k") is True, "k should be a pause key"
11
- assert keys.is_pause_key("\r") is True, "Carriage return should be a pause key"
11
+ assert keys.is_pause_key("\r") is True, (
12
+ "Carriage return should be a pause key"
13
+ )
12
14
  assert keys.is_pause_key("\n") is True, "Newline should be a pause key"
13
15
  assert keys.is_pause_key("a") is False, "a should not be a pause key"
14
16
  assert keys.is_pause_key("x") is False, "x should not be a pause key"
@@ -20,10 +22,12 @@ def test_is_time_adjust_key_with_strings():
20
22
  assert keys.is_time_adjust_key("+") is True, "+ should be a time adjust key"
21
23
  assert keys.is_time_adjust_key("=") is True, "= should be a time adjust key"
22
24
  assert keys.is_time_adjust_key("-") is True, "- should be a time adjust key"
23
- assert keys.is_time_adjust_key("a") is False, "a should not be a time adjust key"
24
- assert (
25
- keys.is_time_adjust_key(" ") is False
26
- ), "space should not be a time adjust key"
25
+ assert keys.is_time_adjust_key("a") is False, (
26
+ "a should not be a time adjust key"
27
+ )
28
+ assert keys.is_time_adjust_key(" ") is False, (
29
+ "space should not be a time adjust key"
30
+ )
27
31
 
28
32
 
29
33
  def test_get_time_adjustment():