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.
- countdown_cli-2.1.0/.uv-cache/.gitignore +1 -0
- countdown_cli-2.1.0/.uv-cache/.lock +0 -0
- countdown_cli-2.1.0/.uv-cache/CACHEDIR.TAG +1 -0
- countdown_cli-2.1.0/.uv-cache/interpreter-v4/3ae53c40791d22fd/16472b94e5618f48.msgpack +0 -0
- countdown_cli-2.1.0/.uv-cache/sdists-v9/.git +0 -0
- countdown_cli-2.1.0/.uv-cache/sdists-v9/.gitignore +0 -0
- countdown_cli-2.1.0/AGENTS.md +12 -0
- countdown_cli-2.1.0/CONTRIBUTING.rst +75 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/PKG-INFO +9 -11
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/README.rst +7 -9
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/pyproject.toml +2 -10
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/src/countdown/__main__.py +5 -5
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/src/countdown/digits.py +7 -2
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/src/countdown/display.py +26 -13
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/src/countdown/terminal.py +2 -2
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/src/countdown/timer.py +2 -2
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/tests/test_display.py +92 -29
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/tests/test_keys.py +9 -5
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/tests/test_main.py +93 -40
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/tests/test_timer.py +16 -0
- countdown_cli-2.1.0/uv.lock +319 -0
- countdown_cli-2.0.0/CONTRIBUTING.rst +0 -121
- countdown_cli-2.0.0/uv.lock +0 -319
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/.claude/settings.local.json +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/.envrc +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/.gitattributes +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/.github/labels.yml +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/.github/release-drafter.yml +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/.github/workflows/labeler.yml +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/.github/workflows/release.yml +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/.github/workflows/tests.yml +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/.gitignore +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/.pre-commit-config.yaml +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/.python-version-default +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/CODE_OF_CONDUCT.rst +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/LICENSE.rst +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/codecov.yml +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/.gitignore +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/class_index.html +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/coverage_html_cb_6fb7b396.js +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/coverage_html_cb_da166b87.js +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/favicon_32_cb_58284776.png +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/function_index.html +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/index.html +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/keybd_closed_cb_ce680311.png +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/status.json +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/style_cb_6b508a39.css +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/style_cb_8e611ae1.css +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/z_55719c21e2af63da___init___py.html +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/z_55719c21e2af63da___main___py.html +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/z_55719c21e2af63da_digits_py.html +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/z_55719c21e2af63da_display_py.html +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/z_55719c21e2af63da_keys_py.html +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/z_55719c21e2af63da_terminal_py.html +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/z_55719c21e2af63da_timer_py.html +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/z_a44f0ac069e85531___init___py.html +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/z_a44f0ac069e85531_conftest_py.html +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/z_a44f0ac069e85531_test_display_py.html +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/z_a44f0ac069e85531_test_keys_py.html +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/z_a44f0ac069e85531_test_main_py.html +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/z_a44f0ac069e85531_test_terminal_py.html +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/htmlcov/z_a44f0ac069e85531_test_timer_py.html +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/images/1457.png +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/images/3253.png +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/images/python-morsels-logo.png +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/justfile +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/noxfile.py +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/src/countdown/__init__.py +0 -0
- /countdown_cli-2.0.0/src/countdown/numbers.txt → /countdown_cli-2.1.0/src/countdown/glyphs.txt +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/src/countdown/keys.py +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/tests/__init__.py +0 -0
- {countdown_cli-2.0.0 → countdown_cli-2.1.0}/tests/conftest.py +0 -0
- {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
|
|
Binary file
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
34
|
-
lines =
|
|
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
|
|
39
|
-
"""
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
48
|
-
"""
|
|
49
|
-
|
|
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 =
|
|
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
|
|
21
|
+
return select([sys.stdin], [], [], 0)[0]
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def read_key(): # pragma: no cover
|
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
"
|
|
71
|
-
)
|
|
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(
|
|
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("
|
|
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
|
-
"
|
|
100
|
-
)
|
|
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,
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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("
|
|
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("
|
|
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(
|
|
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(
|
|
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("
|
|
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(
|
|
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,
|
|
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(
|
|
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,
|
|
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("
|
|
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,
|
|
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,
|
|
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,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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():
|