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 ADDED
@@ -0,0 +1,3 @@
1
+ """A live timezone clock for the terminal."""
2
+
3
+ __version__ = "0.1.0"
tzclock/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ tzclock = tzclock.cli:main
@@ -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