crystalwindow 4.4__tar.gz → 4.6__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.
Files changed (49) hide show
  1. {crystalwindow-4.4/crystalwindow.egg-info → crystalwindow-4.6}/PKG-INFO +1 -1
  2. {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/__init__.py +17 -4
  3. crystalwindow-4.6/crystalwindow/ai.py +314 -0
  4. crystalwindow-4.6/crystalwindow/clock.py +189 -0
  5. crystalwindow-4.6/crystalwindow/cw_client.py +95 -0
  6. crystalwindow-4.6/crystalwindow/websearch.py +233 -0
  7. {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/window.py +6 -1
  8. {crystalwindow-4.4 → crystalwindow-4.6/crystalwindow.egg-info}/PKG-INFO +1 -1
  9. {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow.egg-info/SOURCES.txt +2 -0
  10. {crystalwindow-4.4 → crystalwindow-4.6}/setup.py +1 -1
  11. crystalwindow-4.4/crystalwindow/ai.py +0 -366
  12. crystalwindow-4.4/crystalwindow/clock.py +0 -23
  13. {crystalwindow-4.4 → crystalwindow-4.6}/LICENSE +0 -0
  14. {crystalwindow-4.4 → crystalwindow-4.6}/MANIFEST.in +0 -0
  15. {crystalwindow-4.4 → crystalwindow-4.6}/README.md +0 -0
  16. {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/FileHelper.py +0 -0
  17. {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/Icons/default_icon.png +0 -0
  18. {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/animation.py +0 -0
  19. {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/assets.py +0 -0
  20. {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/camera.py +0 -0
  21. {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/collision.py +0 -0
  22. {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/color_handler.py +0 -0
  23. {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/crystal3d.py +0 -0
  24. {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/docs/getting_started.md +0 -0
  25. {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/docs/index.md +0 -0
  26. {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/draw_helpers.py +0 -0
  27. {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/draw_rects.py +0 -0
  28. {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/draw_text_helper.py +0 -0
  29. {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/draw_tool.py +0 -0
  30. {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/fun_helpers.py +0 -0
  31. {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/gametests/3dsquare.py +0 -0
  32. {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/gametests/__init__.py +0 -0
  33. {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/gametests/__main__.py +0 -0
  34. {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/gametests/gravitytest.py +0 -0
  35. {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/gametests/guitesting.py +0 -0
  36. {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/gametests/sandbox.py +0 -0
  37. {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/gametests/squaremove.py +0 -0
  38. {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/gametests/windowtesting.py +0 -0
  39. {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/gravity.py +0 -0
  40. {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/gui.py +0 -0
  41. {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/gui_ext.py +0 -0
  42. {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/math.py +0 -0
  43. {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/player.py +0 -0
  44. {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/sprites.py +0 -0
  45. {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/tilemap.py +0 -0
  46. {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/ver_warner.py +0 -0
  47. {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow.egg-info/dependency_links.txt +0 -0
  48. {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow.egg-info/top_level.txt +0 -0
  49. {crystalwindow-4.4 → crystalwindow-4.6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: crystalwindow
3
- Version: 4.4
3
+ Version: 4.6
4
4
  Summary: A Tkinter powered window + GUI toolkit made by Crystal (MEEEEEE)! Easier apps, smoother UI and all-in-one helpers!
5
5
  Home-page: https://pypi.org/project/crystalwindow/
6
6
  Author: CrystalBallyHereXD
@@ -32,8 +32,9 @@ from .collision import check_collision, resolve_collision
32
32
  from .gui import Button, Label, GUIManager, hex_to_rgb, Fade
33
33
  from .gui_ext import Toggle, Slider
34
34
 
35
- # === Time ===
36
- from .clock import Clock
35
+ # === Time System ===
36
+ # Includes upgraded Clock + all helper utilities
37
+ from .clock import Clock, Stopwatch, CountdownTimer, Scheduler
37
38
 
38
39
  # === Drawing Helpers ===
39
40
  from .draw_helpers import gradient_rect, CameraShake
@@ -45,6 +46,7 @@ from .draw_tool import CrystalDraw
45
46
  from .fun_helpers import random_name, DebugOverlay, random_color, random_palette, lerp
46
47
  from .camera import Camera
47
48
  from .color_handler import Colors, Color
49
+ from .websearch import WebSearchResult, WebSearch
48
50
 
49
51
  # === 3D Engine ===
50
52
  from .crystal3d import CW3D
@@ -52,6 +54,9 @@ from .crystal3d import CW3D
52
54
  # === AI Engine ===
53
55
  from .ai import AI
54
56
 
57
+ # === Server Manager ===
58
+ from .cw_client import CWClient
59
+
55
60
  __all__ = [
56
61
  # --- Core ---
57
62
  "Window", "Sprite", "TileMap", "Player", "Gravity", "FileHelper", "Mathematics",
@@ -77,18 +82,26 @@ __all__ = [
77
82
  # --- GUI Extensions ---
78
83
  "Toggle", "Slider",
79
84
 
80
- # --- Time ---
85
+ # --- Time System ---
86
+ # FULL time system is now exposed
81
87
  "Clock",
88
+ "Stopwatch",
89
+ "CountdownTimer",
90
+ "Scheduler",
82
91
 
83
92
  # --- Drawing ---
84
93
  "gradient_rect", "CameraShake", "DrawHelper", "DrawTextManager", "CrystalDraw",
85
94
 
86
95
  # --- Misc ---
87
- "random_name", "DebugOverlay", "Camera", "Colors", "Color", "random_palette", "lerp"
96
+ "random_name", "DebugOverlay", "Camera", "Colors", "Color",
97
+ "random_palette", "lerp", "WebSearchResult", "WebSearch",
88
98
 
89
99
  # --- 3D ---
90
100
  "CW3D",
91
101
 
92
102
  # --- AI ---
93
103
  "AI",
104
+
105
+ # --- Server Client ---
106
+ "CWClient",
94
107
  ]
@@ -0,0 +1,314 @@
1
+ # ==========================================================
2
+ # CrystalAI v0.7 — Stabilized & Corrected Engine
3
+ # ----------------------------------------------------------
4
+
5
+ import os
6
+ import ast
7
+ import difflib
8
+ import requests
9
+ import json
10
+ from typing import Optional, Dict, Any
11
+
12
+
13
+ # ==========================================================
14
+ # Response Wrapper
15
+ # ==========================================================
16
+ class CrystalAIResponse:
17
+ def __init__(self, text: str, meta: Optional[Dict[str, Any]] = None):
18
+ self.text = text
19
+ self.meta = meta or {}
20
+
21
+ def __str__(self):
22
+ return self.text
23
+
24
+
25
+ # ==========================================================
26
+ # MAIN ENGINE
27
+ # ==========================================================
28
+ class AI:
29
+ DEFAULT_MODEL = "llama-3.1-70b-versatile"
30
+ DEFAULT_PERSONALITY = (
31
+ "You are CrystalWindow AI. You help users with Python code, "
32
+ "debugging, error analysis, documentation, and file analysis. "
33
+ "Be friendly, technical, clear, and precise."
34
+ )
35
+
36
+ PLACEHOLDER_KEY = "NO_KEY_PROVIDED"
37
+
38
+ # ------------------------------------------------------
39
+ def __init__(self, key=None, model=None):
40
+
41
+ # --- KEY VALIDATION ---
42
+ # If no key passed try common environment variables (makes key optional)
43
+ if not key:
44
+ key = (
45
+ os.environ.get("CRYSTALAI_API_KEY")
46
+ or os.environ.get("GROQ_API_KEY")
47
+ or os.environ.get("OPENAI_API_KEY")
48
+ or os.environ.get("API_KEY")
49
+ )
50
+ if key:
51
+ key = str(key).strip()
52
+
53
+ if not key or len(str(key).strip()) == 0:
54
+ print("[CrystalAI] No API key provided → using offline mode.")
55
+ self.key = None # forces offline fallback
56
+ self.force_local = True
57
+ else:
58
+ self.key = key
59
+ self.force_local = False
60
+
61
+ # --- MODEL VALIDATION ---
62
+ # If model is omitted (None) use default silently; only warn if an invalid model is explicitly passed
63
+ if model is None:
64
+ self.model = self.DEFAULT_MODEL
65
+ elif not isinstance(model, str) or len(model) < 3:
66
+ print("[CrystalAI] Unknown model → using default.")
67
+ self.model = self.DEFAULT_MODEL
68
+ else:
69
+ self.model = model
70
+
71
+ self.personality = self.DEFAULT_PERSONALITY
72
+ self.memory = []
73
+ self.use_memory = True
74
+ self.library_context = ""
75
+
76
+ # ==========================================================
77
+ # PERSONALITY
78
+ # ==========================================================
79
+ def set_personality(self, txt):
80
+ if not isinstance(txt, str) or len(txt.strip()) < 10:
81
+ print("[CrystalAI] Personality too short → reverting to default.")
82
+ self.personality = self.DEFAULT_PERSONALITY
83
+ return
84
+
85
+ if len(txt) > 3000:
86
+ print("[CrystalAI] Personality too long → using default.")
87
+ self.personality = self.DEFAULT_PERSONALITY
88
+ return
89
+
90
+ self.personality = txt.strip()
91
+
92
+ # ==========================================================
93
+ # LIBRARY INGESTION
94
+ # ==========================================================
95
+ def index_library(self, folder):
96
+ """
97
+ Load all Python files as context for smarter answers.
98
+ """
99
+ if not os.path.exists(folder):
100
+ print("[CrystalAI] Library folder not found.")
101
+ return
102
+
103
+ collected = []
104
+ for root, _, files in os.walk(folder):
105
+ for f in files:
106
+ if f.endswith(".py"):
107
+ try:
108
+ p = os.path.join(root, f)
109
+ with open(p, "r", encoding="utf8") as fp:
110
+ collected.append(
111
+ f"# FILE: {p}\n{fp.read()}\n\n"
112
+ )
113
+ except:
114
+ pass
115
+
116
+ self.library_context = "\n".join(collected)[:120_000] # trimmed
117
+
118
+ # ==========================================================
119
+ # FILE READER
120
+ # ==========================================================
121
+ def _read_file(self, path):
122
+ if not path:
123
+ return None
124
+ if not os.path.exists(path):
125
+ return f"[CrystalAI] file not found: {path}"
126
+ try:
127
+ with open(path, "r", encoding="utf8") as f:
128
+ return f.read()
129
+ except:
130
+ return "[CrystalAI] could not read file."
131
+
132
+ # ==========================================================
133
+ # PROMPT BUILDER
134
+ # ==========================================================
135
+ def _build_prompt(self, user_text, file_data):
136
+ final = (
137
+ f"[SYSTEM]\n{self.personality}\n\n"
138
+ f"[USER]\n{user_text}\n\n"
139
+ )
140
+
141
+ if self.use_memory and self.memory:
142
+ final += "[MEMORY]\n"
143
+ for m in self.memory[-6:]:
144
+ final += f"User: {m['user']}\nAI: {m['ai']}\n"
145
+ final += "\n"
146
+
147
+ if self.library_context:
148
+ final += f"[LIBRARY]\n{self.library_context}\n\n"
149
+
150
+ if file_data and not file_data.startswith("[CrystalAI]"):
151
+ final += f"[FILE]\n{file_data}\n\n"
152
+
153
+ return final[:190_000] # safety limit
154
+
155
+ def _save_memory(self, user, ai):
156
+ self.memory.append({"user": user, "ai": ai})
157
+ if len(self.memory) > 60:
158
+ self.memory.pop(0)
159
+
160
+ # ==========================================================
161
+ # LOCAL FALLBACK AI
162
+ # ==========================================================
163
+ def _local_ai(self, user_text, file_data):
164
+ """
165
+ Much cleaner, more reliable fallback engine.
166
+ """
167
+
168
+ # --- AST SECTION ---
169
+ if file_data and not file_data.startswith("[CrystalAI]"):
170
+ try:
171
+ ast.parse(file_data)
172
+ return (
173
+ "[LocalAI] File parsed successfully — no syntax errors.\n"
174
+ "Ask for refactoring, explanation, or improvements."
175
+ )
176
+ except SyntaxError as se:
177
+ snippet = self._snippet(file_data, se.lineno)
178
+ return (
179
+ "[LocalAI] SyntaxError found:\n"
180
+ f"• {se.msg}\n"
181
+ f"• Line {se.lineno}\n\n"
182
+ f"{snippet}"
183
+ )
184
+
185
+ # --- GENERIC HELP ---
186
+ lower = user_text.lower()
187
+ if "python" in lower or "fix" in lower or "error" in lower:
188
+ return (
189
+ "[LocalAI] Offline mode: I can help with general Python logic.\n"
190
+ "If you provide a file, I can analyze it using AST."
191
+ )
192
+
193
+ return "[LocalAI] Offline mode active — limited responses available."
194
+
195
+ # ==========================================================
196
+ # MAIN "ASK" FUNCTION
197
+ # ==========================================================
198
+ def ask(self, text, file=None):
199
+ file_data = self._read_file(file)
200
+ prompt = self._build_prompt(text, file_data)
201
+
202
+ # If no API key → offline only
203
+ if self.force_local:
204
+ resp = self._local_ai(text, file_data)
205
+ self._save_memory(text, resp)
206
+ return CrystalAIResponse(resp)
207
+
208
+ # Online mode
209
+ try:
210
+ url = "https://api.groq.com/openai/v1/chat/completions"
211
+ headers = {
212
+ "Authorization": f"Bearer {self.key}",
213
+ "Content-Type": "application/json"
214
+ }
215
+
216
+ payload = {
217
+ "model": self.model,
218
+ "messages": [
219
+ {"role": "system", "content": self.personality},
220
+ {"role": "user", "content": prompt}
221
+ ],
222
+ "temperature": 0.3
223
+ }
224
+
225
+ r = requests.post(url, json=payload, headers=headers, timeout=8)
226
+ data = r.json()
227
+
228
+ if "error" in data:
229
+ raise RuntimeError(data["error"])
230
+
231
+ resp = data["choices"][0]["message"]["content"]
232
+
233
+ except Exception:
234
+ resp = self._local_ai(text, file_data)
235
+
236
+ self._save_memory(text, resp)
237
+ return CrystalAIResponse(resp)
238
+
239
+ # ==========================================================
240
+ # TERMINAL HELPER
241
+ # ==========================================================
242
+ def ask_t(self, text, file=None):
243
+ return self.ask(f"[TERMINAL] {text}", file)
244
+
245
+ # ==========================================================
246
+ # AUTO FIX CODE
247
+ # ==========================================================
248
+ def fix_code(self, file_path):
249
+ orig = self._read_file(file_path)
250
+
251
+ if not orig or orig.startswith("[CrystalAI]"):
252
+ return CrystalAIResponse(orig or "[CrystalAI] file missing")
253
+
254
+ try:
255
+ ast.parse(orig)
256
+ return CrystalAIResponse("[AI] No syntax errors found.")
257
+ except SyntaxError as se:
258
+ fixed, notes = self._simple_fix(orig, se)
259
+ diff = self._make_diff(orig, fixed)
260
+ msg = "[AI] Auto-fix result:\n" + "\n".join(notes) + "\n\n" + diff
261
+ return CrystalAIResponse(msg, {"diff": diff, "notes": notes})
262
+
263
+ # ==========================================================
264
+ # SIMPLE AUTO-FIX ENGINE
265
+ # ==========================================================
266
+ def _simple_fix(self, src, err):
267
+ notes = []
268
+ lines = src.splitlines()
269
+ msg = getattr(err, "msg", "")
270
+ lineno = err.lineno or 0
271
+
272
+ if "expected" in msg and ":" in msg:
273
+ if 1 <= lineno <= len(lines):
274
+ l = lines[lineno - 1].rstrip()
275
+ if not l.endswith(":"):
276
+ lines[lineno - 1] = l + ":"
277
+ notes.append("[fix] added missing ':'")
278
+ candidate = "\n".join(lines)
279
+ try:
280
+ ast.parse(candidate)
281
+ return candidate, notes
282
+ except:
283
+ pass
284
+
285
+ notes.append("[info] auto-fix could not fix the error")
286
+ return src, notes
287
+
288
+ # ==========================================================
289
+ # DIFF HELPER
290
+ # ==========================================================
291
+ def _make_diff(self, old, new):
292
+ return "\n".join(
293
+ difflib.unified_diff(
294
+ old.splitlines(), new.splitlines(),
295
+ fromfile="old", tofile="new", lineterm=""
296
+ )
297
+ )
298
+
299
+ # ==========================================================
300
+ # SNIPPET HELPER
301
+ # ==========================================================
302
+ def _snippet(self, src, lineno, ctx=2):
303
+ lines = src.splitlines()
304
+ start = max(0, lineno - ctx - 1)
305
+ end = min(len(lines), lineno + ctx)
306
+ out = []
307
+ for i in range(start, end):
308
+ mark = "->" if (i + 1) == lineno else " "
309
+ out.append(f"{mark} {i+1:4}: {lines[i]}")
310
+ return "\n".join(out)
311
+
312
+ # ==========================================================
313
+ # END OF ENGINE
314
+ # ==========================================================
@@ -0,0 +1,189 @@
1
+ import time
2
+ from datetime import datetime
3
+ from collections import deque
4
+
5
+
6
+ # ============================================================
7
+ # Stopwatch Helper
8
+ # ============================================================
9
+ class Stopwatch:
10
+ def __init__(self):
11
+ self.start_time = None
12
+ self.elapsed_time = 0.0
13
+ self.running = False
14
+
15
+ def start(self):
16
+ if not self.running:
17
+ self.start_time = time.perf_counter()
18
+ self.running = True
19
+
20
+ def stop(self):
21
+ if self.running:
22
+ self.elapsed_time += time.perf_counter() - self.start_time
23
+ self.running = False
24
+
25
+ def reset(self):
26
+ self.start_time = None
27
+ self.elapsed_time = 0.0
28
+ self.running = False
29
+
30
+ def elapsed(self):
31
+ if self.running:
32
+ return self.elapsed_time + (time.perf_counter() - self.start_time)
33
+ return self.elapsed_time
34
+
35
+
36
+ # ============================================================
37
+ # Countdown Timer Helper
38
+ # ============================================================
39
+ class CountdownTimer:
40
+ def __init__(self):
41
+ self.target_time = None
42
+
43
+ def start(self, seconds: float):
44
+ self.target_time = time.perf_counter() + seconds
45
+
46
+ def remaining(self):
47
+ if not self.target_time:
48
+ return 0
49
+ return max(0, self.target_time - time.perf_counter())
50
+
51
+ def done(self):
52
+ return self.remaining() == 0
53
+
54
+
55
+ # ============================================================
56
+ # Event Scheduler Helper
57
+ # ============================================================
58
+ class Scheduler:
59
+ def __init__(self):
60
+ self.events = [] # list of (interval, last_run, function)
61
+
62
+ def schedule(self, interval: float, func):
63
+ """Run function every `interval` seconds."""
64
+ self.events.append([interval, time.perf_counter(), func])
65
+
66
+ def run_pending(self):
67
+ now = time.perf_counter()
68
+ for event in self.events:
69
+ interval, last_run, func = event
70
+ if now - last_run >= interval:
71
+ func()
72
+ event[1] = now # update last_run
73
+
74
+
75
+ # ============================================================
76
+ # MAIN CLOCK
77
+ # ============================================================
78
+ class Clock:
79
+ def __init__(self, target_fps: int = 60, smooth_fps: int = 30):
80
+ self.last = time.perf_counter()
81
+ self.delta = 0.0
82
+
83
+ # FPS
84
+ self.target_fps = target_fps
85
+ self.min_frame_time = 1 / target_fps if target_fps else 0
86
+ self.frame_times = deque(maxlen=smooth_fps)
87
+
88
+ self.paused = False
89
+
90
+ # Utilities
91
+ self.stopwatch = Stopwatch()
92
+ self.timer = CountdownTimer()
93
+ self.scheduler = Scheduler()
94
+
95
+ # ----------------------------------------------------------
96
+ # TICK + FPS
97
+ # ----------------------------------------------------------
98
+ def tick(self, fps: int | None = None):
99
+ if fps:
100
+ self.target_fps = fps
101
+ self.min_frame_time = 1 / fps
102
+
103
+ now = time.perf_counter()
104
+ raw_delta = now - self.last
105
+ self.last = now
106
+
107
+ # Paused = no delta
108
+ if self.paused:
109
+ self.delta = 0.0
110
+ return 0.0
111
+
112
+ # FPS limiting
113
+ sleep_time = self.min_frame_time - raw_delta
114
+ if sleep_time > 0:
115
+ time.sleep(sleep_time)
116
+ now2 = time.perf_counter()
117
+ raw_delta = now2 - (self.last - raw_delta)
118
+
119
+ self.delta = raw_delta
120
+ self.frame_times.append(raw_delta)
121
+
122
+ # Scheduler update
123
+ self.scheduler.run_pending()
124
+
125
+ return self.delta
126
+
127
+ def get_fps(self):
128
+ if not self.frame_times:
129
+ return 0.0
130
+ avg = sum(self.frame_times) / len(self.frame_times)
131
+ return round(1 / avg, 2) if avg > 0 else 0
132
+
133
+ # ----------------------------------------------------------
134
+ # PAUSE / RESUME
135
+ # ----------------------------------------------------------
136
+ def pause(self):
137
+ self.paused = True
138
+
139
+ def resume(self):
140
+ self.paused = False
141
+ self.last = time.perf_counter()
142
+
143
+ # ----------------------------------------------------------
144
+ # TIME + DATE UTILITIES
145
+ # ----------------------------------------------------------
146
+ def time(self, milliseconds: bool = True, hour_12: bool = False):
147
+ now = datetime.now()
148
+
149
+ if hour_12:
150
+ fmt = "%I:%M:%S" + (" %p" if not milliseconds else ".%f %p")
151
+ else:
152
+ fmt = "%H:%M:%S" + (".%f" if milliseconds else "")
153
+
154
+ text = now.strftime(fmt)
155
+ if milliseconds:
156
+ # Trim microseconds to milliseconds
157
+ if hour_12:
158
+ # Example: "03:20:15.123000 PM" -> "03:20:15.123 PM"
159
+ parts = text.split(".")
160
+ ms = parts[1][:3] + parts[1][6:] # keep .mmm, remove "000 PM"
161
+ text = parts[0] + "." + parts[1][:3] + " " + now.strftime("%p")
162
+ else:
163
+ text = text[:-3] # remove last 3 microseconds digits
164
+
165
+ return text
166
+
167
+ def date(self, format: str = "%m/%d/%Y"):
168
+ return datetime.now().strftime(format)
169
+
170
+ def time_date(self, order: str = "time_first", **kwargs):
171
+ t = self.time(**kwargs)
172
+ d = self.date()
173
+ return f"{t} | {d}" if order == "time_first" else f"{d} | {t}"
174
+
175
+ # ----------------------------------------------------------
176
+ # CUSTOM FORMATTERS
177
+ # ----------------------------------------------------------
178
+ def format_time(self, fmt: str):
179
+ return datetime.now().strftime(fmt)
180
+
181
+ def format_date(self, fmt: str):
182
+ return datetime.now().strftime(fmt)
183
+
184
+ # ----------------------------------------------------------
185
+ # MONOTONIC TIMESTAMP
186
+ # ----------------------------------------------------------
187
+ def timestamp(self):
188
+ """High-precision monotonic timestamp."""
189
+ return time.perf_counter()
@@ -0,0 +1,95 @@
1
+ # cw_client.py
2
+ # pip install websockets
3
+ import asyncio
4
+ import websockets
5
+ import json
6
+ import threading
7
+ import time
8
+
9
+ class CWClient:
10
+ """
11
+ Lightweight client wrapper. Usage:
12
+ c = CWClient("MyName", "ws://host:8765")
13
+ c.on_event = lambda ev: print("ev", ev)
14
+ c.start()
15
+ c.move(x,y)
16
+ c.chat("hi")
17
+ """
18
+ def __init__(self, pid, uri="ws://127.0.0.1:8765"):
19
+ self.pid = pid
20
+ self.uri = uri
21
+ self._send_q = asyncio.Queue()
22
+ self.on_event = None # callback for incoming events: fn(dict)
23
+ self._running = False
24
+ self._loop_thread = None
25
+
26
+ async def _run(self):
27
+ try:
28
+ async with websockets.connect(self.uri) as ws:
29
+ # send join
30
+ await ws.send(json.dumps({"type":"join","id":self.pid}))
31
+ self._running = True
32
+ sender_task = asyncio.create_task(self._sender(ws))
33
+ try:
34
+ async for raw in ws:
35
+ try:
36
+ data = json.loads(raw)
37
+ except:
38
+ continue
39
+ if self.on_event:
40
+ # run callback in same thread (safe)
41
+ try:
42
+ self.on_event(data)
43
+ except Exception as cb_e:
44
+ # swallow callback errors
45
+ print("on_event callback error:", cb_e)
46
+ finally:
47
+ sender_task.cancel()
48
+ except Exception as e:
49
+ # notify user on connection lost
50
+ if self.on_event:
51
+ try:
52
+ self.on_event({"type":"sys","msg":f"connection_lost:{e}"})
53
+ except:
54
+ pass
55
+ finally:
56
+ self._running = False
57
+
58
+ async def _sender(self, ws):
59
+ while True:
60
+ try:
61
+ obj = await self._send_q.get()
62
+ await ws.send(json.dumps(obj))
63
+ except Exception:
64
+ break
65
+
66
+ def start(self):
67
+ if self._loop_thread and self._loop_thread.is_alive():
68
+ return
69
+ def target():
70
+ asyncio.run(self._run())
71
+ self._loop_thread = threading.Thread(target=target, daemon=True)
72
+ self._loop_thread.start()
73
+ # give a bit of time to connect
74
+ time.sleep(0.05)
75
+
76
+ def send(self, obj):
77
+ # enqueue safely from any thread
78
+ async def push():
79
+ await self._send_q.put(obj)
80
+ try:
81
+ loop = asyncio.get_running_loop()
82
+ # if main thread has a loop, schedule
83
+ asyncio.run_coroutine_threadsafe(push(), loop)
84
+ except RuntimeError:
85
+ # no running loop in this thread: create a tiny loop to push
86
+ def run_push():
87
+ asyncio.run(push())
88
+ threading.Thread(target=run_push, daemon=True).start()
89
+
90
+ # convenience APIs
91
+ def move(self, x, y):
92
+ self.send({"type":"move","id":self.pid,"x":int(x),"y":int(y)})
93
+
94
+ def chat(self, msg):
95
+ self.send({"type":"chat","id":self.pid,"msg":str(msg)})