tzclock-cli 0.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.
- tzclock_cli-0.1.0/LICENSE +21 -0
- tzclock_cli-0.1.0/MANIFEST.in +2 -0
- tzclock_cli-0.1.0/PKG-INFO +109 -0
- tzclock_cli-0.1.0/PUBLISH.md +90 -0
- tzclock_cli-0.1.0/README.md +91 -0
- tzclock_cli-0.1.0/pyproject.toml +33 -0
- tzclock_cli-0.1.0/setup.cfg +4 -0
- tzclock_cli-0.1.0/src/tzclock/__init__.py +3 -0
- tzclock_cli-0.1.0/src/tzclock/__main__.py +4 -0
- tzclock_cli-0.1.0/src/tzclock/cli.py +173 -0
- tzclock_cli-0.1.0/src/tzclock_cli.egg-info/PKG-INFO +109 -0
- tzclock_cli-0.1.0/src/tzclock_cli.egg-info/SOURCES.txt +15 -0
- tzclock_cli-0.1.0/src/tzclock_cli.egg-info/dependency_links.txt +1 -0
- tzclock_cli-0.1.0/src/tzclock_cli.egg-info/entry_points.txt +2 -0
- tzclock_cli-0.1.0/src/tzclock_cli.egg-info/requires.txt +4 -0
- tzclock_cli-0.1.0/src/tzclock_cli.egg-info/top_level.txt +1 -0
- tzclock_cli-0.1.0/tests/test_clock.py +48 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 tzclock contributors
|
|
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 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,109 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tzclock-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A small live timezone clock with a searchable terminal menu
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Keywords: timezone,clock,terminal,curses,cli
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Environment :: Console :: Curses
|
|
9
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
10
|
+
Classifier: Operating System :: MacOS
|
|
11
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: tzdata
|
|
16
|
+
Requires-Dist: windows-curses>=2.4; sys_platform == "win32"
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# tzclock
|
|
20
|
+
|
|
21
|
+
A small live timezone converter for the terminal. Reads your system clock and
|
|
22
|
+
shows local time alongside a selected timezone, including the date, UTC offset,
|
|
23
|
+
and date difference. No network requests during use.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
Requires Python 3.9 or newer. Once published to PyPI:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
python -m pip install tzclock-cli
|
|
31
|
+
tzclock
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Use `python3` on macOS/Linux or `py` on Windows if that is your Python launcher.
|
|
35
|
+
For an isolated command-line installation, `pipx install tzclock-cli` also works
|
|
36
|
+
if you already use pipx. In a conda environment, activate it and use the same pip
|
|
37
|
+
command; a separate conda package is not required.
|
|
38
|
+
|
|
39
|
+
Before publication, install a supplied wheel:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
python -m pip install ./dist/tzclock_cli-0.1.0-py3-none-any.whl
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Pip creates the `tzclock` launcher, including an executable launcher on Windows.
|
|
46
|
+
If your Python scripts directory is not on PATH, use `python -m tzclock` instead.
|
|
47
|
+
No copying into system directories or manually setting executable permissions.
|
|
48
|
+
|
|
49
|
+
## Use
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
tzclock
|
|
53
|
+
tzclock --zone Asia/Seoul
|
|
54
|
+
tzclock --zone Asia/Seoul --once
|
|
55
|
+
tzclock --version
|
|
56
|
+
python -m tzclock --zone Europe/Warsaw --once
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
| Key | Action |
|
|
60
|
+
| --- | --- |
|
|
61
|
+
| Up / Down | Choose a timezone |
|
|
62
|
+
| Enter | Apply the highlighted timezone |
|
|
63
|
+
| / | Search all installed IANA timezones |
|
|
64
|
+
| Page Up / Page Down | Scroll a page |
|
|
65
|
+
| Backspace | Edit search |
|
|
66
|
+
| Esc | Leave search and return to favorites |
|
|
67
|
+
| Q | Quit outside search |
|
|
68
|
+
|
|
69
|
+
During search, Q is a normal letter; press Esc then Q to quit. Ctrl+C also exits.
|
|
70
|
+
Quick choices include Warsaw, Seoul, Taipei, Hong Kong, Tokyo and UTC.
|
|
71
|
+
The menu needs at least 48 columns by 16 rows. Resize the terminal if prompted.
|
|
72
|
+
Redirected output automatically uses the one-shot text mode.
|
|
73
|
+
|
|
74
|
+
## Platform handling
|
|
75
|
+
|
|
76
|
+
Dependencies are declared in `pyproject.toml`, not installed by application code:
|
|
77
|
+
|
|
78
|
+
```toml
|
|
79
|
+
dependencies = [
|
|
80
|
+
"tzdata",
|
|
81
|
+
"windows-curses>=2.4; sys_platform == 'win32'",
|
|
82
|
+
]
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Pip installs `windows-curses` only on Windows. macOS and Linux use their Python
|
|
86
|
+
build's standard curses module. The small `tzdata` package is installed on all
|
|
87
|
+
platforms as a fallback when no system timezone database is available. Python's
|
|
88
|
+
zoneinfo normally uses system timezone data first. DST rules come from that
|
|
89
|
+
database; keep your system and dependencies updated.
|
|
90
|
+
|
|
91
|
+
Interactive Windows support depends on a windows-curses wheel being available
|
|
92
|
+
for your Python version and architecture. Conventional CPython installations
|
|
93
|
+
are the intended target; not every Python implementation or platform is promised.
|
|
94
|
+
On minimal Unix Python builds without curses, use `--once` or a Python build
|
|
95
|
+
with curses support. No administrator privileges are needed inside a virtual
|
|
96
|
+
environment.
|
|
97
|
+
|
|
98
|
+
If you previously copied a standalone `tzclock` into `/usr/local/bin`, that old
|
|
99
|
+
file may take precedence. Check `command -v tzclock` (Windows: `where tzclock`).
|
|
100
|
+
Use `python -m tzclock --version` to verify the installed package directly.
|
|
101
|
+
|
|
102
|
+
## Development
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
python -m pip install -e .
|
|
106
|
+
python -m unittest discover -s tests -v
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
See PUBLISH.md for the short release procedure. MIT licensed.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Publish tzclock to PyPI
|
|
2
|
+
|
|
3
|
+
This folder is a complete source project. `dist/` in the handoff ZIP contains
|
|
4
|
+
the built wheel and source archive for version 0.1.0. Nothing has been uploaded.
|
|
5
|
+
The provisional PyPI distribution name is `tzclock-cli`; the PyPI JSON API returned 404 for that name on 13 October 2026
|
|
6
|
+
(no existing project record). This does not reserve the name or guarantee
|
|
7
|
+
that PyPI will accept it. The shell command is always `tzclock`, independently of that name.
|
|
8
|
+
|
|
9
|
+
## 1. Name and release details
|
|
10
|
+
|
|
11
|
+
Confirm that you can use `tzclock-cli` on https://pypi.org/ before uploading.
|
|
12
|
+
If it is taken or reserved, change only `[project].name` in pyproject.toml to a
|
|
13
|
+
unique name (for example one including your PyPI username), then rebuild.
|
|
14
|
+
Optionally add your preferred author name to project metadata. This project
|
|
15
|
+
uses the included MIT license; review that before publishing.
|
|
16
|
+
|
|
17
|
+
## 2. Prepare publishing tools
|
|
18
|
+
|
|
19
|
+
From this project directory, create a small publishing environment.
|
|
20
|
+
|
|
21
|
+
macOS / Linux:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
python3 -m venv .venv
|
|
25
|
+
source .venv/bin/activate
|
|
26
|
+
python -m pip install --upgrade build twine
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Windows CMD:
|
|
30
|
+
|
|
31
|
+
```bat
|
|
32
|
+
py -m venv .venv
|
|
33
|
+
.venv\Scripts\activate.bat
|
|
34
|
+
python -m pip install --upgrade build twine
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Windows PowerShell: use `.\.venv\Scripts\Activate.ps1` instead, or call
|
|
38
|
+
`.\.venv\Scripts\python.exe` directly if activation is restricted.
|
|
39
|
+
|
|
40
|
+
## 3. Build, check, upload
|
|
41
|
+
|
|
42
|
+
These commands work on each platform. Run from the project directory. The first
|
|
43
|
+
command clears only the generated dist directory so stale releases are not
|
|
44
|
+
accidentally uploaded. Always rebuild after changing metadata or source.
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
python -c "import shutil; shutil.rmtree('dist', ignore_errors=True)"
|
|
48
|
+
python -m build
|
|
49
|
+
python -m twine check dist/*
|
|
50
|
+
python -m twine upload --username __token__ dist/*
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
At the password/API-token prompt, paste your PyPI token. Twine accepts the
|
|
54
|
+
`dist/*` pattern on Windows as well. The upload goes to real PyPI by default.
|
|
55
|
+
No signing, attestations, CI, GitHub setup, or TestPyPI detour is needed for this
|
|
56
|
+
manual workflow. Normal HTTPS and token authentication still apply.
|
|
57
|
+
|
|
58
|
+
For a brand-new project, your token must permit creating it (an account-wide
|
|
59
|
+
token can do that). A token scoped to another existing project cannot create
|
|
60
|
+
this one. Keep the token out of source files, this ZIP, and shell command text.
|
|
61
|
+
|
|
62
|
+
## 4. Give colleagues the install command
|
|
63
|
+
|
|
64
|
+
After a successful upload, using the actual name you published:
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
python -m pip install tzclock-cli
|
|
68
|
+
tzclock
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Windows users can use `py -m pip install tzclock-cli`. The Windows-only dependency
|
|
72
|
+
is selected automatically by pip. They need no separate curses installation.
|
|
73
|
+
Alternatively send them the wheel directly and have them pip-install its path;
|
|
74
|
+
PyPI publication is not necessary for that route. Pip still needs access to any
|
|
75
|
+
missing dependencies.
|
|
76
|
+
|
|
77
|
+
For future releases, change `__version__` in `src/tzclock/__init__.py`, rebuild,
|
|
78
|
+
check and upload. PyPI does not allow replacing an already-used release filename.
|
|
79
|
+
|
|
80
|
+
## Existing standalone installation
|
|
81
|
+
|
|
82
|
+
Your earlier `/usr/local/bin/tzclock` is a separate manual installation. After
|
|
83
|
+
installing the package, check `command -v tzclock` and `tzclock --version`. If the
|
|
84
|
+
old manually copied file wins, move that specific old copy aside. The package
|
|
85
|
+
can always be launched with `python -m tzclock` using the Python where installed.
|
|
86
|
+
|
|
87
|
+
## Reference
|
|
88
|
+
|
|
89
|
+
https://packaging.python.org/en/latest/tutorials/packaging-projects/
|
|
90
|
+
https://packaging.python.org/en/latest/specifications/dependency-specifiers/
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# tzclock
|
|
2
|
+
|
|
3
|
+
A small live timezone converter for the terminal. Reads your system clock and
|
|
4
|
+
shows local time alongside a selected timezone, including the date, UTC offset,
|
|
5
|
+
and date difference. No network requests during use.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Requires Python 3.9 or newer. Once published to PyPI:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
python -m pip install tzclock-cli
|
|
13
|
+
tzclock
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Use `python3` on macOS/Linux or `py` on Windows if that is your Python launcher.
|
|
17
|
+
For an isolated command-line installation, `pipx install tzclock-cli` also works
|
|
18
|
+
if you already use pipx. In a conda environment, activate it and use the same pip
|
|
19
|
+
command; a separate conda package is not required.
|
|
20
|
+
|
|
21
|
+
Before publication, install a supplied wheel:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
python -m pip install ./dist/tzclock_cli-0.1.0-py3-none-any.whl
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Pip creates the `tzclock` launcher, including an executable launcher on Windows.
|
|
28
|
+
If your Python scripts directory is not on PATH, use `python -m tzclock` instead.
|
|
29
|
+
No copying into system directories or manually setting executable permissions.
|
|
30
|
+
|
|
31
|
+
## Use
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
tzclock
|
|
35
|
+
tzclock --zone Asia/Seoul
|
|
36
|
+
tzclock --zone Asia/Seoul --once
|
|
37
|
+
tzclock --version
|
|
38
|
+
python -m tzclock --zone Europe/Warsaw --once
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
| Key | Action |
|
|
42
|
+
| --- | --- |
|
|
43
|
+
| Up / Down | Choose a timezone |
|
|
44
|
+
| Enter | Apply the highlighted timezone |
|
|
45
|
+
| / | Search all installed IANA timezones |
|
|
46
|
+
| Page Up / Page Down | Scroll a page |
|
|
47
|
+
| Backspace | Edit search |
|
|
48
|
+
| Esc | Leave search and return to favorites |
|
|
49
|
+
| Q | Quit outside search |
|
|
50
|
+
|
|
51
|
+
During search, Q is a normal letter; press Esc then Q to quit. Ctrl+C also exits.
|
|
52
|
+
Quick choices include Warsaw, Seoul, Taipei, Hong Kong, Tokyo and UTC.
|
|
53
|
+
The menu needs at least 48 columns by 16 rows. Resize the terminal if prompted.
|
|
54
|
+
Redirected output automatically uses the one-shot text mode.
|
|
55
|
+
|
|
56
|
+
## Platform handling
|
|
57
|
+
|
|
58
|
+
Dependencies are declared in `pyproject.toml`, not installed by application code:
|
|
59
|
+
|
|
60
|
+
```toml
|
|
61
|
+
dependencies = [
|
|
62
|
+
"tzdata",
|
|
63
|
+
"windows-curses>=2.4; sys_platform == 'win32'",
|
|
64
|
+
]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Pip installs `windows-curses` only on Windows. macOS and Linux use their Python
|
|
68
|
+
build's standard curses module. The small `tzdata` package is installed on all
|
|
69
|
+
platforms as a fallback when no system timezone database is available. Python's
|
|
70
|
+
zoneinfo normally uses system timezone data first. DST rules come from that
|
|
71
|
+
database; keep your system and dependencies updated.
|
|
72
|
+
|
|
73
|
+
Interactive Windows support depends on a windows-curses wheel being available
|
|
74
|
+
for your Python version and architecture. Conventional CPython installations
|
|
75
|
+
are the intended target; not every Python implementation or platform is promised.
|
|
76
|
+
On minimal Unix Python builds without curses, use `--once` or a Python build
|
|
77
|
+
with curses support. No administrator privileges are needed inside a virtual
|
|
78
|
+
environment.
|
|
79
|
+
|
|
80
|
+
If you previously copied a standalone `tzclock` into `/usr/local/bin`, that old
|
|
81
|
+
file may take precedence. Check `command -v tzclock` (Windows: `where tzclock`).
|
|
82
|
+
Use `python -m tzclock --version` to verify the installed package directly.
|
|
83
|
+
|
|
84
|
+
## Development
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
python -m pip install -e .
|
|
88
|
+
python -m unittest discover -s tests -v
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
See PUBLISH.md for the short release procedure. MIT licensed.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77.0.3"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tzclock-cli"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "A small live timezone clock with a searchable terminal menu"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
keywords = ["timezone", "clock", "terminal", "curses", "cli"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Environment :: Console :: Curses",
|
|
17
|
+
"Operating System :: Microsoft :: Windows",
|
|
18
|
+
"Operating System :: MacOS",
|
|
19
|
+
"Operating System :: POSIX :: Linux",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"tzdata",
|
|
23
|
+
"windows-curses>=2.4; sys_platform == 'win32'",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
tzclock = "tzclock.cli:main"
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.packages.find]
|
|
30
|
+
where = ["src"]
|
|
31
|
+
|
|
32
|
+
[tool.setuptools.dynamic]
|
|
33
|
+
version = {attr = "tzclock.__version__"}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Live system-clock timezone converter. Python 3.9+; macOS/Linux terminal.
|
|
3
|
+
|
|
4
|
+
Run: tzclock
|
|
5
|
+
tzclock --zone Asia/Seoul --once
|
|
6
|
+
|
|
7
|
+
Arrows select a menu item; Enter applies it. / searches all IANA zones.
|
|
8
|
+
Esc leaves search, F returns to favorites, Q quits outside search.
|
|
9
|
+
Uses the machine's clock and installed timezone database; no network access.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
from . import __version__
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
import sys
|
|
16
|
+
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError, available_timezones
|
|
17
|
+
|
|
18
|
+
FAVORITES = [
|
|
19
|
+
"Europe/Warsaw", "Asia/Seoul", "Asia/Taipei", "Asia/Hong_Kong",
|
|
20
|
+
"Asia/Tokyo", "UTC", "Europe/London", "America/New_York",
|
|
21
|
+
"America/Los_Angeles", "Australia/Sydney",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def clock_lines(zone, now=None):
|
|
26
|
+
"""Convert one aware instant, so all displayed clocks agree."""
|
|
27
|
+
now = now if now is not None else datetime.now(timezone.utc)
|
|
28
|
+
if now.tzinfo is None or now.utcoffset() is None:
|
|
29
|
+
raise ValueError("The source time must be timezone-aware")
|
|
30
|
+
local = now.astimezone()
|
|
31
|
+
target = now.astimezone(zone)
|
|
32
|
+
offset = target.strftime("%z")
|
|
33
|
+
offset = offset[:3] + ":" + offset[3:]
|
|
34
|
+
days = (target.date() - local.date()).days
|
|
35
|
+
day_note = "same date as local" if days == 0 else f"{days:+d} day(s) from local"
|
|
36
|
+
return [
|
|
37
|
+
f"System local: {local:%Y-%m-%d %H:%M:%S %Z}",
|
|
38
|
+
f"Selected: {zone.key}",
|
|
39
|
+
f" {target:%A, %Y-%m-%d}",
|
|
40
|
+
f" {target:%H:%M:%S} {target.tzname()} UTC{offset}",
|
|
41
|
+
f" {day_note}",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def terminal_app(screen, initial_zone):
|
|
46
|
+
import curses
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
curses.curs_set(0)
|
|
50
|
+
except curses.error:
|
|
51
|
+
pass
|
|
52
|
+
screen.keypad(True)
|
|
53
|
+
screen.timeout(200)
|
|
54
|
+
active = ZoneInfo(initial_zone)
|
|
55
|
+
favorites = list(dict.fromkeys([*FAVORITES, initial_zone]))
|
|
56
|
+
favorites = [name for name in favorites if zone_exists(name)]
|
|
57
|
+
all_zones = sorted(available_timezones() | set(favorites))
|
|
58
|
+
searching = False
|
|
59
|
+
query = ""
|
|
60
|
+
index = favorites.index(initial_zone)
|
|
61
|
+
status = ""
|
|
62
|
+
|
|
63
|
+
def put(y, text, style=0):
|
|
64
|
+
height, width = screen.getmaxyx()
|
|
65
|
+
if 0 <= y < height and width > 1:
|
|
66
|
+
try:
|
|
67
|
+
screen.addnstr(y, 0, text, width - 1, style)
|
|
68
|
+
except curses.error:
|
|
69
|
+
pass # Resizing can race the screen update.
|
|
70
|
+
|
|
71
|
+
while True:
|
|
72
|
+
items = ([z for z in all_zones if query.casefold().replace(" ", "_")
|
|
73
|
+
in z.casefold()] if searching else favorites)
|
|
74
|
+
index = min(max(0, index), max(0, len(items) - 1))
|
|
75
|
+
height, width = screen.getmaxyx()
|
|
76
|
+
screen.erase()
|
|
77
|
+
if height < 16 or width < 48:
|
|
78
|
+
put(0, "Please resize to at least 48 columns x 16 rows.")
|
|
79
|
+
put(1, "Esc or Q: quit")
|
|
80
|
+
screen.refresh()
|
|
81
|
+
key = screen.getch()
|
|
82
|
+
if key in (27, ord("q"), ord("Q")):
|
|
83
|
+
return
|
|
84
|
+
continue
|
|
85
|
+
put(0, "TIMEZONE CONVERTER | live system clock", curses.A_BOLD)
|
|
86
|
+
for row, line in enumerate(clock_lines(active), 2):
|
|
87
|
+
put(row, line, curses.A_BOLD if row == 5 else 0)
|
|
88
|
+
put(8, f"Search all timezones: {query}_" if searching else "Quick choices")
|
|
89
|
+
capacity = height - 13
|
|
90
|
+
start = max(0, min(index - capacity // 2, len(items) - capacity))
|
|
91
|
+
for row, name in enumerate(items[start:start + capacity], 9):
|
|
92
|
+
selected = row - 9 + start == index
|
|
93
|
+
marker = "*" if name == active.key else " "
|
|
94
|
+
put(row, f"{marker} {name}", curses.A_REVERSE if selected else 0)
|
|
95
|
+
if not items:
|
|
96
|
+
put(9, "No matches. Backspace to edit; Esc for favorites.")
|
|
97
|
+
put(height - 3, status or f"{len(items)} zones | * = displayed timezone")
|
|
98
|
+
put(height - 2, "Up/Down: choose Enter: apply Esc: favorites" if searching
|
|
99
|
+
else "Up/Down: choose Enter: apply /: search")
|
|
100
|
+
put(height - 1, "Type to filter (Q is a search character)." if searching
|
|
101
|
+
else "F: favorites Q: quit Refreshes automatically")
|
|
102
|
+
screen.refresh()
|
|
103
|
+
try:
|
|
104
|
+
key = screen.get_wch()
|
|
105
|
+
except curses.error:
|
|
106
|
+
continue
|
|
107
|
+
status = ""
|
|
108
|
+
if key == curses.KEY_UP:
|
|
109
|
+
index = max(0, index - 1)
|
|
110
|
+
elif key == curses.KEY_DOWN:
|
|
111
|
+
index = min(len(items) - 1, index + 1)
|
|
112
|
+
elif key == curses.KEY_NPAGE:
|
|
113
|
+
index = min(len(items) - 1, index + capacity)
|
|
114
|
+
elif key == curses.KEY_PPAGE:
|
|
115
|
+
index = max(0, index - capacity)
|
|
116
|
+
elif key in ("\n", "\r", curses.KEY_ENTER):
|
|
117
|
+
if items:
|
|
118
|
+
active = ZoneInfo(items[index])
|
|
119
|
+
status = f"Now displaying {active.key}"
|
|
120
|
+
elif key == "\x1b":
|
|
121
|
+
searching, query, index = False, "", 0
|
|
122
|
+
elif searching:
|
|
123
|
+
if key in (curses.KEY_BACKSPACE, "\x7f", "\b"):
|
|
124
|
+
query, index = query[:-1], 0
|
|
125
|
+
elif isinstance(key, str) and key.isprintable():
|
|
126
|
+
query, index = query + key, 0
|
|
127
|
+
elif key in ("q", "Q"):
|
|
128
|
+
return
|
|
129
|
+
elif key == "/":
|
|
130
|
+
searching, query, index = True, "", 0
|
|
131
|
+
elif key in ("f", "F"):
|
|
132
|
+
index = 0
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def zone_exists(name):
|
|
136
|
+
try:
|
|
137
|
+
ZoneInfo(name)
|
|
138
|
+
return True
|
|
139
|
+
except ZoneInfoNotFoundError:
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def main():
|
|
144
|
+
parser = argparse.ArgumentParser(description=__doc__,
|
|
145
|
+
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
146
|
+
parser.add_argument("--version", action="version", version=f"tzclock {__version__}")
|
|
147
|
+
parser.add_argument("--zone", default="Europe/Warsaw", help="Initial IANA timezone")
|
|
148
|
+
parser.add_argument("--once", action="store_true", help="Print once without a menu")
|
|
149
|
+
args = parser.parse_args()
|
|
150
|
+
try:
|
|
151
|
+
zone = ZoneInfo(args.zone)
|
|
152
|
+
except (ZoneInfoNotFoundError, ValueError):
|
|
153
|
+
parser.error(f"Timezone unavailable: {args.zone!r}. Use an IANA name such as "
|
|
154
|
+
"Asia/Seoul. If timezone data is missing, install the tzdata package.")
|
|
155
|
+
if args.once or not (sys.stdin.isatty() and sys.stdout.isatty()):
|
|
156
|
+
print("\n".join(clock_lines(zone)))
|
|
157
|
+
return
|
|
158
|
+
try:
|
|
159
|
+
import curses
|
|
160
|
+
except ImportError:
|
|
161
|
+
parser.exit(1, "Interactive mode requires curses (included in most macOS/Linux "
|
|
162
|
+
"Python builds; provided by windows-curses on Windows). Reinstall with pip "
|
|
163
|
+
"to restore dependencies, or use --once for plain output.\n")
|
|
164
|
+
try:
|
|
165
|
+
curses.wrapper(terminal_app, args.zone)
|
|
166
|
+
except KeyboardInterrupt:
|
|
167
|
+
pass
|
|
168
|
+
except curses.error as exc:
|
|
169
|
+
parser.exit(1, f"Terminal could not initialize: {exc}. Try --once.\n")
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
if __name__ == "__main__":
|
|
173
|
+
main()
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tzclock-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A small live timezone clock with a searchable terminal menu
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Keywords: timezone,clock,terminal,curses,cli
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Environment :: Console :: Curses
|
|
9
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
10
|
+
Classifier: Operating System :: MacOS
|
|
11
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: tzdata
|
|
16
|
+
Requires-Dist: windows-curses>=2.4; sys_platform == "win32"
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# tzclock
|
|
20
|
+
|
|
21
|
+
A small live timezone converter for the terminal. Reads your system clock and
|
|
22
|
+
shows local time alongside a selected timezone, including the date, UTC offset,
|
|
23
|
+
and date difference. No network requests during use.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
Requires Python 3.9 or newer. Once published to PyPI:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
python -m pip install tzclock-cli
|
|
31
|
+
tzclock
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Use `python3` on macOS/Linux or `py` on Windows if that is your Python launcher.
|
|
35
|
+
For an isolated command-line installation, `pipx install tzclock-cli` also works
|
|
36
|
+
if you already use pipx. In a conda environment, activate it and use the same pip
|
|
37
|
+
command; a separate conda package is not required.
|
|
38
|
+
|
|
39
|
+
Before publication, install a supplied wheel:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
python -m pip install ./dist/tzclock_cli-0.1.0-py3-none-any.whl
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Pip creates the `tzclock` launcher, including an executable launcher on Windows.
|
|
46
|
+
If your Python scripts directory is not on PATH, use `python -m tzclock` instead.
|
|
47
|
+
No copying into system directories or manually setting executable permissions.
|
|
48
|
+
|
|
49
|
+
## Use
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
tzclock
|
|
53
|
+
tzclock --zone Asia/Seoul
|
|
54
|
+
tzclock --zone Asia/Seoul --once
|
|
55
|
+
tzclock --version
|
|
56
|
+
python -m tzclock --zone Europe/Warsaw --once
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
| Key | Action |
|
|
60
|
+
| --- | --- |
|
|
61
|
+
| Up / Down | Choose a timezone |
|
|
62
|
+
| Enter | Apply the highlighted timezone |
|
|
63
|
+
| / | Search all installed IANA timezones |
|
|
64
|
+
| Page Up / Page Down | Scroll a page |
|
|
65
|
+
| Backspace | Edit search |
|
|
66
|
+
| Esc | Leave search and return to favorites |
|
|
67
|
+
| Q | Quit outside search |
|
|
68
|
+
|
|
69
|
+
During search, Q is a normal letter; press Esc then Q to quit. Ctrl+C also exits.
|
|
70
|
+
Quick choices include Warsaw, Seoul, Taipei, Hong Kong, Tokyo and UTC.
|
|
71
|
+
The menu needs at least 48 columns by 16 rows. Resize the terminal if prompted.
|
|
72
|
+
Redirected output automatically uses the one-shot text mode.
|
|
73
|
+
|
|
74
|
+
## Platform handling
|
|
75
|
+
|
|
76
|
+
Dependencies are declared in `pyproject.toml`, not installed by application code:
|
|
77
|
+
|
|
78
|
+
```toml
|
|
79
|
+
dependencies = [
|
|
80
|
+
"tzdata",
|
|
81
|
+
"windows-curses>=2.4; sys_platform == 'win32'",
|
|
82
|
+
]
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Pip installs `windows-curses` only on Windows. macOS and Linux use their Python
|
|
86
|
+
build's standard curses module. The small `tzdata` package is installed on all
|
|
87
|
+
platforms as a fallback when no system timezone database is available. Python's
|
|
88
|
+
zoneinfo normally uses system timezone data first. DST rules come from that
|
|
89
|
+
database; keep your system and dependencies updated.
|
|
90
|
+
|
|
91
|
+
Interactive Windows support depends on a windows-curses wheel being available
|
|
92
|
+
for your Python version and architecture. Conventional CPython installations
|
|
93
|
+
are the intended target; not every Python implementation or platform is promised.
|
|
94
|
+
On minimal Unix Python builds without curses, use `--once` or a Python build
|
|
95
|
+
with curses support. No administrator privileges are needed inside a virtual
|
|
96
|
+
environment.
|
|
97
|
+
|
|
98
|
+
If you previously copied a standalone `tzclock` into `/usr/local/bin`, that old
|
|
99
|
+
file may take precedence. Check `command -v tzclock` (Windows: `where tzclock`).
|
|
100
|
+
Use `python -m tzclock --version` to verify the installed package directly.
|
|
101
|
+
|
|
102
|
+
## Development
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
python -m pip install -e .
|
|
106
|
+
python -m unittest discover -s tests -v
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
See PUBLISH.md for the short release procedure. MIT licensed.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
PUBLISH.md
|
|
4
|
+
README.md
|
|
5
|
+
pyproject.toml
|
|
6
|
+
src/tzclock/__init__.py
|
|
7
|
+
src/tzclock/__main__.py
|
|
8
|
+
src/tzclock/cli.py
|
|
9
|
+
src/tzclock_cli.egg-info/PKG-INFO
|
|
10
|
+
src/tzclock_cli.egg-info/SOURCES.txt
|
|
11
|
+
src/tzclock_cli.egg-info/dependency_links.txt
|
|
12
|
+
src/tzclock_cli.egg-info/entry_points.txt
|
|
13
|
+
src/tzclock_cli.egg-info/requires.txt
|
|
14
|
+
src/tzclock_cli.egg-info/top_level.txt
|
|
15
|
+
tests/test_clock.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tzclock
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import sys
|
|
3
|
+
import unittest
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from zoneinfo import ZoneInfo
|
|
6
|
+
|
|
7
|
+
from tzclock.cli import clock_lines
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ClockTests(unittest.TestCase):
|
|
11
|
+
def test_warsaw_dst_transition(self):
|
|
12
|
+
before = datetime(2026, 3, 29, 0, 59, tzinfo=timezone.utc)
|
|
13
|
+
after = datetime(2026, 3, 29, 1, 0, tzinfo=timezone.utc)
|
|
14
|
+
first = "\n".join(clock_lines(ZoneInfo("Europe/Warsaw"), before))
|
|
15
|
+
second = "\n".join(clock_lines(ZoneInfo("Europe/Warsaw"), after))
|
|
16
|
+
self.assertIn("01:59:00 CET UTC+01:00", first)
|
|
17
|
+
self.assertIn("03:00:00 CEST UTC+02:00", second)
|
|
18
|
+
|
|
19
|
+
def test_seoul_date_rollover(self):
|
|
20
|
+
lines = clock_lines(ZoneInfo("Asia/Seoul"),
|
|
21
|
+
datetime(2026, 1, 15, 20, tzinfo=timezone.utc))
|
|
22
|
+
self.assertIn("2026-01-16", lines[2])
|
|
23
|
+
self.assertIn("05:00:00", lines[3])
|
|
24
|
+
|
|
25
|
+
def test_fractional_offset(self):
|
|
26
|
+
lines = clock_lines(ZoneInfo("Asia/Kathmandu"),
|
|
27
|
+
datetime(2026, 1, 15, 20, tzinfo=timezone.utc))
|
|
28
|
+
self.assertIn("UTC+05:45", lines[3])
|
|
29
|
+
|
|
30
|
+
def test_naive_datetime_rejected(self):
|
|
31
|
+
with self.assertRaises(ValueError):
|
|
32
|
+
clock_lines(ZoneInfo("UTC"), datetime(2026, 1, 1))
|
|
33
|
+
|
|
34
|
+
def test_module_entrypoint(self):
|
|
35
|
+
result = subprocess.run([sys.executable, "-m", "tzclock", "--zone", "Asia/Seoul", "--once"],
|
|
36
|
+
text=True, capture_output=True)
|
|
37
|
+
self.assertEqual(result.returncode, 0, result.stderr)
|
|
38
|
+
self.assertIn("Asia/Seoul", result.stdout)
|
|
39
|
+
|
|
40
|
+
def test_invalid_zone(self):
|
|
41
|
+
result = subprocess.run([sys.executable, "-m", "tzclock", "--zone", "No/Such_Zone"],
|
|
42
|
+
text=True, capture_output=True)
|
|
43
|
+
self.assertEqual(result.returncode, 2)
|
|
44
|
+
self.assertIn("Timezone unavailable", result.stderr)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
if __name__ == "__main__":
|
|
48
|
+
unittest.main()
|