timy-cli 2.0.0__tar.gz → 2.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.
- {timy-cli-2.0.0 → timy_cli-2.1.0}/PKG-INFO +7 -2
- {timy-cli-2.0.0 → timy_cli-2.1.0}/pyproject.toml +5 -2
- timy_cli-2.1.0/src/timy_cli/__init__.py +2 -0
- timy_cli-2.1.0/src/timy_cli/__main__.py +9 -0
- timy_cli-2.1.0/src/timy_cli/settings.py +37 -0
- timy_cli-2.1.0/src/timy_cli/timy.py +427 -0
- {timy-cli-2.0.0 → timy_cli-2.1.0}/src/timy_cli.egg-info/PKG-INFO +7 -2
- {timy-cli-2.0.0 → timy_cli-2.1.0}/src/timy_cli.egg-info/SOURCES.txt +1 -0
- timy_cli-2.1.0/src/timy_cli.egg-info/requires.txt +6 -0
- timy-cli-2.0.0/src/timy_cli/__init__.py +0 -0
- timy-cli-2.0.0/src/timy_cli/__main__.py +0 -7
- timy-cli-2.0.0/src/timy_cli/timy.py +0 -189
- timy-cli-2.0.0/src/timy_cli.egg-info/requires.txt +0 -1
- {timy-cli-2.0.0 → timy_cli-2.1.0}/LICENSE +0 -0
- {timy-cli-2.0.0 → timy_cli-2.1.0}/README.md +0 -0
- {timy-cli-2.0.0 → timy_cli-2.1.0}/setup.cfg +0 -0
- {timy-cli-2.0.0 → timy_cli-2.1.0}/src/timy_cli.egg-info/dependency_links.txt +0 -0
- {timy-cli-2.0.0 → timy_cli-2.1.0}/src/timy_cli.egg-info/entry_points.txt +0 -0
- {timy-cli-2.0.0 → timy_cli-2.1.0}/src/timy_cli.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: timy-cli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Analog Clock and Timer
|
|
5
5
|
Author-email: espehon <espehon@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/espehon/timy-cli
|
|
@@ -14,6 +14,11 @@ Classifier: Topic :: Utilities
|
|
|
14
14
|
Requires-Python: >=3.8
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
16
|
License-File: LICENSE
|
|
17
|
+
Requires-Dist: colorama>=0.4.6
|
|
18
|
+
Requires-Dist: questionary>=2.1.0
|
|
19
|
+
Requires-Dist: tzdata>=2024.1
|
|
20
|
+
Requires-Dist: backports.zoneinfo; python_version < "3.9"
|
|
21
|
+
Dynamic: license-file
|
|
17
22
|
|
|
18
23
|
# timy
|
|
19
24
|
Simple console clock/timer
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" # Boilerplate
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "timy-cli" # Your pip install <name> (must be unique)
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.1.0" # Must be manually updated here with every change
|
|
8
8
|
authors = [
|
|
9
9
|
{ name="espehon", email="espehon@gmail.com" }, # Email is optional (as array?)
|
|
10
10
|
]
|
|
@@ -12,7 +12,10 @@ description = "Analog Clock and Timer" # Header on PyPI (above rea
|
|
|
12
12
|
readme = "README.md" # Boilerplate and will be added to the PyPI page
|
|
13
13
|
requires-python = ">=3.8" # Python version
|
|
14
14
|
dependencies = [ # Any site-package dependencies (as a list)
|
|
15
|
-
"colorama >= 0.4.6"
|
|
15
|
+
"colorama >= 0.4.6",
|
|
16
|
+
"questionary >= 2.1.0",
|
|
17
|
+
"tzdata >= 2024.1",
|
|
18
|
+
"backports.zoneinfo; python_version < \"3.9\""
|
|
16
19
|
]
|
|
17
20
|
classifiers = [ # Kinda boilerplate. Used as hashtags really
|
|
18
21
|
"Programming Language :: Python :: 3", # Tweak this to match your project
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
CONFIG_FILE = Path.home() / ".local/share/timy/settings.json"
|
|
6
|
+
|
|
7
|
+
DEFAULT_SETTINGS = {
|
|
8
|
+
"MainZone": "local",
|
|
9
|
+
"TimeZone1": "local",
|
|
10
|
+
"TimeZone2": "UTC",
|
|
11
|
+
"TimeZone3": "America/New_York",
|
|
12
|
+
"TimeZone4": "Europe/London",
|
|
13
|
+
"stretch_x": True
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_or_create_settings():
|
|
19
|
+
if not os.path.exists(CONFIG_FILE):
|
|
20
|
+
CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
21
|
+
with open(CONFIG_FILE, 'w') as f:
|
|
22
|
+
json.dump(DEFAULT_SETTINGS, f, indent=4) # Create file with defaults
|
|
23
|
+
return DEFAULT_SETTINGS
|
|
24
|
+
else:
|
|
25
|
+
with open(CONFIG_FILE, 'r') as f:
|
|
26
|
+
user_settings = json.load(f) # Load user settings
|
|
27
|
+
|
|
28
|
+
# Merge user settings with defaults (user settings take precedence)
|
|
29
|
+
settings = DEFAULT_SETTINGS.copy()
|
|
30
|
+
settings.update(user_settings)
|
|
31
|
+
return settings
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def save_settings(settings):
|
|
35
|
+
CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
with open(CONFIG_FILE, 'w') as f:
|
|
37
|
+
json.dump(settings, f, indent=4)
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
|
|
2
|
+
import math
|
|
3
|
+
import time
|
|
4
|
+
import sys
|
|
5
|
+
import argparse
|
|
6
|
+
import importlib.metadata
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from os import name, get_terminal_size
|
|
9
|
+
from subprocess import run
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from zoneinfo import ZoneInfo
|
|
13
|
+
except ImportError:
|
|
14
|
+
from backports.zoneinfo import ZoneInfo
|
|
15
|
+
|
|
16
|
+
from colorama import Fore, init
|
|
17
|
+
from questionary import Choice
|
|
18
|
+
import questionary
|
|
19
|
+
from timy_cli.settings import load_or_create_settings
|
|
20
|
+
|
|
21
|
+
init(autoreset=True)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Set __version__
|
|
25
|
+
try:
|
|
26
|
+
__version__ = f"timy {importlib.metadata.version('timy-cli')}"
|
|
27
|
+
except importlib.metadata.PackageNotFoundError:
|
|
28
|
+
__version__ = "Package not installed..."
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
parser = argparse.ArgumentParser(description='Print an analog clock to the consol!', add_help=False)
|
|
32
|
+
|
|
33
|
+
parser.add_argument('-?', '--help', action='help', help='Show this help message and exit.')
|
|
34
|
+
|
|
35
|
+
parser.add_argument('-v', '--version', action='version', version='%(prog)s {version}'.format(version=__version__))
|
|
36
|
+
|
|
37
|
+
parser.add_argument('-l', '--live', dest='_refresh', action='store_true', help='Refresh every minute until stopped. (live clock)')
|
|
38
|
+
|
|
39
|
+
parser.add_argument('-s', '--stopwatch', action='store_true', help='Interactive Stopwatch timer')
|
|
40
|
+
|
|
41
|
+
parser.add_argument('-c', '--countdown', metavar='M', action='append', type=int, nargs='?', const=60, help='Countdown timer for [M] minutes (default 60)')
|
|
42
|
+
|
|
43
|
+
parser.add_argument('-m', '--multiple', action='store_true', help='Show multiple timezones (defined in settings.json)')
|
|
44
|
+
|
|
45
|
+
parser.add_argument('-z', '--zone', action='store_true', help='Configure timezone settings interactively')
|
|
46
|
+
|
|
47
|
+
args = parser.parse_args() #Execute parse_args()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def clear():
|
|
53
|
+
if name == 'nt':
|
|
54
|
+
_ = run('cls', shell=True)
|
|
55
|
+
else:
|
|
56
|
+
_ = run('clear')
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def load_zone(zone_name):
|
|
60
|
+
if zone_name is None:
|
|
61
|
+
return None
|
|
62
|
+
if isinstance(zone_name, str) and zone_name.lower() == 'local':
|
|
63
|
+
return None
|
|
64
|
+
try:
|
|
65
|
+
return ZoneInfo(zone_name)
|
|
66
|
+
except Exception:
|
|
67
|
+
normalized = zone_name.replace(' ', '_') if isinstance(zone_name, str) else zone_name
|
|
68
|
+
try:
|
|
69
|
+
return ZoneInfo(normalized)
|
|
70
|
+
except Exception:
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def zone_label(zone_name):
|
|
75
|
+
if zone_name is None:
|
|
76
|
+
return 'Unknown'
|
|
77
|
+
if isinstance(zone_name, str) and zone_name.lower() == 'local':
|
|
78
|
+
return 'Local'
|
|
79
|
+
return zone_name
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
POPULAR_ZONES = [
|
|
83
|
+
('UTC', 'UTC'),
|
|
84
|
+
('US Eastern (New York)', 'America/New_York'),
|
|
85
|
+
('US Central (Chicago)', 'America/Chicago'),
|
|
86
|
+
('US Mountain (Denver)', 'America/Denver'),
|
|
87
|
+
('US Pacific (Los Angeles)', 'America/Los_Angeles'),
|
|
88
|
+
('Arizona (no DST)', 'America/Phoenix'),
|
|
89
|
+
('Hawaii (no DST)', 'Pacific/Honolulu'),
|
|
90
|
+
('Alaska', 'America/Anchorage'),
|
|
91
|
+
('London', 'Europe/London'),
|
|
92
|
+
('Central Europe (Berlin)', 'Europe/Berlin'),
|
|
93
|
+
('Eastern Europe (Athens)', 'Europe/Athens'),
|
|
94
|
+
('Moscow', 'Europe/Moscow'),
|
|
95
|
+
('India (Kolkata)', 'Asia/Kolkata'),
|
|
96
|
+
('China (Shanghai)', 'Asia/Shanghai'),
|
|
97
|
+
('Japan (Tokyo)', 'Asia/Tokyo'),
|
|
98
|
+
('Singapore', 'Asia/Singapore'),
|
|
99
|
+
('Australia Eastern (Sydney)', 'Australia/Sydney'),
|
|
100
|
+
('Australia Central (Darwin, no DST)', 'Australia/Darwin'),
|
|
101
|
+
('Australia Western (Perth, no DST)', 'Australia/Perth'),
|
|
102
|
+
('New Zealand (Auckland)', 'Pacific/Auckland'),
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
ZONE_CANCELLED = '__cancelled__'
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def zone_choices(include_disabled=False):
|
|
109
|
+
choices = [Choice('Local', 'local')]
|
|
110
|
+
if include_disabled:
|
|
111
|
+
choices.append(Choice('Disabled', '__disabled__'))
|
|
112
|
+
choices.extend(Choice(label, value) for label, value in POPULAR_ZONES)
|
|
113
|
+
choices.append(Choice('More', '__more__'))
|
|
114
|
+
return choices
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def choose_zone(current_zone, include_disabled=False):
|
|
118
|
+
selected = questionary.select(
|
|
119
|
+
'Select a timezone:',
|
|
120
|
+
choices=zone_choices(include_disabled),
|
|
121
|
+
default=current_zone,
|
|
122
|
+
).ask()
|
|
123
|
+
if selected is None:
|
|
124
|
+
return ZONE_CANCELLED
|
|
125
|
+
if selected == '__disabled__':
|
|
126
|
+
return None
|
|
127
|
+
if selected != '__more__':
|
|
128
|
+
return selected
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
from zoneinfo import available_timezones
|
|
132
|
+
except ImportError:
|
|
133
|
+
from backports.zoneinfo import available_timezones
|
|
134
|
+
|
|
135
|
+
all_zones = sorted(available_timezones())
|
|
136
|
+
|
|
137
|
+
return questionary.select(
|
|
138
|
+
'Select a timezone from the full list:',
|
|
139
|
+
choices=[Choice('Local', 'local')] + [Choice(zone, zone) for zone in all_zones],
|
|
140
|
+
default=current_zone if current_zone in all_zones else None,
|
|
141
|
+
).ask() or ZONE_CANCELLED
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def configure_zones():
|
|
145
|
+
from timy_cli.settings import save_settings
|
|
146
|
+
|
|
147
|
+
settings = load_or_create_settings()
|
|
148
|
+
target = questionary.select(
|
|
149
|
+
'Which timezone setting do you want to change?',
|
|
150
|
+
choices=[
|
|
151
|
+
Choice('Main clock', 'MainZone'),
|
|
152
|
+
Choice('Multi clock 1', 'TimeZone1'),
|
|
153
|
+
Choice('Multi clock 2', 'TimeZone2'),
|
|
154
|
+
Choice('Multi clock 3', 'TimeZone3'),
|
|
155
|
+
Choice('Multi clock 4', 'TimeZone4'),
|
|
156
|
+
Choice('Done', '__done__'),
|
|
157
|
+
],
|
|
158
|
+
).ask()
|
|
159
|
+
|
|
160
|
+
while target not in (None, '__done__'):
|
|
161
|
+
selected_zone = choose_zone(settings.get(target), target != 'MainZone')
|
|
162
|
+
if selected_zone == ZONE_CANCELLED:
|
|
163
|
+
return
|
|
164
|
+
settings[target] = selected_zone
|
|
165
|
+
save_settings(settings)
|
|
166
|
+
print(f"{target} set to {settings[target] or 'disabled'}")
|
|
167
|
+
target = questionary.select(
|
|
168
|
+
'Choose another setting:',
|
|
169
|
+
choices=[
|
|
170
|
+
Choice('Main clock', 'MainZone'),
|
|
171
|
+
Choice('Multi clock 1', 'TimeZone1'),
|
|
172
|
+
Choice('Multi clock 2', 'TimeZone2'),
|
|
173
|
+
Choice('Multi clock 3', 'TimeZone3'),
|
|
174
|
+
Choice('Multi clock 4', 'TimeZone4'),
|
|
175
|
+
Choice('Done', '__done__'),
|
|
176
|
+
],
|
|
177
|
+
).ask()
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def countdownTimer(Minutes):
|
|
181
|
+
clear()
|
|
182
|
+
paddingWithGlass = get_terminal_size()[1] - 32 # 32 is length of the following outputs
|
|
183
|
+
if paddingWithGlass > 0:
|
|
184
|
+
print("\n" * paddingWithGlass)
|
|
185
|
+
print('''
|
|
186
|
+
_.-"""-._
|
|
187
|
+
_.-"" ""-._
|
|
188
|
+
:"-. .-":
|
|
189
|
+
'"-_"-._ _.-".-"'
|
|
190
|
+
||T+._"-._.-"_.-"|
|
|
191
|
+
||: "-.|.-" : ||
|
|
192
|
+
|| . ' || . ||
|
|
193
|
+
|| . '|| . ||
|
|
194
|
+
|| ';.:||' ||
|
|
195
|
+
|| '::|| ||
|
|
196
|
+
|| :|| ||
|
|
197
|
+
|| ':|| ||
|
|
198
|
+
|| .' :||. ||
|
|
199
|
+
|| ' . :||.' ||
|
|
200
|
+
||.'- .:|| -'._||
|
|
201
|
+
.-'": .::::||:. : "'-.
|
|
202
|
+
:"-.'::::::||::' .-":
|
|
203
|
+
"-."-._"--:" .-".-"
|
|
204
|
+
"-._"-._.-".-"
|
|
205
|
+
"-.|.-"
|
|
206
|
+
''')
|
|
207
|
+
try:
|
|
208
|
+
for m in progressbar(range(Minutes), prefix="Timer: " +str(Minutes) + " Min ", suffix="(pass ← wait)"):
|
|
209
|
+
time.sleep(60)
|
|
210
|
+
if m == Minutes - 1:
|
|
211
|
+
clear()
|
|
212
|
+
print("\n" * get_terminal_size()[1])
|
|
213
|
+
print(f'''{Fore.LIGHTGREEN_EX}
|
|
214
|
+
+====+
|
|
215
|
+
|( )|
|
|
216
|
+
| )( |
|
|
217
|
+
|(::)|
|
|
218
|
+
+====+
|
|
219
|
+
Timer has ended!''')
|
|
220
|
+
print("\a")
|
|
221
|
+
except:
|
|
222
|
+
print(f"\n\n{Fore.YELLOW}[Timer interrupted]\n\n")
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def progressbar(it, prefix="", suffix=""): #progressbar --> prefix: [############################.............................] i/it
|
|
226
|
+
size = abs(get_terminal_size()[0] - len(prefix) - len(suffix) - 16)
|
|
227
|
+
count = len(it)
|
|
228
|
+
def show(j):
|
|
229
|
+
x = int(size*j/count)
|
|
230
|
+
sys.stdout.write("%s[%s%s] %i ← %i %s \r" % (prefix, "#"*x, "."*(size-x), j, (count-j), suffix))
|
|
231
|
+
sys.stdout.flush()
|
|
232
|
+
show(0) #This prints the progressbar at 0 progress. Then next for loop renders the rest (stating at 1)
|
|
233
|
+
for i, item in enumerate(it): #This is the 'i' in the comment on the 'def' line
|
|
234
|
+
yield item
|
|
235
|
+
show(i+1)
|
|
236
|
+
sys.stdout.write("\n")
|
|
237
|
+
sys.stdout.flush()
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class AnalogClock:
|
|
241
|
+
def __init__(self, width=None, height=25, stretch_x=True, zone_name=None):
|
|
242
|
+
self.height = height
|
|
243
|
+
self.stretch_x = stretch_x
|
|
244
|
+
self.zone_name = zone_name
|
|
245
|
+
self.timezone = load_zone(zone_name)
|
|
246
|
+
self.width = width if width is not None else (height * 2 if stretch_x else height)
|
|
247
|
+
self.canvas = [[' '] * self.width for _ in range(self.height)]
|
|
248
|
+
self.center_x = self.width // 2
|
|
249
|
+
self.center_y = self.height // 2
|
|
250
|
+
self.x_radius = self.width // 2 - 1
|
|
251
|
+
self.y_radius = self.height // 2 - 1
|
|
252
|
+
self.x_scale = self.x_radius / 24.0
|
|
253
|
+
self.y_scale = self.y_radius / 24.0
|
|
254
|
+
|
|
255
|
+
def reset_canvas(self):
|
|
256
|
+
self.canvas = [[' '] * self.width for _ in range(self.height)]
|
|
257
|
+
|
|
258
|
+
def plot(self, t, r, sym='*'):
|
|
259
|
+
row = int(self.center_y - r * self.y_scale * math.cos(t))
|
|
260
|
+
col = int(self.center_x + r * self.x_scale * math.sin(t))
|
|
261
|
+
|
|
262
|
+
if 0 <= row < self.height and 0 <= col < self.width:
|
|
263
|
+
self.canvas[row][col] = sym
|
|
264
|
+
|
|
265
|
+
def current_time(self):
|
|
266
|
+
if self.timezone is None:
|
|
267
|
+
return datetime.now().astimezone()
|
|
268
|
+
return datetime.now(self.timezone)
|
|
269
|
+
|
|
270
|
+
def draw(self):
|
|
271
|
+
self.reset_canvas()
|
|
272
|
+
now = self.current_time()
|
|
273
|
+
h = now.hour * 6.283 + now.minute / 9.549
|
|
274
|
+
min_size = 0.02
|
|
275
|
+
hr_size = 0.01
|
|
276
|
+
hr_fmt = 12
|
|
277
|
+
|
|
278
|
+
for i in range(999):
|
|
279
|
+
self.plot(i / 158.0, 24)
|
|
280
|
+
self.plot(h, i * min_size, "▓")
|
|
281
|
+
self.plot(h / hr_fmt, i * hr_size, "█")
|
|
282
|
+
for q in range(12):
|
|
283
|
+
self.plot(q / 1.91, 24 - i * 0.005, '•')
|
|
284
|
+
|
|
285
|
+
rendered = '\n'.join(''.join(row) for row in self.canvas)
|
|
286
|
+
time_str = now.strftime("%H:%M")
|
|
287
|
+
return rendered, time_str
|
|
288
|
+
|
|
289
|
+
def render(self, refresh=False):
|
|
290
|
+
try:
|
|
291
|
+
while True:
|
|
292
|
+
print('\n' * 4)
|
|
293
|
+
rendered, time_str = self.draw()
|
|
294
|
+
print(zone_label(self.zone_name).center(self.width))
|
|
295
|
+
print(rendered)
|
|
296
|
+
print(" " * int(((self.width / 2) - 2)) + time_str)
|
|
297
|
+
if refresh:
|
|
298
|
+
print("\n[ctrl + c] to terminate", end='')
|
|
299
|
+
time.sleep(60)
|
|
300
|
+
clear()
|
|
301
|
+
else:
|
|
302
|
+
break
|
|
303
|
+
except KeyboardInterrupt:
|
|
304
|
+
return
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class SmallClock:
|
|
308
|
+
def __init__(self, zone_name=None, stretch_x=False):
|
|
309
|
+
self.zone_name = zone_name
|
|
310
|
+
self.timezone = load_zone(zone_name)
|
|
311
|
+
self.stretch_x = stretch_x
|
|
312
|
+
self.base_width = 13
|
|
313
|
+
self.height = 11
|
|
314
|
+
self.width = self.base_width * 2 if stretch_x else self.base_width
|
|
315
|
+
self.center_x = self.width // 2
|
|
316
|
+
self.center_y = self.height // 2
|
|
317
|
+
self.radius = self.center_y - 1
|
|
318
|
+
self.x_scale = 2 if stretch_x else 1
|
|
319
|
+
|
|
320
|
+
def current_time(self):
|
|
321
|
+
if self.timezone is None:
|
|
322
|
+
return datetime.now().astimezone()
|
|
323
|
+
return datetime.now(self.timezone)
|
|
324
|
+
|
|
325
|
+
def hand_position(self, step, radius):
|
|
326
|
+
angle = step * (math.pi / 6)
|
|
327
|
+
row = int(round(self.center_y - math.cos(angle) * radius))
|
|
328
|
+
col = int(round(self.center_x + math.sin(angle) * radius * self.x_scale))
|
|
329
|
+
return row, col
|
|
330
|
+
|
|
331
|
+
def draw_line(self, canvas, step, radius, symbol):
|
|
332
|
+
end_row, end_col = self.hand_position(step, radius)
|
|
333
|
+
for distance in range(1, radius + 1):
|
|
334
|
+
fraction = distance / radius
|
|
335
|
+
row = int(round(self.center_y + (end_row - self.center_y) * fraction))
|
|
336
|
+
col = int(round(self.center_x + (end_col - self.center_x) * fraction))
|
|
337
|
+
if 0 <= row < self.height and 0 <= col < self.width:
|
|
338
|
+
canvas[row][col] = symbol
|
|
339
|
+
|
|
340
|
+
def draw(self):
|
|
341
|
+
now = self.current_time()
|
|
342
|
+
canvas = [[' '] * self.width for _ in range(self.height)]
|
|
343
|
+
|
|
344
|
+
for tick in range(12):
|
|
345
|
+
row, col = self.hand_position(tick, self.radius)
|
|
346
|
+
if 0 <= row < self.height and 0 <= col < self.width:
|
|
347
|
+
canvas[row][col] = 'o'
|
|
348
|
+
|
|
349
|
+
for label, step, offset in [('12', 0, -1), ('3', 3, 0), ('6', 6, 0), ('9', 9, 0)]:
|
|
350
|
+
row, col = self.hand_position(step, self.radius)
|
|
351
|
+
col += offset
|
|
352
|
+
for index, character in enumerate(label):
|
|
353
|
+
if 0 <= row < self.height and 0 <= col + index < self.width:
|
|
354
|
+
canvas[row][col + index] = character
|
|
355
|
+
|
|
356
|
+
minute_step = round(now.minute / 5) % 12
|
|
357
|
+
hour_step = round(((now.hour % 12) * 60 + now.minute) / 60) % 12
|
|
358
|
+
self.draw_line(canvas, hour_step, self.radius - 2, 'H')
|
|
359
|
+
self.draw_line(canvas, minute_step, self.radius - 1, 'M')
|
|
360
|
+
|
|
361
|
+
if 0 <= self.center_y < self.height and 0 <= self.center_x < self.width:
|
|
362
|
+
canvas[self.center_y][self.center_x] = '+'
|
|
363
|
+
|
|
364
|
+
rendered = '\n'.join(''.join(row) for row in canvas)
|
|
365
|
+
time_str = now.strftime("%H:%M")
|
|
366
|
+
return rendered, time_str
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
class MultipleClockRenderer:
|
|
370
|
+
def __init__(self, zone_names, stretch_x=False, padding=4):
|
|
371
|
+
self.zone_names = [zone for zone in zone_names if zone is not None][:4]
|
|
372
|
+
if not self.zone_names:
|
|
373
|
+
self.zone_names = ['local']
|
|
374
|
+
self.padding_string = ' ' * padding
|
|
375
|
+
self.clocks = [SmallClock(zone_name=zone, stretch_x=stretch_x) for zone in self.zone_names]
|
|
376
|
+
|
|
377
|
+
def render_once(self):
|
|
378
|
+
rendered_clocks = [clock.draw() for clock in self.clocks]
|
|
379
|
+
clock_lines = [rendered.splitlines() for rendered, _ in rendered_clocks]
|
|
380
|
+
widths = [len(lines[0]) for lines in clock_lines]
|
|
381
|
+
|
|
382
|
+
top_labels = [zone_label(zone)[:width].center(width) for zone, width in zip(self.zone_names, widths)]
|
|
383
|
+
bottom_labels = [time_str.center(width) for (_, time_str), width in zip(rendered_clocks, widths)]
|
|
384
|
+
|
|
385
|
+
composed = [self.padding_string.join(top_labels)]
|
|
386
|
+
for row in range(len(clock_lines[0])):
|
|
387
|
+
composed.append(self.padding_string.join(lines[row] for lines in clock_lines))
|
|
388
|
+
composed.append(self.padding_string.join(bottom_labels))
|
|
389
|
+
return '\n'.join(composed)
|
|
390
|
+
|
|
391
|
+
def render(self, refresh=False):
|
|
392
|
+
try:
|
|
393
|
+
while True:
|
|
394
|
+
print('\n' * 4)
|
|
395
|
+
print(self.render_once())
|
|
396
|
+
if refresh:
|
|
397
|
+
print("\n[ctrl + c] to terminate", end='')
|
|
398
|
+
time.sleep(60)
|
|
399
|
+
clear()
|
|
400
|
+
else:
|
|
401
|
+
break
|
|
402
|
+
except KeyboardInterrupt:
|
|
403
|
+
return
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def cli():
|
|
407
|
+
if args.zone:
|
|
408
|
+
configure_zones()
|
|
409
|
+
elif args.countdown is not None:
|
|
410
|
+
countdownTimer(args.countdown[0])
|
|
411
|
+
elif args.multiple:
|
|
412
|
+
settings = load_or_create_settings()
|
|
413
|
+
zones = [settings.get(key) for key in [
|
|
414
|
+
'TimeZone1',
|
|
415
|
+
'TimeZone2',
|
|
416
|
+
'TimeZone3',
|
|
417
|
+
'TimeZone4',
|
|
418
|
+
] if settings.get(key) is not None][:4]
|
|
419
|
+
stretch_x = settings.get('stretch_x', True)
|
|
420
|
+
renderer = MultipleClockRenderer(zones, stretch_x=stretch_x)
|
|
421
|
+
renderer.render(args._refresh)
|
|
422
|
+
else:
|
|
423
|
+
settings = load_or_create_settings()
|
|
424
|
+
stretch_x = settings.get('stretch_x', True)
|
|
425
|
+
main_zone = settings.get('MainZone', 'local')
|
|
426
|
+
clock = AnalogClock(stretch_x=stretch_x, zone_name=main_zone)
|
|
427
|
+
clock.render(args._refresh)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: timy-cli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Analog Clock and Timer
|
|
5
5
|
Author-email: espehon <espehon@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/espehon/timy-cli
|
|
@@ -14,6 +14,11 @@ Classifier: Topic :: Utilities
|
|
|
14
14
|
Requires-Python: >=3.8
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
16
|
License-File: LICENSE
|
|
17
|
+
Requires-Dist: colorama>=0.4.6
|
|
18
|
+
Requires-Dist: questionary>=2.1.0
|
|
19
|
+
Requires-Dist: tzdata>=2024.1
|
|
20
|
+
Requires-Dist: backports.zoneinfo; python_version < "3.9"
|
|
21
|
+
Dynamic: license-file
|
|
17
22
|
|
|
18
23
|
# timy
|
|
19
24
|
Simple console clock/timer
|
|
File without changes
|
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import math
|
|
3
|
-
import time
|
|
4
|
-
import sys
|
|
5
|
-
import argparse
|
|
6
|
-
import importlib.metadata
|
|
7
|
-
from os import system, name, get_terminal_size
|
|
8
|
-
|
|
9
|
-
from colorama import Fore, init
|
|
10
|
-
init(autoreset=True)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
# Set __version__
|
|
14
|
-
try:
|
|
15
|
-
__version__ = f"timy {importlib.metadata.version('timy-cli')}"
|
|
16
|
-
except importlib.metadata.PackageNotFoundError:
|
|
17
|
-
__version__ = "Package not installed..."
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
parser = argparse.ArgumentParser(description='Print an analog clock to the consol!', add_help=False)
|
|
21
|
-
|
|
22
|
-
parser.add_argument('-?', '--help', action='help', help='Show this help message and exit.')
|
|
23
|
-
|
|
24
|
-
parser.add_argument('-v', '--version', action='version', version='%(prog)s {version}'.format(version=__version__))
|
|
25
|
-
|
|
26
|
-
parser.add_argument('-r', '--refresh', dest='_refresh', action='store_true', help='Refresh every minute until stopped')
|
|
27
|
-
|
|
28
|
-
parser.add_argument('-c', '--continuous', dest='_refresh', action='store_true', help='Alias for --refresh')
|
|
29
|
-
|
|
30
|
-
parser.add_argument('-t', '--timer', metavar='M', action='append', type=int, nargs='?', const=60, help='Countdown timer for [M] minutes (default 60)')
|
|
31
|
-
|
|
32
|
-
args = parser.parse_args() #Execute parse_args()
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def clear():
|
|
38
|
-
# for windows
|
|
39
|
-
if name == 'nt':
|
|
40
|
-
_ = system('cls')
|
|
41
|
-
# for mac and linux(here, os.name is 'posix')
|
|
42
|
-
else:
|
|
43
|
-
_ = system('clear')
|
|
44
|
-
|
|
45
|
-
def countdownTimer(Minutes):
|
|
46
|
-
clear()
|
|
47
|
-
paddingWithGlass = get_terminal_size()[1] - 32 # 32 is length of the following outputs
|
|
48
|
-
if paddingWithGlass > 0:
|
|
49
|
-
print("\n" * paddingWithGlass)
|
|
50
|
-
print('''
|
|
51
|
-
_.-"""-._
|
|
52
|
-
_.-"" ""-._
|
|
53
|
-
:"-. .-":
|
|
54
|
-
'"-_"-._ _.-".-"'
|
|
55
|
-
||T+._"-._.-"_.-"|
|
|
56
|
-
||: "-.|.-" : ||
|
|
57
|
-
|| . ' || . ||
|
|
58
|
-
|| . '|| . ||
|
|
59
|
-
|| ';.:||' ||
|
|
60
|
-
|| '::|| ||
|
|
61
|
-
|| :|| ||
|
|
62
|
-
|| ':|| ||
|
|
63
|
-
|| .' :||. ||
|
|
64
|
-
|| ' . :||.' ||
|
|
65
|
-
||.'- .:|| -'._||
|
|
66
|
-
.-'": .::::||:. : "'-.
|
|
67
|
-
:"-.'::::::||::' .-":
|
|
68
|
-
"-."-._"--:" .-".-"
|
|
69
|
-
"-._"-._.-".-"
|
|
70
|
-
"-.|.-"
|
|
71
|
-
''')
|
|
72
|
-
try:
|
|
73
|
-
for m in progressbar(range(Minutes), prefix="Timer: " +str(Minutes) + " Min ", sufix="(pass ← wait)"):
|
|
74
|
-
time.sleep(60)
|
|
75
|
-
if m == Minutes - 1:
|
|
76
|
-
clear()
|
|
77
|
-
print("\n" * get_terminal_size()[1])
|
|
78
|
-
print(f'''{Fore.LIGHTGREEN_EX}
|
|
79
|
-
+====+
|
|
80
|
-
|( )|
|
|
81
|
-
| )( |
|
|
82
|
-
|(::)|
|
|
83
|
-
+====+
|
|
84
|
-
Timer has ended!''')
|
|
85
|
-
print("\a")
|
|
86
|
-
except:
|
|
87
|
-
print(f"\n\n{Fore.YELLOW}[Timer interrupted]\n\n")
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def progressbar(it, prefix="", sufix=""): #progressbar --> prefix: [############################.............................] i/it
|
|
91
|
-
size = abs(get_terminal_size()[0] - len(prefix) - len(sufix) - 16)
|
|
92
|
-
count = len(it)
|
|
93
|
-
def show(j):
|
|
94
|
-
x = int(size*j/count)
|
|
95
|
-
sys.stdout.write("%s[%s%s] %i ← %i %s \r" % (prefix, "#"*x, "."*(size-x), j, (count-j), sufix))
|
|
96
|
-
sys.stdout.flush()
|
|
97
|
-
show(0) #This prints the progressbar at 0 progress. Then next for loop renders the rest (stating at 1)
|
|
98
|
-
for i, item in enumerate(it): #This is the 'i' in the comment on the 'def' line
|
|
99
|
-
yield item
|
|
100
|
-
show(i+1)
|
|
101
|
-
sys.stdout.write("\n")
|
|
102
|
-
sys.stdout.flush()
|
|
103
|
-
|
|
104
|
-
def p(t,r,sym='*'):
|
|
105
|
-
global c
|
|
106
|
-
if stretch_x == True:
|
|
107
|
-
c[int((clock_hight-r*math.cos(t))/2)][int(clock_hight+r*math.sin(t))]=sym
|
|
108
|
-
else:
|
|
109
|
-
c[int(clock_hight-r*math.cos(t))][int(clock_hight+r*math.sin(t))]=sym
|
|
110
|
-
|
|
111
|
-
def analog_clock(_refresh):
|
|
112
|
-
global c
|
|
113
|
-
global stretch_x
|
|
114
|
-
global clock_hight
|
|
115
|
-
|
|
116
|
-
hr_fmt = 12
|
|
117
|
-
stretch_x = True #--> if clock_width is twice that of clock_hight
|
|
118
|
-
min_size = 0.02
|
|
119
|
-
hr_size = 0.01
|
|
120
|
-
clock_width = 50
|
|
121
|
-
clock_hight = 25
|
|
122
|
-
|
|
123
|
-
try:
|
|
124
|
-
while True:
|
|
125
|
-
print('\n' * 4)
|
|
126
|
-
c = [[' '] * clock_width for i in range(clock_width)]
|
|
127
|
-
t = time.localtime()
|
|
128
|
-
h = t.tm_hour * 6.283 + t.tm_min / 9.549
|
|
129
|
-
for i in range(999):
|
|
130
|
-
p(i/158.0,24)
|
|
131
|
-
p(h,i*min_size,"▓")
|
|
132
|
-
p(h/hr_fmt,i*hr_size,"█")
|
|
133
|
-
for q in range(12):
|
|
134
|
-
p(q/1.91,24-i*.005,'•')
|
|
135
|
-
for y in range(clock_hight):
|
|
136
|
-
print(''.join(c[y]))
|
|
137
|
-
print((" "*int(((clock_width/2)-2))) + str(time.localtime().tm_hour).zfill(2) + ":" + str(time.localtime().tm_min).zfill(2))
|
|
138
|
-
if _refresh == True:
|
|
139
|
-
print("\n[ctrl + c] to terminate", end='')
|
|
140
|
-
time.sleep(60)
|
|
141
|
-
clear()
|
|
142
|
-
else:
|
|
143
|
-
break
|
|
144
|
-
except:
|
|
145
|
-
#exit without error message
|
|
146
|
-
return
|
|
147
|
-
|
|
148
|
-
def mini_clocks(_refresh):
|
|
149
|
-
global c
|
|
150
|
-
global stretch_x
|
|
151
|
-
global clock_hight
|
|
152
|
-
|
|
153
|
-
hr_fmt = 12
|
|
154
|
-
stretch_x = True #--> if clock_width is twice that of clock_hight
|
|
155
|
-
min_size = 0.02
|
|
156
|
-
hr_size = 0.01
|
|
157
|
-
clock_width = 26
|
|
158
|
-
clock_hight = 13
|
|
159
|
-
|
|
160
|
-
try:
|
|
161
|
-
while True:
|
|
162
|
-
print('\n' * 4)
|
|
163
|
-
c = [[' '] * clock_width for i in range(clock_width)]
|
|
164
|
-
t = time.localtime()
|
|
165
|
-
h = t.tm_hour * 6.283 + t.tm_min / 9.549
|
|
166
|
-
for i in range(999):
|
|
167
|
-
p(i/158.0,24)
|
|
168
|
-
p(h,i*min_size,"▓")
|
|
169
|
-
p(h/hr_fmt,i*hr_size,"█")
|
|
170
|
-
for q in range(12):
|
|
171
|
-
p(q/1.91,24-i*.005,'•')
|
|
172
|
-
for y in range(clock_hight):
|
|
173
|
-
print(''.join(c[y]))
|
|
174
|
-
print((" "*int(((clock_width/2)-2))) + str(time.localtime().tm_hour).zfill(2) + ":" + str(time.localtime().tm_min).zfill(2))
|
|
175
|
-
if _refresh == True:
|
|
176
|
-
print("\n[ctrl + c] to terminate", end='')
|
|
177
|
-
time.sleep(60)
|
|
178
|
-
clear()
|
|
179
|
-
else:
|
|
180
|
-
break
|
|
181
|
-
except:
|
|
182
|
-
#exit without error message
|
|
183
|
-
return
|
|
184
|
-
|
|
185
|
-
def cli():
|
|
186
|
-
if args.timer != None:
|
|
187
|
-
countdownTimer(args.timer[0])
|
|
188
|
-
else:
|
|
189
|
-
analog_clock(args._refresh)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
colorama>=0.4.6
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|