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.
@@ -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,2 @@
1
+ include PUBLISH.md
2
+ recursive-include tests *.py
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """A live timezone clock for the terminal."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -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,2 @@
1
+ [console_scripts]
2
+ tzclock = tzclock.cli:main
@@ -0,0 +1,4 @@
1
+ tzdata
2
+
3
+ [:sys_platform == "win32"]
4
+ windows-curses>=2.4
@@ -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()