drunken-chunk 1.0.0
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.
- package/LICENSE +21 -0
- package/README.md +266 -0
- package/bin/index.js +49 -0
- package/drunken_chunk/__init__.py +2 -0
- package/drunken_chunk/analytics.py +172 -0
- package/drunken_chunk/cli.py +834 -0
- package/drunken_chunk/db.py +245 -0
- package/drunken_chunk/profile.py +232 -0
- package/drunken_chunk/reminder.py +386 -0
- package/drunken_chunk/shortcut.py +43 -0
- package/drunken_chunk/utils.py +122 -0
- package/package.json +33 -0
|
@@ -0,0 +1,834 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import sys
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import collections
|
|
6
|
+
import io
|
|
7
|
+
from datetime import datetime, timedelta
|
|
8
|
+
|
|
9
|
+
# Force UTF-8 stdout/stderr on Windows to prevent cp1252 box-drawing encode errors
|
|
10
|
+
if sys.platform == 'win32':
|
|
11
|
+
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
|
12
|
+
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
|
|
13
|
+
|
|
14
|
+
from .db import (
|
|
15
|
+
init_db, get_profile, save_profile, log_water, get_today_logs,
|
|
16
|
+
get_history_logs, get_shortcuts, get_daemon_state, set_daemon_state
|
|
17
|
+
)
|
|
18
|
+
from .shortcut import add_custom_shortcut, remove_custom_shortcut
|
|
19
|
+
from .profile import (
|
|
20
|
+
calculate_recommended_intake, get_profile_insights, get_hydration_level,
|
|
21
|
+
calculate_bmi, get_bmi_category, get_situational_art_and_quote
|
|
22
|
+
)
|
|
23
|
+
from .reminder import (
|
|
24
|
+
start_background_daemon, stop_background_daemon, is_pid_running, is_waking_hours
|
|
25
|
+
)
|
|
26
|
+
from .analytics import analyze_drinking_behavior, get_day_name
|
|
27
|
+
from .utils import (
|
|
28
|
+
Colors, colorize, bold, italic, print_banner, get_progress_bar, print_table
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def parse_relative_time(time_str):
|
|
32
|
+
"""Parses relative time strings like '10m ago', '2h ago', '14:30', or 'now'."""
|
|
33
|
+
now = datetime.now()
|
|
34
|
+
time_str = time_str.strip().lower()
|
|
35
|
+
|
|
36
|
+
if time_str == "now":
|
|
37
|
+
return now
|
|
38
|
+
|
|
39
|
+
# Check for minutes ago (e.g. 10m ago, 5 mins ago)
|
|
40
|
+
m_match = re.match(r'^(\d+)\s*(m|min|minute|minutes)\s*ago$', time_str)
|
|
41
|
+
if m_match:
|
|
42
|
+
minutes = int(m_match.group(1))
|
|
43
|
+
return now - timedelta(minutes=minutes)
|
|
44
|
+
|
|
45
|
+
# Check for hours ago (e.g. 2h ago, 1 hour ago)
|
|
46
|
+
h_match = re.match(r'^(\d+)\s*(h|hr|hour|hours)\s*ago$', time_str)
|
|
47
|
+
if h_match:
|
|
48
|
+
hours = int(h_match.group(1))
|
|
49
|
+
return now - timedelta(hours=hours)
|
|
50
|
+
|
|
51
|
+
# Check for absolute HH:MM
|
|
52
|
+
time_match = re.match(r'^(\d{1,2}):(\d{2})$', time_str)
|
|
53
|
+
if time_match:
|
|
54
|
+
h, m = int(time_match.group(1)), int(time_match.group(2))
|
|
55
|
+
if 0 <= h < 24 and 0 <= m < 60:
|
|
56
|
+
target_time = now.replace(hour=h, minute=m, second=0, microsecond=0)
|
|
57
|
+
if target_time > now:
|
|
58
|
+
# If target time is in the future, assume it refers to yesterday
|
|
59
|
+
target_time -= timedelta(days=1)
|
|
60
|
+
return target_time
|
|
61
|
+
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
def check_profile_exists():
|
|
65
|
+
"""Checks if the user profile exists, prompts to initialize if not."""
|
|
66
|
+
profile = get_profile()
|
|
67
|
+
if not profile:
|
|
68
|
+
print(colorize("No user profile found! Let's set up your profile now.", Colors.YELLOW))
|
|
69
|
+
print()
|
|
70
|
+
handle_init(None)
|
|
71
|
+
profile = get_profile()
|
|
72
|
+
if not profile:
|
|
73
|
+
print(colorize("Error: Profile setup is required to run this command.", Colors.RED))
|
|
74
|
+
sys.exit(1)
|
|
75
|
+
return profile
|
|
76
|
+
|
|
77
|
+
# ---------------- COMMAND HANDLERS ----------------
|
|
78
|
+
|
|
79
|
+
def handle_init(args):
|
|
80
|
+
"""Runs the interactive wizard to set up or reset the profile."""
|
|
81
|
+
print_banner()
|
|
82
|
+
print(colorize("Welcome to Drunken Chunk setup! Let's build your profile.", Colors.BOLD + Colors.CYAN))
|
|
83
|
+
print(colorize("We will calculate your ideal hydration targets based on your dimensions.", Colors.GRAY))
|
|
84
|
+
print()
|
|
85
|
+
|
|
86
|
+
# Try getting existing profile
|
|
87
|
+
existing = get_profile()
|
|
88
|
+
if existing:
|
|
89
|
+
confirm = input(colorize(f"A profile for '{existing['name']}' already exists. Overwrite? (y/N): ", Colors.YELLOW)).strip().lower()
|
|
90
|
+
if confirm != 'y':
|
|
91
|
+
print("Setup cancelled.")
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
# User Input with basic validations
|
|
95
|
+
while True:
|
|
96
|
+
name = input("Enter your name: ").strip()
|
|
97
|
+
if name:
|
|
98
|
+
break
|
|
99
|
+
print(colorize("Name cannot be empty.", Colors.RED))
|
|
100
|
+
|
|
101
|
+
while True:
|
|
102
|
+
try:
|
|
103
|
+
weight = float(input("Enter your weight in kg (e.g. 72.5): "))
|
|
104
|
+
if weight > 0:
|
|
105
|
+
break
|
|
106
|
+
print(colorize("Weight must be greater than 0.", Colors.RED))
|
|
107
|
+
except ValueError:
|
|
108
|
+
print(colorize("Please enter a valid decimal number.", Colors.RED))
|
|
109
|
+
|
|
110
|
+
while True:
|
|
111
|
+
try:
|
|
112
|
+
height = float(input("Enter your height in cm (e.g. 178): "))
|
|
113
|
+
if height > 0:
|
|
114
|
+
break
|
|
115
|
+
print(colorize("Height must be greater than 0.", Colors.RED))
|
|
116
|
+
except ValueError:
|
|
117
|
+
print(colorize("Please enter a valid decimal number.", Colors.RED))
|
|
118
|
+
|
|
119
|
+
while True:
|
|
120
|
+
wake_input = input("Waking up time (24h format, e.g. 07:00 or 7:00): ").strip()
|
|
121
|
+
time_match = re.match(r'^(\d{1,2}):(\d{2})$', wake_input)
|
|
122
|
+
if time_match:
|
|
123
|
+
h, m = int(time_match.group(1)), int(time_match.group(2))
|
|
124
|
+
if 0 <= h < 24 and 0 <= m < 60:
|
|
125
|
+
wake = f"{h:02d}:{m:02d}"
|
|
126
|
+
break
|
|
127
|
+
print(colorize("Invalid format. Please enter as H:MM or HH:MM.", Colors.RED))
|
|
128
|
+
|
|
129
|
+
while True:
|
|
130
|
+
sleep_input = input("Sleeping time (24h format, e.g. 23:00 or 11:00): ").strip()
|
|
131
|
+
time_match = re.match(r'^(\d{1,2}):(\d{2})$', sleep_input)
|
|
132
|
+
if time_match:
|
|
133
|
+
h, m = int(time_match.group(1)), int(time_match.group(2))
|
|
134
|
+
if 0 <= h < 24 and 0 <= m < 60:
|
|
135
|
+
sleep = f"{h:02d}:{m:02d}"
|
|
136
|
+
break
|
|
137
|
+
print(colorize("Invalid format. Please enter as H:MM or HH:MM.", Colors.RED))
|
|
138
|
+
|
|
139
|
+
while True:
|
|
140
|
+
try:
|
|
141
|
+
interval_str = input("Reminder frequency in minutes (default 60): ").strip()
|
|
142
|
+
if not interval_str:
|
|
143
|
+
interval = 60
|
|
144
|
+
break
|
|
145
|
+
interval = int(interval_str)
|
|
146
|
+
if interval > 0:
|
|
147
|
+
break
|
|
148
|
+
print(colorize("Interval must be a positive integer.", Colors.RED))
|
|
149
|
+
except ValueError:
|
|
150
|
+
print(colorize("Please enter a valid integer.", Colors.RED))
|
|
151
|
+
|
|
152
|
+
# Calculate recommended intake
|
|
153
|
+
recommended = calculate_recommended_intake(weight, height)
|
|
154
|
+
bmi = calculate_bmi(weight, height)
|
|
155
|
+
bmi_cat, bmi_color = get_bmi_category(bmi)
|
|
156
|
+
|
|
157
|
+
print()
|
|
158
|
+
print(colorize(f"[ BODY PROFILE RESULTS ]", Colors.BOLD + Colors.CYAN))
|
|
159
|
+
print(f" - BMI: {colorize(f'{bmi:.1f}', Colors.BOLD)} ({colorize(bmi_cat, bmi_color)})")
|
|
160
|
+
print(f" - Calculated daily water target: {colorize(f'{recommended} ml', Colors.BOLD + Colors.GREEN)}")
|
|
161
|
+
print()
|
|
162
|
+
|
|
163
|
+
custom_target = None
|
|
164
|
+
override = input("Would you like to set a custom target? (y/N): ").strip().lower()
|
|
165
|
+
if override == 'y':
|
|
166
|
+
while True:
|
|
167
|
+
try:
|
|
168
|
+
custom_target = int(input("Enter custom target in ml (e.g. 2500): "))
|
|
169
|
+
if custom_target > 0:
|
|
170
|
+
break
|
|
171
|
+
print(colorize("Target must be greater than 0.", Colors.RED))
|
|
172
|
+
except ValueError:
|
|
173
|
+
print(colorize("Please enter a valid integer.", Colors.RED))
|
|
174
|
+
|
|
175
|
+
save_profile(name, weight, height, wake, sleep, interval, custom_target)
|
|
176
|
+
|
|
177
|
+
print()
|
|
178
|
+
print(colorize("[SUCCESS] Profile created successfully!", Colors.BOLD + Colors.GREEN))
|
|
179
|
+
print(colorize("To start receiving periodic notifications, run: ", Colors.GRAY) + colorize("drunken-chunk start", Colors.BOLD + Colors.WHITE))
|
|
180
|
+
print(colorize("To log water, run: ", Colors.GRAY) + colorize("drunken-chunk log <amount_or_shortcut>", Colors.BOLD + Colors.WHITE))
|
|
181
|
+
|
|
182
|
+
def get_next_alert_status(profile, today_logs):
|
|
183
|
+
"""Calculates next reminder alert timing or state for the user."""
|
|
184
|
+
pid = get_daemon_state("daemon_pid", "")
|
|
185
|
+
is_active = get_daemon_state("is_active", "0")
|
|
186
|
+
if not (is_active == "1" and pid and is_pid_running(pid)):
|
|
187
|
+
return "Service Stopped"
|
|
188
|
+
|
|
189
|
+
now = datetime.now()
|
|
190
|
+
current_time_str = now.strftime("%H:%M")
|
|
191
|
+
if not is_waking_hours(current_time_str, profile['wake_time'], profile['sleep_time']):
|
|
192
|
+
return "Quiet Hours"
|
|
193
|
+
|
|
194
|
+
target = profile['custom_target'] if profile['custom_target'] else calculate_recommended_intake(profile['weight'], profile['height'])
|
|
195
|
+
total_today = sum(log['amount'] for log in today_logs)
|
|
196
|
+
if total_today >= target:
|
|
197
|
+
return "Completed"
|
|
198
|
+
|
|
199
|
+
snooze_until_str = get_daemon_state("snooze_until", "")
|
|
200
|
+
if snooze_until_str:
|
|
201
|
+
try:
|
|
202
|
+
snooze_until = datetime.strptime(snooze_until_str, "%Y-%m-%d %H:%M:%S")
|
|
203
|
+
if now < snooze_until:
|
|
204
|
+
snooze_left = int((snooze_until - now).total_seconds() / 60.0)
|
|
205
|
+
return f"Snoozed ({snooze_left}m left)"
|
|
206
|
+
except ValueError:
|
|
207
|
+
pass
|
|
208
|
+
|
|
209
|
+
if today_logs:
|
|
210
|
+
last_drink_time = datetime.strptime(today_logs[-1]['timestamp'], "%Y-%m-%d %H:%M:%S")
|
|
211
|
+
else:
|
|
212
|
+
wake_h, wake_m = map(int, profile['wake_time'].split(':'))
|
|
213
|
+
last_drink_time = now.replace(hour=wake_h, minute=wake_m, second=0, microsecond=0)
|
|
214
|
+
if last_drink_time > now:
|
|
215
|
+
last_drink_time -= timedelta(days=1)
|
|
216
|
+
|
|
217
|
+
last_notified_str = get_daemon_state("last_notified", "")
|
|
218
|
+
if last_notified_str:
|
|
219
|
+
try:
|
|
220
|
+
last_notified_time = datetime.strptime(last_notified_str, "%Y-%m-%d %H:%M:%S")
|
|
221
|
+
except ValueError:
|
|
222
|
+
last_notified_time = datetime.min
|
|
223
|
+
else:
|
|
224
|
+
last_notified_time = datetime.min
|
|
225
|
+
|
|
226
|
+
interval_mins = profile['reminder_interval']
|
|
227
|
+
next_due_after_drink = last_drink_time + timedelta(minutes=interval_mins)
|
|
228
|
+
next_due_after_notif = last_notified_time + timedelta(minutes=interval_mins)
|
|
229
|
+
|
|
230
|
+
due_time = max(next_due_after_drink, next_due_after_notif)
|
|
231
|
+
|
|
232
|
+
mins_remaining = int((due_time - now).total_seconds() / 60.0)
|
|
233
|
+
if mins_remaining <= 0:
|
|
234
|
+
return "Alert Imminent"
|
|
235
|
+
else:
|
|
236
|
+
return f"Alert in {mins_remaining}m"
|
|
237
|
+
|
|
238
|
+
def handle_status(args):
|
|
239
|
+
"""Displays today's progress, logs, profile insights, and level."""
|
|
240
|
+
profile = check_profile_exists()
|
|
241
|
+
today_logs = get_today_logs()
|
|
242
|
+
|
|
243
|
+
total_today = sum(log['amount'] for log in today_logs)
|
|
244
|
+
target = profile['custom_target'] if profile['custom_target'] else calculate_recommended_intake(profile['weight'], profile['height'])
|
|
245
|
+
|
|
246
|
+
percentage = (total_today / target * 100) if target > 0 else 0
|
|
247
|
+
|
|
248
|
+
if hasattr(args, "mini") and args.mini:
|
|
249
|
+
bar = get_progress_bar(percentage, width=15)
|
|
250
|
+
alert_status = get_next_alert_status(profile, today_logs)
|
|
251
|
+
print(f"{bar} | {total_today}/{target} ml | {alert_status}")
|
|
252
|
+
return
|
|
253
|
+
|
|
254
|
+
# Get profile insights
|
|
255
|
+
insights_data = get_profile_insights(profile, total_today)
|
|
256
|
+
|
|
257
|
+
print_banner()
|
|
258
|
+
print(f"Hello, {bold(profile['name'])}!")
|
|
259
|
+
print(f"Daily Target: {bold(f'{target} ml')} | Today's Consumption: {colorize(f'{total_today} ml', Colors.GREEN if total_today >= target else Colors.CYAN)}")
|
|
260
|
+
print()
|
|
261
|
+
|
|
262
|
+
# Progress Bar
|
|
263
|
+
print(f"Progress: {get_progress_bar(percentage)}")
|
|
264
|
+
print()
|
|
265
|
+
|
|
266
|
+
# Hydration level
|
|
267
|
+
print(f"Current Level: {colorize(bold(insights_data['level_name']), insights_data['level_color'])}")
|
|
268
|
+
print(f"Status: {italic(insights_data['level_desc'])}")
|
|
269
|
+
print()
|
|
270
|
+
|
|
271
|
+
# Situational ASCII art & funny quote
|
|
272
|
+
art, quote = get_situational_art_and_quote(percentage / 100.0)
|
|
273
|
+
print(colorize(art, insights_data['level_color']))
|
|
274
|
+
print(colorize(f'"{quote}"', Colors.GRAY + Colors.BOLD))
|
|
275
|
+
print()
|
|
276
|
+
|
|
277
|
+
# Display Daemon status
|
|
278
|
+
pid = get_daemon_state("daemon_pid", "")
|
|
279
|
+
is_active = get_daemon_state("is_active", "0")
|
|
280
|
+
if is_active == "1" and pid and is_pid_running(pid):
|
|
281
|
+
daemon_status = colorize(f"ACTIVE (PID: {pid})", Colors.GREEN)
|
|
282
|
+
else:
|
|
283
|
+
daemon_status = colorize("STOPPED (Run 'drunken-chunk start' to activate)", Colors.RED)
|
|
284
|
+
print(f"Reminder Service: {daemon_status}")
|
|
285
|
+
print()
|
|
286
|
+
|
|
287
|
+
# Display Today's Logs in table
|
|
288
|
+
if today_logs:
|
|
289
|
+
rows = []
|
|
290
|
+
for i, log in enumerate(today_logs, 1):
|
|
291
|
+
log_time = log['timestamp'].split(' ')[1]
|
|
292
|
+
rows.append([f"#{i}", log_time, f"{log['amount']} ml", log['type']])
|
|
293
|
+
print_table(["Index", "Time Logged", "Amount", "Source"], rows, title="Today's Drink Logs")
|
|
294
|
+
else:
|
|
295
|
+
print(colorize("You haven't logged any water today yet. Stay active!", Colors.YELLOW))
|
|
296
|
+
print()
|
|
297
|
+
|
|
298
|
+
# Profile Insights
|
|
299
|
+
print(colorize("─── Body & Hydration Insights ───", Colors.BOLD + Colors.CYAN))
|
|
300
|
+
for insight in insights_data['insights']:
|
|
301
|
+
print(insight)
|
|
302
|
+
print()
|
|
303
|
+
|
|
304
|
+
def handle_log(args):
|
|
305
|
+
"""Logs water consumption using numbers or shortcuts."""
|
|
306
|
+
profile = check_profile_exists()
|
|
307
|
+
arg_input = args.amount_or_shortcut.strip().lower()
|
|
308
|
+
|
|
309
|
+
shortcuts = get_shortcuts()
|
|
310
|
+
|
|
311
|
+
# Check if numerical
|
|
312
|
+
if arg_input.isdigit():
|
|
313
|
+
amount = int(arg_input)
|
|
314
|
+
log_water(amount, "direct")
|
|
315
|
+
print(colorize(f"[SUCCESS] Gulp gulp! Logged {amount} ml of water. Your kidneys are celebrating!", Colors.GREEN))
|
|
316
|
+
# Check if it matches a shortcut
|
|
317
|
+
elif arg_input in shortcuts:
|
|
318
|
+
amount = shortcuts[arg_input]
|
|
319
|
+
log_water(amount, arg_input)
|
|
320
|
+
print(colorize(f"[SUCCESS] Gulp gulp! Logged {amount} ml of water using shortcut '{arg_input}'! Your kidneys are celebrating!", Colors.GREEN))
|
|
321
|
+
else:
|
|
322
|
+
print(colorize(f"Error: '{arg_input}' is not a valid number or shortcut.", Colors.RED))
|
|
323
|
+
if shortcuts:
|
|
324
|
+
print("Available shortcuts: " + ", ".join(f"{k} ({v}ml)" for k, v in shortcuts.items()))
|
|
325
|
+
else:
|
|
326
|
+
print("No shortcuts configured. Create one using: drunken-chunk shortcut add <name> <amount>")
|
|
327
|
+
|
|
328
|
+
def handle_log_custom(args):
|
|
329
|
+
"""Logs custom water amount at a specific past time."""
|
|
330
|
+
check_profile_exists()
|
|
331
|
+
amount = args.amount
|
|
332
|
+
time_str = args.time
|
|
333
|
+
|
|
334
|
+
if amount <= 0:
|
|
335
|
+
print(colorize("Amount must be a positive integer.", Colors.RED))
|
|
336
|
+
return
|
|
337
|
+
|
|
338
|
+
dt = parse_relative_time(time_str)
|
|
339
|
+
if not dt:
|
|
340
|
+
print(colorize(f"Error: Invalid time format '{time_str}'.", Colors.RED))
|
|
341
|
+
print(colorize("Supported formats: '10m ago', '2h ago', '14:30' (24h absolute), 'now'.", Colors.GRAY))
|
|
342
|
+
return
|
|
343
|
+
|
|
344
|
+
timestamp = dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
345
|
+
log_water(amount, "custom-log", timestamp)
|
|
346
|
+
print(colorize(f"[SUCCESS] Time-travel logging complete. Registered {amount} ml at {dt.strftime('%H:%M')} ({time_str}).", Colors.GREEN))
|
|
347
|
+
|
|
348
|
+
def handle_shortcut(args):
|
|
349
|
+
"""Manages custom drinking shortcuts."""
|
|
350
|
+
check_profile_exists()
|
|
351
|
+
subcommand = args.subcommand
|
|
352
|
+
|
|
353
|
+
if subcommand == "add":
|
|
354
|
+
if not args.name or not args.amount:
|
|
355
|
+
print(colorize("Error: Name and Amount are required for 'add'.", Colors.RED))
|
|
356
|
+
print("Usage: drunken-chunk shortcut add <name> <amount_in_ml>")
|
|
357
|
+
return
|
|
358
|
+
success, msg = add_custom_shortcut(args.name, args.amount)
|
|
359
|
+
print(colorize(msg, Colors.GREEN if success else Colors.RED))
|
|
360
|
+
|
|
361
|
+
elif subcommand == "remove":
|
|
362
|
+
if not args.name:
|
|
363
|
+
print(colorize("Error: Name is required for 'remove'.", Colors.RED))
|
|
364
|
+
print("Usage: drunken-chunk shortcut remove <name>")
|
|
365
|
+
return
|
|
366
|
+
success, msg = remove_custom_shortcut(args.name)
|
|
367
|
+
print(colorize(msg, Colors.GREEN if success else Colors.RED))
|
|
368
|
+
|
|
369
|
+
elif subcommand == "list" or subcommand is None:
|
|
370
|
+
shortcuts = get_shortcuts()
|
|
371
|
+
if not shortcuts:
|
|
372
|
+
print(colorize("No custom shortcuts registered.", Colors.YELLOW))
|
|
373
|
+
return
|
|
374
|
+
rows = [[name, f"{amount} ml"] for name, amount in shortcuts.items()]
|
|
375
|
+
print_table(["Shortcut Name", "Intake Volume"], rows, title="Configured Shortcuts")
|
|
376
|
+
|
|
377
|
+
def handle_profile(args):
|
|
378
|
+
"""View or update user profile parameters."""
|
|
379
|
+
# Delete does not strictly require check_profile_exists to succeed, but we want to know if one exists first.
|
|
380
|
+
profile = get_profile()
|
|
381
|
+
|
|
382
|
+
if args.delete:
|
|
383
|
+
if not profile:
|
|
384
|
+
print(colorize("No profile exists to delete.", Colors.YELLOW))
|
|
385
|
+
return
|
|
386
|
+
|
|
387
|
+
confirm = input(colorize("Are you sure you want to delete your profile? (y/N): ", Colors.RED + Colors.BOLD)).strip().lower()
|
|
388
|
+
if confirm == 'y':
|
|
389
|
+
# Stop daemon first if active
|
|
390
|
+
stop_background_daemon()
|
|
391
|
+
|
|
392
|
+
# Check logs deletion
|
|
393
|
+
delete_logs = input(colorize("Do you also want to delete all your water consumption logs? (y/N): ", Colors.YELLOW)).strip().lower()
|
|
394
|
+
|
|
395
|
+
from .db import get_connection
|
|
396
|
+
conn = get_connection()
|
|
397
|
+
cursor = conn.cursor()
|
|
398
|
+
cursor.execute("DELETE FROM profile")
|
|
399
|
+
if delete_logs == 'y':
|
|
400
|
+
cursor.execute("DELETE FROM water_log")
|
|
401
|
+
print(colorize("Historical water consumption logs deleted.", Colors.YELLOW))
|
|
402
|
+
cursor.execute("DELETE FROM reminder_state")
|
|
403
|
+
cursor.execute("INSERT OR IGNORE INTO reminder_state (key, value) VALUES ('is_active', '0')")
|
|
404
|
+
cursor.execute("INSERT OR IGNORE INTO reminder_state (key, value) VALUES ('last_notified', '')")
|
|
405
|
+
conn.commit()
|
|
406
|
+
conn.close()
|
|
407
|
+
print(colorize("[SUCCESS] Profile deleted successfully. No data loss of logs occurred unless explicitly chosen.", Colors.GREEN))
|
|
408
|
+
else:
|
|
409
|
+
print("Deletion cancelled.")
|
|
410
|
+
return
|
|
411
|
+
|
|
412
|
+
# Check existence for other operations
|
|
413
|
+
profile = check_profile_exists()
|
|
414
|
+
|
|
415
|
+
if args.edit:
|
|
416
|
+
print(colorize("--- Interactive Profile Editor ---", Colors.BOLD + Colors.CYAN))
|
|
417
|
+
print("Press Enter to keep the current value.")
|
|
418
|
+
print()
|
|
419
|
+
|
|
420
|
+
# Name
|
|
421
|
+
name = input(f"Enter your name [current: {profile['name']}]: ").strip()
|
|
422
|
+
if not name:
|
|
423
|
+
name = profile['name']
|
|
424
|
+
|
|
425
|
+
# Weight
|
|
426
|
+
while True:
|
|
427
|
+
weight_str = input(f"Enter weight in kg [current: {profile['weight']}]: ").strip()
|
|
428
|
+
if not weight_str:
|
|
429
|
+
weight = profile['weight']
|
|
430
|
+
break
|
|
431
|
+
try:
|
|
432
|
+
weight = float(weight_str)
|
|
433
|
+
if weight > 0:
|
|
434
|
+
break
|
|
435
|
+
print(colorize("Weight must be greater than 0.", Colors.RED))
|
|
436
|
+
except ValueError:
|
|
437
|
+
print(colorize("Please enter a valid decimal number.", Colors.RED))
|
|
438
|
+
|
|
439
|
+
# Height
|
|
440
|
+
while True:
|
|
441
|
+
height_str = input(f"Enter height in cm [current: {profile['height']}]: ").strip()
|
|
442
|
+
if not height_str:
|
|
443
|
+
height = profile['height']
|
|
444
|
+
break
|
|
445
|
+
try:
|
|
446
|
+
height = float(height_str)
|
|
447
|
+
if height > 0:
|
|
448
|
+
break
|
|
449
|
+
print(colorize("Height must be greater than 0.", Colors.RED))
|
|
450
|
+
except ValueError:
|
|
451
|
+
print(colorize("Please enter a valid decimal number.", Colors.RED))
|
|
452
|
+
|
|
453
|
+
# Wake time
|
|
454
|
+
while True:
|
|
455
|
+
wake_input = input(f"Waking up time [current: {profile['wake_time']}]: ").strip()
|
|
456
|
+
if not wake_input:
|
|
457
|
+
wake = profile['wake_time']
|
|
458
|
+
break
|
|
459
|
+
time_match = re.match(r'^(\d{1,2}):(\d{2})$', wake_input)
|
|
460
|
+
if time_match:
|
|
461
|
+
h, m = int(time_match.group(1)), int(time_match.group(2))
|
|
462
|
+
if 0 <= h < 24 and 0 <= m < 60:
|
|
463
|
+
wake = f"{h:02d}:{m:02d}"
|
|
464
|
+
break
|
|
465
|
+
print(colorize("Invalid format. Please enter as H:MM or HH:MM.", Colors.RED))
|
|
466
|
+
|
|
467
|
+
# Sleep time
|
|
468
|
+
while True:
|
|
469
|
+
sleep_input = input(f"Sleeping time [current: {profile['sleep_time']}]: ").strip()
|
|
470
|
+
if not sleep_input:
|
|
471
|
+
sleep = profile['sleep_time']
|
|
472
|
+
break
|
|
473
|
+
time_match = re.match(r'^(\d{1,2}):(\d{2})$', sleep_input)
|
|
474
|
+
if time_match:
|
|
475
|
+
h, m = int(time_match.group(1)), int(time_match.group(2))
|
|
476
|
+
if 0 <= h < 24 and 0 <= m < 60:
|
|
477
|
+
sleep = f"{h:02d}:{m:02d}"
|
|
478
|
+
break
|
|
479
|
+
print(colorize("Invalid format. Please enter as H:MM or HH:MM.", Colors.RED))
|
|
480
|
+
|
|
481
|
+
# Interval
|
|
482
|
+
while True:
|
|
483
|
+
interval_str = input(f"Reminder frequency in minutes [current: {profile['reminder_interval']}]: ").strip()
|
|
484
|
+
if not interval_str:
|
|
485
|
+
interval = profile['reminder_interval']
|
|
486
|
+
break
|
|
487
|
+
try:
|
|
488
|
+
interval = int(interval_str)
|
|
489
|
+
if interval > 0:
|
|
490
|
+
break
|
|
491
|
+
print(colorize("Interval must be a positive integer.", Colors.RED))
|
|
492
|
+
except ValueError:
|
|
493
|
+
print(colorize("Please enter a valid integer.", Colors.RED))
|
|
494
|
+
|
|
495
|
+
# Daily target
|
|
496
|
+
recommended = calculate_recommended_intake(weight, height)
|
|
497
|
+
curr_target_desc = f"{profile['custom_target']} ml" if profile['custom_target'] else f"{recommended} ml (Auto-calculated)"
|
|
498
|
+
while True:
|
|
499
|
+
target_str = input(f"Daily target in ml [current: {curr_target_desc} - enter 'reset' for auto]: ").strip()
|
|
500
|
+
if not target_str:
|
|
501
|
+
custom_target = profile['custom_target']
|
|
502
|
+
break
|
|
503
|
+
if target_str.lower() == 'reset':
|
|
504
|
+
custom_target = None
|
|
505
|
+
break
|
|
506
|
+
try:
|
|
507
|
+
custom_target = int(target_str)
|
|
508
|
+
if custom_target > 0:
|
|
509
|
+
break
|
|
510
|
+
print(colorize("Target must be a positive integer.", Colors.RED))
|
|
511
|
+
except ValueError:
|
|
512
|
+
print(colorize("Please enter a valid integer or 'reset'.", Colors.RED))
|
|
513
|
+
|
|
514
|
+
save_profile(name, weight, height, wake, sleep, interval, custom_target)
|
|
515
|
+
print()
|
|
516
|
+
print(colorize("[SUCCESS] Profile updated successfully interactively!", Colors.GREEN))
|
|
517
|
+
profile = get_profile() # Reload
|
|
518
|
+
|
|
519
|
+
elif args.weight or args.height or args.wake or args.sleep or args.interval or args.target:
|
|
520
|
+
# Perform updates
|
|
521
|
+
name = profile['name']
|
|
522
|
+
weight = args.weight if args.weight else profile['weight']
|
|
523
|
+
height = args.height if args.height else profile['height']
|
|
524
|
+
wake = args.wake if args.wake else profile['wake_time']
|
|
525
|
+
sleep = args.sleep if args.sleep else profile['sleep_time']
|
|
526
|
+
interval = args.interval if args.interval else profile['reminder_interval']
|
|
527
|
+
custom_target = profile['custom_target']
|
|
528
|
+
|
|
529
|
+
if args.target:
|
|
530
|
+
if args.target.lower() == 'reset':
|
|
531
|
+
custom_target = None
|
|
532
|
+
else:
|
|
533
|
+
try:
|
|
534
|
+
custom_target = int(args.target)
|
|
535
|
+
except ValueError:
|
|
536
|
+
print(colorize("Target must be a number or 'reset'.", Colors.RED))
|
|
537
|
+
return
|
|
538
|
+
|
|
539
|
+
save_profile(name, weight, height, wake, sleep, interval, custom_target)
|
|
540
|
+
print(colorize("Profile updated successfully!", Colors.GREEN))
|
|
541
|
+
profile = get_profile() # Reload
|
|
542
|
+
|
|
543
|
+
# Display Profile
|
|
544
|
+
recommended = calculate_recommended_intake(profile['weight'], profile['height'])
|
|
545
|
+
target = profile['custom_target'] if profile['custom_target'] else recommended
|
|
546
|
+
bmi = calculate_bmi(profile['weight'], profile['height'])
|
|
547
|
+
bmi_cat, bmi_color = get_bmi_category(bmi)
|
|
548
|
+
|
|
549
|
+
print_table(
|
|
550
|
+
["Field", "Value"],
|
|
551
|
+
[
|
|
552
|
+
["Name", profile['name']],
|
|
553
|
+
["Weight", f"{profile['weight']} kg"],
|
|
554
|
+
["Height", f"{profile['height']} cm"],
|
|
555
|
+
["BMI", f"{bmi:.1f} ({bmi_cat})"],
|
|
556
|
+
["Waking Hours", f"{profile['wake_time']} - {profile['sleep_time']}"],
|
|
557
|
+
["Reminder Interval", f"{profile['reminder_interval']} minutes"],
|
|
558
|
+
["Recommended Target", f"{recommended} ml"],
|
|
559
|
+
["Current Target", f"{target} ml" + (" (Custom)" if profile['custom_target'] else " (Auto)")],
|
|
560
|
+
["Member Since", profile['joined_date'].split(' ')[0]]
|
|
561
|
+
],
|
|
562
|
+
title="User Profile Details"
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
def handle_history(args):
|
|
566
|
+
"""Shows logging history over the last N days."""
|
|
567
|
+
profile = check_profile_exists()
|
|
568
|
+
days = args.days
|
|
569
|
+
|
|
570
|
+
logs = get_history_logs(days)
|
|
571
|
+
if not logs:
|
|
572
|
+
print(colorize(f"No history logs found for the last {days} days.", Colors.YELLOW))
|
|
573
|
+
return
|
|
574
|
+
|
|
575
|
+
target = profile['custom_target'] if profile['custom_target'] else calculate_recommended_intake(profile['weight'], profile['height'])
|
|
576
|
+
|
|
577
|
+
# Group logs by date
|
|
578
|
+
daily_groups = collections.defaultdict(int)
|
|
579
|
+
daily_counts = collections.defaultdict(int)
|
|
580
|
+
for log in logs:
|
|
581
|
+
date = log['timestamp'].split(' ')[0]
|
|
582
|
+
daily_groups[date] += log['amount']
|
|
583
|
+
daily_counts[date] += 1
|
|
584
|
+
|
|
585
|
+
rows = []
|
|
586
|
+
# Print chronological or reverse chronological? Let's do reverse chronological (newest first)
|
|
587
|
+
sorted_dates = sorted(daily_groups.keys(), reverse=True)
|
|
588
|
+
|
|
589
|
+
for date in sorted_dates:
|
|
590
|
+
amount = daily_groups[date]
|
|
591
|
+
count = daily_counts[date]
|
|
592
|
+
pct = (amount / target * 100) if target > 0 else 0
|
|
593
|
+
status_lvl, _, _ = get_hydration_level(amount, target)
|
|
594
|
+
|
|
595
|
+
# Colorize success
|
|
596
|
+
amt_str = colorize(f"{amount} ml", Colors.GREEN if amount >= target else Colors.CYAN)
|
|
597
|
+
bar = get_progress_bar(pct, width=15)
|
|
598
|
+
|
|
599
|
+
rows.append([
|
|
600
|
+
date,
|
|
601
|
+
get_day_name(date),
|
|
602
|
+
amt_str,
|
|
603
|
+
f"{count} drinks",
|
|
604
|
+
bar,
|
|
605
|
+
status_lvl.split(' [')[0] if ' [' in status_lvl else status_lvl
|
|
606
|
+
])
|
|
607
|
+
|
|
608
|
+
print_table(["Date", "Day", "Total Consumed", "Count", "Progress Bar", "Level"], rows, title=f"Hydration Log - Last {days} Days")
|
|
609
|
+
|
|
610
|
+
def handle_analyze(args):
|
|
611
|
+
"""Runs the analytics engine to draw deep drinking habits insight."""
|
|
612
|
+
check_profile_exists()
|
|
613
|
+
analysis = analyze_drinking_behavior()
|
|
614
|
+
|
|
615
|
+
if "error" in analysis:
|
|
616
|
+
print(colorize(analysis["error"], Colors.RED))
|
|
617
|
+
return
|
|
618
|
+
|
|
619
|
+
print_banner()
|
|
620
|
+
print(bold(colorize("[ HYDRO-TELEMETRY INTELLIGENCE REPORT ]", Colors.CYAN)))
|
|
621
|
+
print(colorize("Analyzing your historical logs to map dehydration vectors & habit profiles.", Colors.GRAY))
|
|
622
|
+
print()
|
|
623
|
+
|
|
624
|
+
# 1. High-level Summary Card
|
|
625
|
+
target = analysis['target']
|
|
626
|
+
summary_rows = [
|
|
627
|
+
["Total Consumed Logs", f"{analysis['total_consumed']} ml across {analysis['total_entries']} entries"],
|
|
628
|
+
["Unique Active Days", f"{analysis['unique_days']} days"],
|
|
629
|
+
["Average Daily Intake", f"{analysis['avg_daily']:.0f} ml / day (Target: {target} ml)"],
|
|
630
|
+
["Target Hit Rate", f"{analysis['hit_rate']:.1f}% of days"],
|
|
631
|
+
["Consistency Index", f"{analysis['consistency_index']:.1f}% (Met 80% target in last 14 days)"]
|
|
632
|
+
]
|
|
633
|
+
print_table(["Metric", "Measurement"], summary_rows, title="Summary Statistics")
|
|
634
|
+
|
|
635
|
+
# 2. Weekday Analysis Graph
|
|
636
|
+
weekday_rows = []
|
|
637
|
+
for day, avg in analysis['weekday_averages'].items():
|
|
638
|
+
pct = (avg / target * 100) if target > 0 else 0
|
|
639
|
+
bar = get_progress_bar(pct, width=12)
|
|
640
|
+
weekday_rows.append([day, f"{avg:.0f} ml", bar])
|
|
641
|
+
print_table(["Day of Week", "Average Volume", "Progress Vs Target"], weekday_rows, title="Weekday Analysis")
|
|
642
|
+
|
|
643
|
+
# 3. Time of Day Analysis Graph
|
|
644
|
+
time_rows = []
|
|
645
|
+
for block, pct in analysis['time_block_pct'].items():
|
|
646
|
+
# Custom small bar for percentage
|
|
647
|
+
bar_len = int(pct // 5)
|
|
648
|
+
bar = colorize("█" * bar_len, Colors.CYAN) + colorize("░" * (20 - bar_len), Colors.GRAY)
|
|
649
|
+
time_rows.append([block, f"{pct:.1f}%", f"[{bar}]"])
|
|
650
|
+
print_table(["Time Window", "Share of Consumption", "Visual Spread"], time_rows, title="Diurnal Hydration Spread")
|
|
651
|
+
|
|
652
|
+
# 4. Habits, Insights and Warnings
|
|
653
|
+
print(colorize("─── Predictive Insights & Suggestions ───", Colors.BOLD + Colors.CYAN))
|
|
654
|
+
for sug in analysis['suggestions']:
|
|
655
|
+
print(sug)
|
|
656
|
+
print()
|
|
657
|
+
|
|
658
|
+
def handle_start(args):
|
|
659
|
+
"""Starts the notification background daemon."""
|
|
660
|
+
check_profile_exists()
|
|
661
|
+
success, msg = start_background_daemon()
|
|
662
|
+
if success:
|
|
663
|
+
print(colorize(msg, Colors.GREEN))
|
|
664
|
+
else:
|
|
665
|
+
print(colorize(msg, Colors.YELLOW))
|
|
666
|
+
|
|
667
|
+
def handle_stop(args):
|
|
668
|
+
"""Stops the notification background daemon."""
|
|
669
|
+
success, msg = stop_background_daemon()
|
|
670
|
+
if success:
|
|
671
|
+
print(colorize(msg, Colors.GREEN))
|
|
672
|
+
else:
|
|
673
|
+
print(colorize(msg, Colors.RED))
|
|
674
|
+
|
|
675
|
+
def handle_snooze(args):
|
|
676
|
+
"""Snoozes the water reminder alerts for a specific time."""
|
|
677
|
+
check_profile_exists()
|
|
678
|
+
minutes = args.minutes
|
|
679
|
+
if minutes <= 0:
|
|
680
|
+
print(colorize("[ERROR] Snooze duration must be a positive integer.", Colors.RED))
|
|
681
|
+
return
|
|
682
|
+
|
|
683
|
+
snooze_until = datetime.now() + timedelta(minutes=minutes)
|
|
684
|
+
snooze_until_str = snooze_until.strftime("%Y-%m-%d %H:%M:%S")
|
|
685
|
+
set_daemon_state("snooze_until", snooze_until_str)
|
|
686
|
+
print(colorize(f"[SUCCESS] Alerts silenced. Go hide from your responsibilities (and hydration) for {minutes} minutes (until {snooze_until.strftime('%H:%M:%S')}).", Colors.GREEN))
|
|
687
|
+
|
|
688
|
+
def handle_undo(args):
|
|
689
|
+
"""Undoes and deletes the last water log recorded."""
|
|
690
|
+
check_profile_exists()
|
|
691
|
+
from .db import undo_last_log
|
|
692
|
+
undone = undo_last_log()
|
|
693
|
+
if undone:
|
|
694
|
+
print(colorize(f"[SUCCESS] Reverting time like a water-bending wizard. Undid last drink: deleted {undone['amount']} ml (logged at {undone['timestamp'].split(' ')[1]}).", Colors.GREEN))
|
|
695
|
+
else:
|
|
696
|
+
print(colorize("[WARNING] No water log entries found to undo.", Colors.YELLOW))
|
|
697
|
+
|
|
698
|
+
def handle_autostart(args):
|
|
699
|
+
"""Handles system startup autostart toggling/status operations."""
|
|
700
|
+
check_profile_exists()
|
|
701
|
+
from .reminder import manage_autostart
|
|
702
|
+
success, msg = manage_autostart(args.action)
|
|
703
|
+
if success:
|
|
704
|
+
print(colorize(msg, Colors.GREEN))
|
|
705
|
+
else:
|
|
706
|
+
print(colorize(f"[ERROR] {msg}", Colors.RED))
|
|
707
|
+
|
|
708
|
+
# ---------------- CLI ARGUMENT ROUTING ----------------
|
|
709
|
+
|
|
710
|
+
def main():
|
|
711
|
+
# Initialize the database and create tables if they do not exist yet
|
|
712
|
+
init_db()
|
|
713
|
+
|
|
714
|
+
# Intercept '-help' typo and convert it to standard '--help'
|
|
715
|
+
for idx, arg in enumerate(sys.argv):
|
|
716
|
+
if arg.lower() in ('-help', '-hlep', '--hlep'):
|
|
717
|
+
sys.argv[idx] = '--help'
|
|
718
|
+
|
|
719
|
+
parser = argparse.ArgumentParser(
|
|
720
|
+
description="drunken-chunk - Premium CLI Water Tracker, Reminders, and Behavioral Analytics."
|
|
721
|
+
)
|
|
722
|
+
subparsers = parser.add_subparsers(dest="command", help="Subcommand to execute")
|
|
723
|
+
|
|
724
|
+
# init
|
|
725
|
+
subparsers.add_parser("init", help="Run interactive setup/wizard to build your profile. Example: drunken-chunk init")
|
|
726
|
+
|
|
727
|
+
# status
|
|
728
|
+
status_parser = subparsers.add_parser("status", help="Show today's logging dashboard and profile insights. Example: drunken-chunk status (or status -m)")
|
|
729
|
+
status_parser.add_argument("-m", "--mini", action="store_true", help="Print a single-line compact status summary")
|
|
730
|
+
|
|
731
|
+
# log
|
|
732
|
+
log_parser = subparsers.add_parser("log", help="Log water intake. Example: drunken-chunk log 300 (or log glass)")
|
|
733
|
+
log_parser.add_argument(
|
|
734
|
+
"amount_or_shortcut",
|
|
735
|
+
help="Volume in ml (e.g. 250) or a registered shortcut name (e.g. glass)"
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
# log-custom
|
|
739
|
+
log_cust_parser = subparsers.add_parser("log-custom", help="Log water at a custom/relative time. Example: drunken-chunk log-custom 250 '10m ago'")
|
|
740
|
+
log_cust_parser.add_argument("amount", type=int, help="Volume in ml")
|
|
741
|
+
log_cust_parser.add_argument("time", help="Relative time (e.g. '10m ago', '2h ago') or absolute HH:MM")
|
|
742
|
+
|
|
743
|
+
# shortcut
|
|
744
|
+
short_parser = subparsers.add_parser("shortcut", help="Manage shortcuts. Example: drunken-chunk shortcut list (or shortcut add cup 150)")
|
|
745
|
+
short_sub = short_parser.add_subparsers(dest="subcommand", help="Shortcut operation")
|
|
746
|
+
|
|
747
|
+
# shortcut add
|
|
748
|
+
add_sub = short_sub.add_parser("add", help="Add/update a custom shortcut. Example: drunken-chunk shortcut add mug 350")
|
|
749
|
+
add_sub.add_argument("name", help="Name of shortcut (e.g. mug)")
|
|
750
|
+
add_sub.add_argument("amount", help="Water volume in ml (e.g. 350)")
|
|
751
|
+
|
|
752
|
+
# shortcut remove
|
|
753
|
+
rem_sub = short_sub.add_parser("remove", help="Remove a shortcut. Example: drunken-chunk shortcut remove bottle")
|
|
754
|
+
rem_sub.add_argument("name", help="Name of shortcut to remove")
|
|
755
|
+
|
|
756
|
+
# shortcut list
|
|
757
|
+
short_sub.add_parser("list", help="List registered shortcuts. Example: drunken-chunk shortcut list")
|
|
758
|
+
|
|
759
|
+
# profile
|
|
760
|
+
prof_parser = subparsers.add_parser("profile", help="View or modify your profile. Example: drunken-chunk profile --weight 72.5")
|
|
761
|
+
prof_parser.add_argument("--weight", type=float, help="Update weight in kg")
|
|
762
|
+
prof_parser.add_argument("--height", type=float, help="Update height in cm")
|
|
763
|
+
prof_parser.add_argument("--wake", help="Update wake time (HH:MM)")
|
|
764
|
+
prof_parser.add_argument("--sleep", help="Update sleep time (HH:MM)")
|
|
765
|
+
prof_parser.add_argument("--interval", type=int, help="Update reminder interval in minutes")
|
|
766
|
+
prof_parser.add_argument("--target", help="Update daily target in ml (or 'reset' to auto-calculate)")
|
|
767
|
+
prof_parser.add_argument("--edit", action="store_true", help="Interactively edit your profile data. Example: drunken-chunk profile --edit")
|
|
768
|
+
prof_parser.add_argument("--delete", action="store_true", help="Delete your profile (with warning check). Example: drunken-chunk profile --delete")
|
|
769
|
+
|
|
770
|
+
# history
|
|
771
|
+
hist_parser = subparsers.add_parser("history", help="Show intake logs over past days. Example: drunken-chunk history --days 5")
|
|
772
|
+
hist_parser.add_argument("--days", type=int, default=7, help="Number of history days to show (default: 7)")
|
|
773
|
+
|
|
774
|
+
# analyze
|
|
775
|
+
subparsers.add_parser("analyze", help="Analyze drinking behavior, habits, and get health insights. Example: drunken-chunk analyze")
|
|
776
|
+
|
|
777
|
+
# start daemon
|
|
778
|
+
subparsers.add_parser("start", help="Start background water reminder daemon. Example: drunken-chunk start")
|
|
779
|
+
|
|
780
|
+
# stop daemon
|
|
781
|
+
subparsers.add_parser("stop", help="Stop background water reminder daemon. Example: drunken-chunk stop")
|
|
782
|
+
|
|
783
|
+
# snooze
|
|
784
|
+
snooze_parser = subparsers.add_parser("snooze", help="Snooze reminder notifications. Example: drunken-chunk snooze 45")
|
|
785
|
+
snooze_parser.add_argument("minutes", type=int, nargs="?", default=30, help="Snooze duration in minutes (default: 30)")
|
|
786
|
+
|
|
787
|
+
# undo
|
|
788
|
+
subparsers.add_parser("undo", help="Undo/delete the last logged water consumption. Example: drunken-chunk undo")
|
|
789
|
+
|
|
790
|
+
# autostart
|
|
791
|
+
auto_parser = subparsers.add_parser("autostart", help="Toggle or configure system startup autostart. Example: drunken-chunk autostart status")
|
|
792
|
+
auto_parser.add_argument("action", nargs="?", default="toggle", choices=["toggle", "enable", "disable", "status"], help="Autostart action to perform (default: toggle)")
|
|
793
|
+
|
|
794
|
+
# Parse arguments
|
|
795
|
+
args = parser.parse_args()
|
|
796
|
+
|
|
797
|
+
# Route command to appropriate handler
|
|
798
|
+
if args.command == "init":
|
|
799
|
+
handle_init(args)
|
|
800
|
+
elif args.command == "status":
|
|
801
|
+
handle_status(args)
|
|
802
|
+
elif args.command == "log":
|
|
803
|
+
handle_log(args)
|
|
804
|
+
elif args.command == "log-custom":
|
|
805
|
+
handle_log_custom(args)
|
|
806
|
+
elif args.command == "shortcut":
|
|
807
|
+
handle_shortcut(args)
|
|
808
|
+
elif args.command == "profile":
|
|
809
|
+
handle_profile(args)
|
|
810
|
+
elif args.command == "history":
|
|
811
|
+
handle_history(args)
|
|
812
|
+
elif args.command == "analyze":
|
|
813
|
+
handle_analyze(args)
|
|
814
|
+
elif args.command == "start":
|
|
815
|
+
handle_start(args)
|
|
816
|
+
elif args.command == "stop":
|
|
817
|
+
handle_stop(args)
|
|
818
|
+
elif args.command == "snooze":
|
|
819
|
+
handle_snooze(args)
|
|
820
|
+
elif args.command == "undo":
|
|
821
|
+
handle_undo(args)
|
|
822
|
+
elif args.command == "autostart":
|
|
823
|
+
handle_autostart(args)
|
|
824
|
+
else:
|
|
825
|
+
# Default behavior: show status if profile exists, otherwise banner + help
|
|
826
|
+
profile = get_profile()
|
|
827
|
+
if profile:
|
|
828
|
+
handle_status(args)
|
|
829
|
+
else:
|
|
830
|
+
print_banner()
|
|
831
|
+
parser.print_help()
|
|
832
|
+
|
|
833
|
+
if __name__ == "__main__":
|
|
834
|
+
main()
|