tzclock-cli 0.1.0__py3-none-any.whl
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/__init__.py +3 -0
- tzclock/__main__.py +4 -0
- tzclock/cli.py +173 -0
- tzclock_cli-0.1.0.dist-info/METADATA +109 -0
- tzclock_cli-0.1.0.dist-info/RECORD +9 -0
- tzclock_cli-0.1.0.dist-info/WHEEL +5 -0
- tzclock_cli-0.1.0.dist-info/entry_points.txt +2 -0
- tzclock_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- tzclock_cli-0.1.0.dist-info/top_level.txt +1 -0
tzclock/__init__.py
ADDED
tzclock/__main__.py
ADDED
tzclock/cli.py
ADDED
|
@@ -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,9 @@
|
|
|
1
|
+
tzclock/__init__.py,sha256=ESEuEuO5-qaTrbWNWl-VMfw0E9lQZwql6Md7Derm5io,69
|
|
2
|
+
tzclock/__main__.py,sha256=MSmt_5Xg84uHqzTN38JwgseJK8rsJn_11A8WD99VtEo,61
|
|
3
|
+
tzclock/cli.py,sha256=CZp6t0MNukZZedGgAmk0AOEGW9TMdUtojTJOKQsbGAo,6777
|
|
4
|
+
tzclock_cli-0.1.0.dist-info/licenses/LICENSE,sha256=DR40GeDLCrY9aLFhHPqqE5QWkdQ98lueYJ7iAGuAHl8,1077
|
|
5
|
+
tzclock_cli-0.1.0.dist-info/METADATA,sha256=NE4nymGkUdxygzz3W3HN9fjuSr8qYdjqupGrWK27sUw,3835
|
|
6
|
+
tzclock_cli-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
7
|
+
tzclock_cli-0.1.0.dist-info/entry_points.txt,sha256=L8mkYmv0Tksw7LR1isDVtTfD550n_o0ugPQJU3ziA0A,45
|
|
8
|
+
tzclock_cli-0.1.0.dist-info/top_level.txt,sha256=5qSPqxgTgpuseNg-ufBk_WSMrJbJjMRAt3MtODxWlPc,8
|
|
9
|
+
tzclock_cli-0.1.0.dist-info/RECORD,,
|
|
@@ -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 @@
|
|
|
1
|
+
tzclock
|