term-clock-app 1.0.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.
- term_clock_app-1.0.0/LICENSE +21 -0
- term_clock_app-1.0.0/PKG-INFO +136 -0
- term_clock_app-1.0.0/README.md +84 -0
- term_clock_app-1.0.0/pyproject.toml +47 -0
- term_clock_app-1.0.0/setup.cfg +4 -0
- term_clock_app-1.0.0/term_clock/__init__.py +3 -0
- term_clock_app-1.0.0/term_clock/__main__.py +6 -0
- term_clock_app-1.0.0/term_clock/cli.py +203 -0
- term_clock_app-1.0.0/term_clock/core.py +345 -0
- term_clock_app-1.0.0/term_clock_app.egg-info/PKG-INFO +136 -0
- term_clock_app-1.0.0/term_clock_app.egg-info/SOURCES.txt +15 -0
- term_clock_app-1.0.0/term_clock_app.egg-info/dependency_links.txt +1 -0
- term_clock_app-1.0.0/term_clock_app.egg-info/entry_points.txt +2 -0
- term_clock_app-1.0.0/term_clock_app.egg-info/requires.txt +3 -0
- term_clock_app-1.0.0/term_clock_app.egg-info/top_level.txt +1 -0
- term_clock_app-1.0.0/tests/test_cli.py +179 -0
- term_clock_app-1.0.0/tests/test_core.py +271 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lorenzo Wood
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT ANY WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: term-clock-app
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A terminal digital clock that scales to fill the window.
|
|
5
|
+
Author: Lorenzo Wood
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Lorenzo Wood
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT ANY WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/lorenzowood/term-clock
|
|
29
|
+
Project-URL: Repository, https://github.com/lorenzowood/term-clock
|
|
30
|
+
Project-URL: Issues, https://github.com/lorenzowood/term-clock/issues
|
|
31
|
+
Keywords: clock,terminal,tui,ssh
|
|
32
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
33
|
+
Classifier: Environment :: Console
|
|
34
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
36
|
+
Classifier: Operating System :: POSIX
|
|
37
|
+
Classifier: Programming Language :: Python :: 3
|
|
38
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
43
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
44
|
+
Classifier: Topic :: Terminals
|
|
45
|
+
Classifier: Topic :: Utilities
|
|
46
|
+
Requires-Python: >=3.9
|
|
47
|
+
Description-Content-Type: text/markdown
|
|
48
|
+
License-File: LICENSE
|
|
49
|
+
Provides-Extra: dev
|
|
50
|
+
Requires-Dist: pytest; extra == "dev"
|
|
51
|
+
Dynamic: license-file
|
|
52
|
+
|
|
53
|
+
# term-clock
|
|
54
|
+
|
|
55
|
+
A digital clock for the terminal. It shows `hh:mm:ss`, stays on the wall-clock
|
|
56
|
+
second, and grows to fill the window.
|
|
57
|
+
|
|
58
|
+
Works over SSH the same way it works locally: alternate screen, no flicker,
|
|
59
|
+
CTRL+C returns you to the prompt.
|
|
60
|
+
|
|
61
|
+
## Install
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
pip install term-clock-app
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The PyPI name is `term-clock-app` because `term-clock` / `termclock` is already taken. The command you run is still `term-clock`.
|
|
68
|
+
|
|
69
|
+
From a clone:
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
pip install -e .
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Python 3.9 or newer. No runtime dependencies.
|
|
76
|
+
|
|
77
|
+
## Run
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
term-clock
|
|
81
|
+
# or
|
|
82
|
+
python -m term_clock
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
term-clock --padding 1 --spacing 2 # defaults
|
|
87
|
+
term-clock --padding 2 --spacing 4
|
|
88
|
+
term-clock --hour-format 12
|
|
89
|
+
term-clock --hour-format 24
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Press **CTRL+C** to quit.
|
|
93
|
+
|
|
94
|
+
## Options
|
|
95
|
+
|
|
96
|
+
| Flag | Default | Meaning |
|
|
97
|
+
| --- | --- | --- |
|
|
98
|
+
| `--padding N` | `1` | Blank rows and columns on every side |
|
|
99
|
+
| `--spacing N` | `2` | Blank columns between digits |
|
|
100
|
+
| `--hour-format {12,24}` | system clock, or `24` | 12-hour with AM/PM, or 24-hour |
|
|
101
|
+
|
|
102
|
+
The default hour format follows the environment's time locale
|
|
103
|
+
(`LC_TIME` / `T_FMT`) when that can be read. If it cannot, the clock uses 24-hour.
|
|
104
|
+
|
|
105
|
+
## Display
|
|
106
|
+
|
|
107
|
+
- **7 or fewer lines**: a plain text clock, centred.
|
|
108
|
+
- **8+ lines**: large seven-segment digits drawn with full blocks `█` for
|
|
109
|
+
horizontal and vertical bars, and the four triangles `◤ ◥ ◣ ◢` for 45°
|
|
110
|
+
corner cuts (one column per row — never sampled, never an off-angle
|
|
111
|
+
diagonal). They grow to fill the window (each axis stretched by at most
|
|
112
|
+
1.5× before the rest becomes centring margin). If the window is too small
|
|
113
|
+
for a readable clock, it falls back to the text version.
|
|
114
|
+
- **Colons**: two small axis-aligned blocks (no diagonals), centred, spanning
|
|
115
|
+
at most one third of the digit height.
|
|
116
|
+
- **12-hour**: AM or PM sits to the right of the digits.
|
|
117
|
+
- **Timing**: after each flip the process sleeps until the next wall-clock
|
|
118
|
+
second, then measures how late or early it woke (target: within 2 ms) and
|
|
119
|
+
shortens or lengthens the following sleep. A resize is picked up on the
|
|
120
|
+
next second. Unchanged cells are not rewritten.
|
|
121
|
+
|
|
122
|
+
## Develop
|
|
123
|
+
|
|
124
|
+
```sh
|
|
125
|
+
pip install -e ".[dev]"
|
|
126
|
+
pytest
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Pure rendering lives in `term_clock/core.py` (unit-tested). The terminal loop
|
|
130
|
+
is in `term_clock/cli.py`.
|
|
131
|
+
|
|
132
|
+
See `DESIGN.md` for the design and the TDD log.
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
MIT. See `LICENSE`.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# term-clock
|
|
2
|
+
|
|
3
|
+
A digital clock for the terminal. It shows `hh:mm:ss`, stays on the wall-clock
|
|
4
|
+
second, and grows to fill the window.
|
|
5
|
+
|
|
6
|
+
Works over SSH the same way it works locally: alternate screen, no flicker,
|
|
7
|
+
CTRL+C returns you to the prompt.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
pip install term-clock-app
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The PyPI name is `term-clock-app` because `term-clock` / `termclock` is already taken. The command you run is still `term-clock`.
|
|
16
|
+
|
|
17
|
+
From a clone:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
pip install -e .
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Python 3.9 or newer. No runtime dependencies.
|
|
24
|
+
|
|
25
|
+
## Run
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
term-clock
|
|
29
|
+
# or
|
|
30
|
+
python -m term_clock
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
term-clock --padding 1 --spacing 2 # defaults
|
|
35
|
+
term-clock --padding 2 --spacing 4
|
|
36
|
+
term-clock --hour-format 12
|
|
37
|
+
term-clock --hour-format 24
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Press **CTRL+C** to quit.
|
|
41
|
+
|
|
42
|
+
## Options
|
|
43
|
+
|
|
44
|
+
| Flag | Default | Meaning |
|
|
45
|
+
| --- | --- | --- |
|
|
46
|
+
| `--padding N` | `1` | Blank rows and columns on every side |
|
|
47
|
+
| `--spacing N` | `2` | Blank columns between digits |
|
|
48
|
+
| `--hour-format {12,24}` | system clock, or `24` | 12-hour with AM/PM, or 24-hour |
|
|
49
|
+
|
|
50
|
+
The default hour format follows the environment's time locale
|
|
51
|
+
(`LC_TIME` / `T_FMT`) when that can be read. If it cannot, the clock uses 24-hour.
|
|
52
|
+
|
|
53
|
+
## Display
|
|
54
|
+
|
|
55
|
+
- **7 or fewer lines**: a plain text clock, centred.
|
|
56
|
+
- **8+ lines**: large seven-segment digits drawn with full blocks `█` for
|
|
57
|
+
horizontal and vertical bars, and the four triangles `◤ ◥ ◣ ◢` for 45°
|
|
58
|
+
corner cuts (one column per row — never sampled, never an off-angle
|
|
59
|
+
diagonal). They grow to fill the window (each axis stretched by at most
|
|
60
|
+
1.5× before the rest becomes centring margin). If the window is too small
|
|
61
|
+
for a readable clock, it falls back to the text version.
|
|
62
|
+
- **Colons**: two small axis-aligned blocks (no diagonals), centred, spanning
|
|
63
|
+
at most one third of the digit height.
|
|
64
|
+
- **12-hour**: AM or PM sits to the right of the digits.
|
|
65
|
+
- **Timing**: after each flip the process sleeps until the next wall-clock
|
|
66
|
+
second, then measures how late or early it woke (target: within 2 ms) and
|
|
67
|
+
shortens or lengthens the following sleep. A resize is picked up on the
|
|
68
|
+
next second. Unchanged cells are not rewritten.
|
|
69
|
+
|
|
70
|
+
## Develop
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
pip install -e ".[dev]"
|
|
74
|
+
pytest
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Pure rendering lives in `term_clock/core.py` (unit-tested). The terminal loop
|
|
78
|
+
is in `term_clock/cli.py`.
|
|
79
|
+
|
|
80
|
+
See `DESIGN.md` for the design and the TDD log.
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
MIT. See `LICENSE`.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "term-clock-app"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "A terminal digital clock that scales to fill the window."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { file = "LICENSE" }
|
|
12
|
+
authors = [{ name = "Lorenzo Wood" }]
|
|
13
|
+
keywords = ["clock", "terminal", "tui", "ssh"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 5 - Production/Stable",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: End Users/Desktop",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: POSIX",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Topic :: Terminals",
|
|
28
|
+
"Topic :: Utilities",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/lorenzowood/term-clock"
|
|
33
|
+
Repository = "https://github.com/lorenzowood/term-clock"
|
|
34
|
+
Issues = "https://github.com/lorenzowood/term-clock/issues"
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
term-clock = "term_clock.cli:main"
|
|
38
|
+
|
|
39
|
+
[project.optional-dependencies]
|
|
40
|
+
dev = ["pytest"]
|
|
41
|
+
|
|
42
|
+
[tool.setuptools]
|
|
43
|
+
packages = ["term_clock"]
|
|
44
|
+
|
|
45
|
+
[tool.pytest.ini_options]
|
|
46
|
+
pythonpath = ["."]
|
|
47
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""Terminal runtime: paints the clock and stays current until CTRL+C."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import shutil
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
from typing import Callable, Sequence, TextIO
|
|
10
|
+
|
|
11
|
+
from . import core
|
|
12
|
+
|
|
13
|
+
_ALT_ON = "\x1b[?1049h"
|
|
14
|
+
_ALT_OFF = "\x1b[?1049l"
|
|
15
|
+
_CURSOR_HIDE = "\x1b[?25l"
|
|
16
|
+
_CURSOR_SHOW = "\x1b[?25h"
|
|
17
|
+
_WRAP_OFF = "\x1b[?7l"
|
|
18
|
+
_WRAP_ON = "\x1b[?7h"
|
|
19
|
+
# Home, erase the visible screen, erase the scrollback. A full-width last
|
|
20
|
+
# line would otherwise wrap and push every frame into the history.
|
|
21
|
+
_CLEAR = "\x1b[H\x1b[2J\x1b[3J"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _nonneg(value: str) -> int:
|
|
25
|
+
n = int(value)
|
|
26
|
+
if n < 0:
|
|
27
|
+
raise argparse.ArgumentTypeError("must be >= 0")
|
|
28
|
+
return n
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_TOLERANCE_MS = 2.0
|
|
32
|
+
_LEAD_MAX_MS = 100.0
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def next_second_ms(now_ms: float) -> float:
|
|
36
|
+
"""Wall-clock millisecond of the next whole second after ``now_ms``."""
|
|
37
|
+
return (int(now_ms) // 1000 + 1) * 1000.0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def sleep_ms(now_ms: float, target_ms: float, lead_ms: float) -> float:
|
|
41
|
+
"""How long to sleep now so we arrive at ``target_ms``, given ``lead_ms``.
|
|
42
|
+
|
|
43
|
+
``lead_ms`` is the estimated sleep overshoot, subtracted from the wait.
|
|
44
|
+
It is never allowed to skip past the boundary.
|
|
45
|
+
"""
|
|
46
|
+
remain = target_ms - now_ms
|
|
47
|
+
if remain <= 0:
|
|
48
|
+
return 0.0
|
|
49
|
+
wait = remain - lead_ms
|
|
50
|
+
if wait <= 0:
|
|
51
|
+
wait = remain
|
|
52
|
+
return wait
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def adjust_lead_ms(lead_ms: float, error_ms: float) -> float:
|
|
56
|
+
"""If we missed the second by more than 2 ms, shift the next sleep."""
|
|
57
|
+
if abs(error_ms) <= _TOLERANCE_MS:
|
|
58
|
+
return lead_ms
|
|
59
|
+
return min(_LEAD_MAX_MS, max(0.0, lead_ms + error_ms))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _wall_ms() -> float:
|
|
63
|
+
return time.time_ns() / 1_000_000.0
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
67
|
+
p = argparse.ArgumentParser(
|
|
68
|
+
prog="term-clock",
|
|
69
|
+
description="A terminal digital clock that scales to fill the window.",
|
|
70
|
+
)
|
|
71
|
+
p.add_argument(
|
|
72
|
+
"--padding",
|
|
73
|
+
type=_nonneg,
|
|
74
|
+
default=1,
|
|
75
|
+
metavar="N",
|
|
76
|
+
help="blank rows and columns around the clock (default: 1)",
|
|
77
|
+
)
|
|
78
|
+
p.add_argument(
|
|
79
|
+
"--spacing",
|
|
80
|
+
type=_nonneg,
|
|
81
|
+
default=2,
|
|
82
|
+
metavar="N",
|
|
83
|
+
help="blank columns between digits (default: 2)",
|
|
84
|
+
)
|
|
85
|
+
p.add_argument(
|
|
86
|
+
"--hour-format",
|
|
87
|
+
choices=("12", "24"),
|
|
88
|
+
default=None,
|
|
89
|
+
dest="hour_format",
|
|
90
|
+
help="12 or 24 (default: follow the system clock, or 24 if unknown)",
|
|
91
|
+
)
|
|
92
|
+
return p.parse_args(argv)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def frame_for(
|
|
96
|
+
t: time.struct_time,
|
|
97
|
+
cols: int,
|
|
98
|
+
rows: int,
|
|
99
|
+
style: core.Style | None = None,
|
|
100
|
+
) -> list[str]:
|
|
101
|
+
"""Build the screen buffer for time ``t`` at the given terminal size."""
|
|
102
|
+
style = style or core.Style()
|
|
103
|
+
time_str = core.format_time(
|
|
104
|
+
t.tm_hour, t.tm_min, t.tm_sec, hour_format=style.hour_format
|
|
105
|
+
)
|
|
106
|
+
suffix = core.hour_period(t.tm_hour) if style.hour_format == "12" else ""
|
|
107
|
+
return core.render(time_str, rows=rows, cols=cols, style=style, suffix=suffix)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _write_diff(out: TextIO, old: Sequence[str], new: Sequence[str]) -> None:
|
|
111
|
+
"""Emit cursor-addressed runs for cells that differ."""
|
|
112
|
+
for y, (a, b) in enumerate(zip(old, new)):
|
|
113
|
+
if a == b:
|
|
114
|
+
continue
|
|
115
|
+
width = max(len(a), len(b))
|
|
116
|
+
a = a.ljust(width)
|
|
117
|
+
b = b.ljust(width)
|
|
118
|
+
x = 0
|
|
119
|
+
while x < width:
|
|
120
|
+
if a[x] == b[x]:
|
|
121
|
+
x += 1
|
|
122
|
+
continue
|
|
123
|
+
start = x
|
|
124
|
+
while x < width and a[x] != b[x]:
|
|
125
|
+
x += 1
|
|
126
|
+
out.write(f"\x1b[{y + 1};{start + 1}H{b[start:x]}")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class Painter:
|
|
130
|
+
"""Off-screen current/next buffers; only changed cells are written."""
|
|
131
|
+
|
|
132
|
+
def __init__(self, out: TextIO) -> None:
|
|
133
|
+
self._out = out
|
|
134
|
+
self._last: list[str] | None = None
|
|
135
|
+
|
|
136
|
+
def paint(self, frame: Sequence[str]) -> None:
|
|
137
|
+
frame = list(frame)
|
|
138
|
+
if frame == self._last:
|
|
139
|
+
return
|
|
140
|
+
if self._last is None or len(self._last) != len(frame):
|
|
141
|
+
self._out.write(_CLEAR + "\n".join(frame))
|
|
142
|
+
else:
|
|
143
|
+
_write_diff(self._out, self._last, frame)
|
|
144
|
+
self._last = frame
|
|
145
|
+
self._out.flush()
|
|
146
|
+
|
|
147
|
+
def invalidate(self) -> None:
|
|
148
|
+
self._last = None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def run(
|
|
152
|
+
out: TextIO | None = None,
|
|
153
|
+
get_size: Callable[[], tuple[int, int]] | None = None,
|
|
154
|
+
get_time: Callable[[], time.struct_time] | None = None,
|
|
155
|
+
sleep: Callable[[float], None] = time.sleep,
|
|
156
|
+
style: core.Style | None = None,
|
|
157
|
+
now_ms: Callable[[], float] | None = None,
|
|
158
|
+
) -> int:
|
|
159
|
+
out = out if out is not None else sys.stdout
|
|
160
|
+
get_size = get_size or (lambda: tuple(shutil.get_terminal_size((80, 24))))
|
|
161
|
+
get_time = get_time or time.localtime
|
|
162
|
+
now_ms = now_ms or _wall_ms
|
|
163
|
+
|
|
164
|
+
out.write(_ALT_ON + _CURSOR_HIDE + _WRAP_OFF)
|
|
165
|
+
out.flush()
|
|
166
|
+
painter = Painter(out)
|
|
167
|
+
last_size: tuple[int, int] | None = None
|
|
168
|
+
lead_ms = 0.0
|
|
169
|
+
|
|
170
|
+
def paint() -> None:
|
|
171
|
+
nonlocal last_size
|
|
172
|
+
cols, rows = get_size()
|
|
173
|
+
if (cols, rows) != last_size:
|
|
174
|
+
painter.invalidate()
|
|
175
|
+
last_size = (cols, rows)
|
|
176
|
+
painter.paint(frame_for(get_time(), cols=cols, rows=rows, style=style))
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
while True:
|
|
180
|
+
paint()
|
|
181
|
+
target = next_second_ms(now_ms())
|
|
182
|
+
now = now_ms()
|
|
183
|
+
wait = sleep_ms(now, target, lead_ms)
|
|
184
|
+
if wait > 0:
|
|
185
|
+
sleep(wait / 1000.0)
|
|
186
|
+
lead_ms = adjust_lead_ms(lead_ms, now_ms() - target)
|
|
187
|
+
except KeyboardInterrupt:
|
|
188
|
+
return 0
|
|
189
|
+
finally:
|
|
190
|
+
out.write(_WRAP_ON + _CURSOR_SHOW + _ALT_OFF)
|
|
191
|
+
out.flush()
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def main(argv: list[str] | None = None) -> int:
|
|
195
|
+
ns = parse_args(argv)
|
|
196
|
+
hour_format = ns.hour_format or core.system_hour_format()
|
|
197
|
+
return run(
|
|
198
|
+
style=core.Style(
|
|
199
|
+
padding=ns.padding,
|
|
200
|
+
spacing=ns.spacing,
|
|
201
|
+
hour_format=hour_format,
|
|
202
|
+
),
|
|
203
|
+
)
|