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.
@@ -0,0 +1,386 @@
1
+ import os
2
+ import sys
3
+ import time
4
+ import subprocess
5
+ import random
6
+ from datetime import datetime, timedelta
7
+
8
+ from .db import (
9
+ get_profile, get_today_logs, get_daemon_state, set_daemon_state, init_db
10
+ )
11
+ from .profile import calculate_recommended_intake
12
+
13
+ PACKAGE_PARENT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
14
+
15
+ # List of rich, motivational, and sometimes humorous notifications
16
+ REMINDER_MESSAGES = [
17
+ "Hydrate or diedrate! Drink up!",
18
+ "A hydrated brain writes fewer bugs. Go grab a cup!",
19
+ "Drunken Chunk alert: Time to wash down those keystrokes!",
20
+ "Your cells are crying out for water! Give them a gulp!",
21
+ "Hydration check! Put down your mouse and pick up your flask.",
22
+ "Drink water now. Future hydrated you will thank you!",
23
+ "Water: 0 calories, 100% vital. Pour yourself a fresh glass!",
24
+ "Time to level up your hydration status. Sip sip hooray!"
25
+ ]
26
+
27
+ def send_windows_toast(title, message):
28
+ """Sends a Windows native toast notification via a headless PowerShell script."""
29
+ escaped_title = title.replace('"', '`"').replace("'", "`'")
30
+ escaped_message = message.replace('"', '`"').replace("'", "`'")
31
+
32
+ ps_code = f'''
33
+ [void][Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime]
34
+ $Template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02)
35
+ $Xml = [xml]$Template.GetXml()
36
+ $Xml.toast.visual.binding.text[0].AppendChild($Xml.CreateTextNode("{escaped_title}")) | Out-Null
37
+ $Xml.toast.visual.binding.text[1].AppendChild($Xml.CreateTextNode("{escaped_message}")) | Out-Null
38
+ $XmlDocument = New-Object Windows.Data.Xml.Dom.XmlDocument
39
+ $XmlDocument.LoadXml($Xml.OuterXml)
40
+ $Toast = [Windows.UI.Notifications.ToastNotification]::new($XmlDocument)
41
+ [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("DrunkenChunk.Reminder").Show($Toast)
42
+ '''
43
+
44
+ # Hide window completely using creationflags=0x08000000 on Windows
45
+ creation_flags = 0x08000000 if os.name == 'nt' else 0
46
+ try:
47
+ subprocess.run(
48
+ ["powershell", "-NoProfile", "-NonInteractive", "-Command", ps_code],
49
+ capture_output=True,
50
+ text=True,
51
+ creationflags=creation_flags
52
+ )
53
+ except Exception:
54
+ # Fallback if Powershell fails
55
+ pass
56
+
57
+ def send_macos_notification(title, message):
58
+ """Sends a macOS native desktop notification via AppleScript."""
59
+ escaped_title = title.replace('\\', '\\\\').replace('"', '\\"')
60
+ escaped_message = message.replace('\\', '\\\\').replace('"', '\\"')
61
+ cmd = ["osascript", "-e", f'display notification "{escaped_message}" with title "{escaped_title}"']
62
+ try:
63
+ subprocess.run(cmd, capture_output=True, check=False)
64
+ except Exception:
65
+ pass
66
+
67
+ def send_linux_notification(title, message):
68
+ """Sends a Linux desktop notification via notify-send."""
69
+ try:
70
+ subprocess.run(["notify-send", title, message], capture_output=True, check=False)
71
+ except Exception:
72
+ pass
73
+
74
+ def send_notification(title, message):
75
+ """Dispatches native desktop notifications based on the current platform."""
76
+ if sys.platform == 'win32':
77
+ send_windows_toast(title, message)
78
+ elif sys.platform == 'darwin':
79
+ send_macos_notification(title, message)
80
+ else:
81
+ send_linux_notification(title, message)
82
+
83
+ def is_waking_hours(current_time_str, wake_time_str, sleep_time_str):
84
+ """
85
+ Checks if current time (HH:MM) is within the waking hours range.
86
+ Supports shift workers (e.g., wake at 22:00, sleep at 06:00).
87
+ """
88
+ curr_h, curr_m = map(int, current_time_str.split(':'))
89
+ wake_h, wake_m = map(int, wake_time_str.split(':'))
90
+ sleep_h, sleep_m = map(int, sleep_time_str.split(':'))
91
+
92
+ curr = curr_h * 60 + curr_m
93
+ wake = wake_h * 60 + wake_m
94
+ sleep = sleep_h * 60 + sleep_m
95
+
96
+ if wake <= sleep:
97
+ # Day shift (e.g. 07:00 to 22:00)
98
+ return wake <= curr <= sleep
99
+ else:
100
+ # Night shift (e.g. 22:00 to 06:00 next day)
101
+ return curr >= wake or curr <= sleep
102
+
103
+ def check_and_notify():
104
+ """Runs the core reminder algorithm logic."""
105
+ # Check if currently snoozed
106
+ snooze_until_str = get_daemon_state("snooze_until", "")
107
+ if snooze_until_str:
108
+ try:
109
+ snooze_until = datetime.strptime(snooze_until_str, "%Y-%m-%d %H:%M:%S")
110
+ if datetime.now() < snooze_until:
111
+ return # In snooze mode, skip alert
112
+ except ValueError:
113
+ pass
114
+
115
+ profile = get_profile()
116
+ if not profile:
117
+ return
118
+
119
+ now = datetime.now()
120
+ current_time_str = now.strftime("%H:%M")
121
+
122
+ # Check if we are inside waking hours
123
+ if not is_waking_hours(current_time_str, profile['wake_time'], profile['sleep_time']):
124
+ return # Quiet hours, do not disturb
125
+
126
+ today_logs = get_today_logs()
127
+
128
+ # Calculate target and check if completed
129
+ target = profile['custom_target'] if profile['custom_target'] else calculate_recommended_intake(profile['weight'], profile['height'])
130
+ total_today = sum(log['amount'] for log in today_logs)
131
+ if total_today >= target:
132
+ return # Target already hit, don't nag the user
133
+
134
+ # Get last drink log timestamp
135
+ if today_logs:
136
+ last_drink_time = datetime.strptime(today_logs[-1]['timestamp'], "%Y-%m-%d %H:%M:%S")
137
+ else:
138
+ # If no drinks today, baseline is today at wake_time
139
+ wake_h, wake_m = map(int, profile['wake_time'].split(':'))
140
+ last_drink_time = now.replace(hour=wake_h, minute=wake_m, second=0, microsecond=0)
141
+ # If wake_time was technically in the future for a night shift, adjust back 1 day
142
+ if last_drink_time > now:
143
+ last_drink_time -= timedelta(days=1)
144
+
145
+ # Get last notified time
146
+ last_notified_str = get_daemon_state("last_notified", "")
147
+ if last_notified_str:
148
+ try:
149
+ last_notified_time = datetime.strptime(last_notified_str, "%Y-%m-%d %H:%M:%S")
150
+ except ValueError:
151
+ last_notified_time = datetime.min
152
+ else:
153
+ last_notified_time = datetime.min
154
+
155
+ # Determine the timing constraint: must be > interval since last drink AND last notification
156
+ interval_mins = profile['reminder_interval']
157
+ next_due_after_drink = last_drink_time + timedelta(minutes=interval_mins)
158
+ next_due_after_notif = last_notified_time + timedelta(minutes=interval_mins)
159
+
160
+ due_time = max(next_due_after_drink, next_due_after_notif)
161
+
162
+ if now >= due_time:
163
+ # Trigger notification!
164
+ title = "Drunken Chunk [~]"
165
+ message = random.choice(REMINDER_MESSAGES)
166
+ send_notification(title, message)
167
+
168
+ # Save state
169
+ set_daemon_state("last_notified", now.strftime("%Y-%m-%d %H:%M:%S"))
170
+
171
+ def is_pid_running(pid):
172
+ """Check to see if a process ID is still running (Windows & Unix native)."""
173
+ if not pid:
174
+ return False
175
+ try:
176
+ pid_val = int(pid)
177
+ if os.name != 'nt':
178
+ try:
179
+ os.kill(pid_val, 0)
180
+ return True
181
+ except ProcessLookupError:
182
+ return False
183
+ except PermissionError:
184
+ return True
185
+ except OSError as e:
186
+ import errno
187
+ return e.errno == errno.EPERM
188
+
189
+ import ctypes
190
+ PROCESS_QUERY_INFORMATION = 0x0400
191
+ PROCESS_SYNCHRONIZE = 0x00100000
192
+ handle = ctypes.windll.kernel32.OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_SYNCHRONIZE, False, pid_val)
193
+ if handle:
194
+ exit_code = ctypes.c_ulong()
195
+ ctypes.windll.kernel32.GetExitCodeProcess(handle, ctypes.byref(exit_code))
196
+ ctypes.windll.kernel32.CloseHandle(handle)
197
+ return exit_code.value == 259 # STILL_ACTIVE
198
+ return False
199
+ except Exception:
200
+ return False
201
+
202
+ def run_daemon_loop():
203
+ """Runs the main daemon polling loop."""
204
+ init_db()
205
+ set_daemon_state("is_active", "1")
206
+ set_daemon_state("daemon_pid", str(os.getpid()))
207
+
208
+ # Send a launch notification
209
+ send_notification("Drunken Chunk [~]", "Water reminder service started. Happy hydrating!")
210
+
211
+ try:
212
+ while True:
213
+ # Check if we were told to stop
214
+ is_active = get_daemon_state("is_active", "0")
215
+ if is_active != "1":
216
+ break
217
+
218
+ check_and_notify()
219
+ time.sleep(20) # Poll every 20 seconds
220
+ finally:
221
+ set_daemon_state("is_active", "0")
222
+ set_daemon_state("daemon_pid", "")
223
+
224
+ def start_background_daemon():
225
+ """Spawns the daemon process detached from the current console."""
226
+ pid = get_daemon_state("daemon_pid", "")
227
+ if pid and is_pid_running(pid):
228
+ return False, f"Daemon is already running with PID {pid}."
229
+
230
+ set_daemon_state("is_active", "1")
231
+
232
+ # Spawn background daemon
233
+ cmd = [sys.executable, "-c", f"import sys; sys.path.insert(0, r'{PACKAGE_PARENT}'); import drunken_chunk.reminder as r; r.run_daemon_loop()"]
234
+
235
+ popen_kwargs = {
236
+ "close_fds": True,
237
+ "stdout": subprocess.DEVNULL,
238
+ "stderr": subprocess.DEVNULL,
239
+ "stdin": subprocess.DEVNULL,
240
+ }
241
+ if os.name == 'nt':
242
+ # DETACHED_PROCESS | CREATE_NO_WINDOW
243
+ popen_kwargs["creationflags"] = 0x00000008 | 0x08000000
244
+ else:
245
+ popen_kwargs["start_new_session"] = True
246
+
247
+ subprocess.Popen(cmd, **popen_kwargs)
248
+
249
+ # Give it a second to boot up and write PID
250
+ time.sleep(1.5)
251
+ new_pid = get_daemon_state("daemon_pid", "")
252
+
253
+ return True, f"Daemon successfully started in background (PID: {new_pid})."
254
+
255
+ def stop_background_daemon():
256
+ """Stops the daemon gracefully by modifying database state."""
257
+ pid = get_daemon_state("daemon_pid", "")
258
+ set_daemon_state("is_active", "0")
259
+
260
+ if not pid:
261
+ return False, "Daemon is not running."
262
+
263
+ # Wait for graceful exit
264
+ for _ in range(5):
265
+ if not is_pid_running(pid):
266
+ set_daemon_state("daemon_pid", "")
267
+ return True, "Daemon stopped gracefully."
268
+
269
+ time.sleep(0.5)
270
+
271
+ # Force kill if it didn't exit
272
+ try:
273
+ if os.name == 'nt':
274
+ subprocess.run(["taskkill", "/F", "/PID", pid], capture_output=True, creationflags=0x08000000)
275
+ else:
276
+ os.kill(int(pid), 9)
277
+ set_daemon_state("daemon_pid", "")
278
+ return True, f"Daemon process {pid} was forced to stop."
279
+ except Exception as e:
280
+ return False, f"Failed to stop daemon {pid}: {str(e)}"
281
+
282
+ def manage_autostart(action):
283
+ """
284
+ Manages startup autostart toggle/enable/disable/status for Windows, macOS, and Linux.
285
+ action: 'status', 'enable', 'disable', or 'toggle'
286
+ Returns: (success_bool, message_str)
287
+ """
288
+ platform = sys.platform
289
+
290
+ # Identify target file and startup path
291
+ if platform == 'win32':
292
+ appdata = os.environ.get("APPDATA")
293
+ if not appdata:
294
+ return False, "Could not locate APPDATA directory."
295
+ startup_dir = os.path.join(appdata, r"Microsoft\Windows\Start Menu\Programs\Startup")
296
+ startup_file = os.path.join(startup_dir, "drunken-chunk-autostart.bat")
297
+ elif platform == 'darwin':
298
+ startup_dir = os.path.expanduser("~/Library/LaunchAgents")
299
+ startup_file = os.path.join(startup_dir, "com.drunkenchunk.reminder.plist")
300
+ else: # Assume Linux / generic Unix
301
+ startup_dir = os.path.expanduser("~/.config/autostart")
302
+ startup_file = os.path.join(startup_dir, "drunken-chunk.desktop")
303
+
304
+ exists = os.path.exists(startup_file)
305
+
306
+ if action == 'status':
307
+ status_str = "ENABLED" if exists else "DISABLED"
308
+ return True, f"Startup Autostart is currently: {status_str}"
309
+
310
+ if action == 'toggle':
311
+ action = 'disable' if exists else 'enable'
312
+
313
+ if action == 'enable':
314
+ try:
315
+ os.makedirs(startup_dir, exist_ok=True)
316
+
317
+ if platform == 'win32':
318
+ python_exe = sys.executable
319
+ cmd_str = f'@"{python_exe}" -c "import sys; sys.path.insert(0, r\'{PACKAGE_PARENT}\'); import drunken_chunk.reminder as r; r.start_background_daemon()"\n'
320
+ with open(startup_file, "w", encoding="utf-8") as f:
321
+ f.write(cmd_str)
322
+ elif platform == 'darwin':
323
+ plist_content = f"""<?xml version="1.0" encoding="UTF-8"?>
324
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
325
+ <plist version="1.0">
326
+ <dict>
327
+ <key>Label</key>
328
+ <string>com.drunkenchunk.reminder</string>
329
+ <key>ProgramArguments</key>
330
+ <array>
331
+ <string>{sys.executable}</string>
332
+ <string>-c</string>
333
+ <string>import sys; sys.path.insert(0, '{PACKAGE_PARENT}'); import drunken_chunk.reminder as r; r.start_background_daemon()</string>
334
+ </array>
335
+ <key>RunAtLoad</key>
336
+ <true/>
337
+ <key>KeepAlive</key>
338
+ <false/>
339
+ </dict>
340
+ </plist>
341
+ """
342
+ with open(startup_file, "w", encoding="utf-8") as f:
343
+ f.write(plist_content)
344
+ # Load the launch agent immediately
345
+ subprocess.run(["launchctl", "load", startup_file], capture_output=True)
346
+ else: # Linux
347
+ desktop_content = f"""[Desktop Entry]
348
+ Type=Application
349
+ Name=Drunken Chunk Reminder
350
+ Comment=Starts the Drunken Chunk background reminder daemon
351
+ Exec={sys.executable} -c "import sys; sys.path.insert(0, '{PACKAGE_PARENT}'); import drunken_chunk.reminder as r; r.start_background_daemon()"
352
+ Terminal=false
353
+ X-GNOME-Autostart-enabled=true
354
+ """
355
+ with open(startup_file, "w", encoding="utf-8") as f:
356
+ f.write(desktop_content)
357
+
358
+ # Automatically start the background reminder service immediately
359
+ daemon_started, daemon_msg = start_background_daemon()
360
+ msg = "Autostart ENABLED. Drunken Chunk will now launch on system startup."
361
+ if daemon_started:
362
+ msg += f"\nReminder service started automatically in background (PID: {get_daemon_state('daemon_pid', '')})."
363
+ else:
364
+ msg += "\nReminder service is already active."
365
+ return True, msg
366
+ except Exception as e:
367
+ return False, f"Failed to enable autostart: {str(e)}"
368
+
369
+ elif action == 'disable':
370
+ if exists:
371
+ try:
372
+ if platform == 'darwin':
373
+ # Unload the launch agent
374
+ subprocess.run(["launchctl", "unload", startup_file], capture_output=True)
375
+ os.remove(startup_file)
376
+ return True, "Autostart DISABLED. Drunken Chunk will no longer launch on system startup."
377
+ except Exception as e:
378
+ return False, f"Failed to disable autostart: {str(e)}"
379
+ else:
380
+ return True, "Autostart was already disabled."
381
+
382
+ return False, f"Unknown action: {action}"
383
+
384
+ if __name__ == "__main__":
385
+ # If run directly as a script
386
+ run_daemon_loop()
@@ -0,0 +1,43 @@
1
+ import re
2
+ from .db import save_shortcut, delete_shortcut, get_shortcuts
3
+
4
+ def validate_shortcut_name(name):
5
+ """
6
+ Validates that a shortcut name is alphanumeric and not a pure integer.
7
+ Pure integers are reserved for raw logging of milliliters (e.g., 'log 250').
8
+ """
9
+ name = name.strip().lower()
10
+ if not name:
11
+ return False, "Shortcut name cannot be empty."
12
+ if not re.match(r"^[a-zA-Z0-9_\-]+$", name):
13
+ return False, "Shortcut name can only contain letters, numbers, underscores, and hyphens."
14
+ if name.isdigit():
15
+ return False, "Shortcut name cannot be a number. Numbers are reserved for logging raw milliliters."
16
+ return True, ""
17
+
18
+ def add_custom_shortcut(name, amount):
19
+ """Registers or updates a shortcut if validation passes."""
20
+ name = name.strip().lower()
21
+ is_valid, err_msg = validate_shortcut_name(name)
22
+ if not is_valid:
23
+ return False, err_msg
24
+
25
+ try:
26
+ amount_val = int(amount)
27
+ if amount_val <= 0:
28
+ return False, "Amount must be a positive integer."
29
+ except ValueError:
30
+ return False, "Amount must be a valid integer number (in ml)."
31
+
32
+ save_shortcut(name, amount_val)
33
+ return True, f"Vessel size locked and loaded. Shortcut '{name}' registered with {amount_val} ml. Logging is now 10x faster."
34
+
35
+ def remove_custom_shortcut(name):
36
+ """Removes a shortcut if it exists."""
37
+ name = name.strip().lower()
38
+ shortcuts = get_shortcuts()
39
+ if name not in shortcuts:
40
+ return False, f"Shortcut '{name}' does not exist."
41
+
42
+ delete_shortcut(name)
43
+ return True, f"Shortcut '{name}' has been destroyed. May it rest in plastic."
@@ -0,0 +1,122 @@
1
+ import os
2
+ import sys
3
+
4
+ # Enable Virtual Terminal Processing for Windows to support ANSI color sequences natively
5
+ def enable_ansi_support():
6
+ if os.name == 'nt':
7
+ import ctypes
8
+ kernel32 = ctypes.windll.kernel32
9
+ # -11 is STD_OUTPUT_HANDLE
10
+ h_out = kernel32.GetStdHandle(-11)
11
+ if h_out != -1:
12
+ mode = ctypes.c_ulong()
13
+ if kernel32.GetConsoleMode(h_out, ctypes.byref(mode)):
14
+ # 0x0004 is ENABLE_VIRTUAL_TERMINAL_PROCESSING
15
+ kernel32.SetConsoleMode(h_out, mode.value | 0x0004)
16
+
17
+ # Call on import to ensure colors are enabled
18
+ enable_ansi_support()
19
+
20
+ # ANSI Color Codes
21
+ class Colors:
22
+ BLUE = "\033[38;5;39m"
23
+ CYAN = "\033[38;5;81m"
24
+ GREEN = "\033[38;5;76m"
25
+ RED = "\033[38;5;196m"
26
+ YELLOW = "\033[38;5;220m"
27
+ MAGENTA = "\033[38;5;170m"
28
+ GRAY = "\033[38;5;244m"
29
+ WHITE = "\033[97m"
30
+
31
+ # Backgrounds
32
+ BG_BLUE = "\033[48;5;27m"
33
+ BG_CYAN = "\033[48;5;39m"
34
+
35
+ # Styles
36
+ BOLD = "\033[1m"
37
+ UNDERLINE = "\033[4m"
38
+ RESET = "\033[0m"
39
+
40
+ # Styling Helper Functions
41
+ def colorize(text, color):
42
+ return f"{color}{text}{Colors.RESET}"
43
+
44
+ def bold(text):
45
+ return f"{Colors.BOLD}{text}{Colors.RESET}"
46
+
47
+ def italic(text):
48
+ return f"\033[3m{text}{Colors.RESET}"
49
+
50
+ def print_banner():
51
+ banner = f"""
52
+ {Colors.BLUE}┌────────────────────────────────────────────────────────┐{Colors.RESET}
53
+ {Colors.BLUE}│{Colors.RESET} {Colors.CYAN}{Colors.BOLD}[ DRUNKEN CHUNK ]{Colors.RESET} {Colors.BLUE}│{Colors.RESET}
54
+ {Colors.BLUE}│{Colors.RESET} {Colors.GRAY}The ultimate CLI water tracker, reminders & analytics{Colors.RESET} {Colors.BLUE}│{Colors.RESET}
55
+ {Colors.BLUE}└────────────────────────────────────────────────────────┘{Colors.RESET}
56
+ """
57
+ print(banner)
58
+
59
+ def get_progress_bar(percentage, width=30):
60
+ """Generates a colored ASCII progress bar."""
61
+ percentage = min(max(percentage, 0.0), 100.0)
62
+ filled_length = int(width * percentage // 100)
63
+ empty_length = width - filled_length
64
+
65
+ # Determine color based on completion
66
+ if percentage < 25:
67
+ bar_color = Colors.RED
68
+ elif percentage < 75:
69
+ bar_color = Colors.YELLOW
70
+ elif percentage < 100:
71
+ bar_color = Colors.CYAN
72
+ else:
73
+ bar_color = Colors.GREEN
74
+
75
+ bar = colorize("█" * filled_length, bar_color) + colorize("░" * empty_length, Colors.GRAY)
76
+ return f"[{bar}] {colorize(f'{percentage:.1f}%', Colors.BOLD)}"
77
+
78
+ def print_table(headers, rows, title=None):
79
+ """Draws a beautiful custom ASCII table."""
80
+ if not rows:
81
+ print(colorize("No data to display in table.", Colors.GRAY))
82
+ return
83
+
84
+ # Calculate column widths
85
+ col_widths = [len(str(h)) for h in headers]
86
+ for row in rows:
87
+ for idx, cell in enumerate(row):
88
+ col_widths[idx] = max(col_widths[idx], len(str(cell)))
89
+
90
+ # Generate border formats
91
+ top_border = "┌─" + "─┬─".join("─" * w for w in col_widths) + "─┐"
92
+ header_sep = "├─" + "─┼─".join("─" * w for w in col_widths) + "─┤"
93
+ bottom_border = "└─" + "─┴─".join("─" * w for w in col_widths) + "─┘"
94
+
95
+ # Display Title
96
+ if title:
97
+ print(colorize(f"\n─── {title} ───", Colors.BOLD + Colors.CYAN))
98
+ else:
99
+ print()
100
+
101
+ # Print Top Border
102
+ print(colorize(top_border, Colors.BLUE))
103
+
104
+ # Print Headers
105
+ header_cells = [str(headers[i]).ljust(col_widths[i]) for i in range(len(headers))]
106
+ print(colorize("│ ", Colors.BLUE) + colorize(" │ ", Colors.BLUE).join(colorize(c, Colors.BOLD + Colors.WHITE) for c in header_cells) + colorize(" │", Colors.BLUE))
107
+
108
+ # Print Header Separator
109
+ print(colorize(header_sep, Colors.BLUE))
110
+
111
+ # Print Rows
112
+ for row in rows:
113
+ row_cells = []
114
+ for i, cell in enumerate(row):
115
+ cell_str = str(cell)
116
+ # Try to colorize numbers/statuses if needed or just display
117
+ row_cells.append(cell_str.ljust(col_widths[i]))
118
+ print(colorize("│ ", Colors.BLUE) + colorize(" │ ", Colors.BLUE).join(row_cells) + colorize(" │", Colors.BLUE))
119
+
120
+ # Print Bottom Border
121
+ print(colorize(bottom_border, Colors.BLUE))
122
+ print()
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "drunken-chunk",
3
+ "version": "1.0.0",
4
+ "description": "A beautiful, premium CLI water tracker, reminders & behavior analytics tool with cross-platform native desktop notifications.",
5
+ "main": "bin/index.js",
6
+ "bin": {
7
+ "drunken-chunk": "bin/index.js"
8
+ },
9
+ "engines": {
10
+ "node": ">=14.0.0"
11
+ },
12
+ "keywords": [
13
+ "water",
14
+ "tracker",
15
+ "cli",
16
+ "hydrator",
17
+ "hydration",
18
+ "health",
19
+ "wellness",
20
+ "reminders",
21
+ "analytics"
22
+ ],
23
+ "author": "Antigravity Team",
24
+ "license": "MIT",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/vikashpatel04/drunken-chunk.git"
28
+ },
29
+ "bugs": {
30
+ "url": "https://github.com/vikashpatel04/drunken-chunk/issues"
31
+ },
32
+ "homepage": "https://github.com/vikashpatel04/drunken-chunk#readme"
33
+ }