crystalwindow 4.4__py3-none-any.whl → 4.6__py3-none-any.whl

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.
crystalwindow/__init__.py CHANGED
@@ -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
  ]
crystalwindow/ai.py CHANGED
@@ -1,18 +1,14 @@
1
1
  # ==========================================================
2
- # CrystalAI v0.8Hybrid Engine (Groq + Symbolic Fallback)
2
+ # CrystalAI v0.7Stabilized & Corrected Engine
3
3
  # ----------------------------------------------------------
4
- # Combines:
5
- # - Groq API for general knowledge (Primary)
6
- # - Pure Python Symbolic Engine for Code Analysis (Fallback)
7
- # - Memory, File Reading, AST Parsing, and Diff Utilities
8
- # ==========================================================
9
4
 
10
5
  import os
11
6
  import ast
12
7
  import difflib
13
- import requests # Re-added for API communication
14
- from typing import Optional, Dict, Any, List
15
- # Removed groq import, using requests for simplicity
8
+ import requests
9
+ import json
10
+ from typing import Optional, Dict, Any
11
+
16
12
 
17
13
  # ==========================================================
18
14
  # Response Wrapper
@@ -30,58 +26,64 @@ class CrystalAIResponse:
30
26
  # MAIN ENGINE
31
27
  # ==========================================================
32
28
  class AI:
33
- DEFAULT_MODEL = "llama-3.1-8b"
29
+ DEFAULT_MODEL = "llama-3.1-70b-versatile"
34
30
  DEFAULT_PERSONALITY = (
35
31
  "You are CrystalWindow AI. You help users with Python code, "
36
- "debugging, errors, docs, and file analysis. You are currently running in "
37
- "Hybrid Mode. Be friendly, technical, clear, and precise."
32
+ "debugging, error analysis, documentation, and file analysis. "
33
+ "Be friendly, technical, clear, and precise."
38
34
  )
39
- # Placeholder Key for testing or when user key is invalid
40
- PLACEHOLDER_KEY = "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"
35
+
36
+ PLACEHOLDER_KEY = "NO_KEY_PROVIDED"
41
37
 
42
38
  # ------------------------------------------------------
43
39
  def __init__(self, key=None, model=None):
40
+
44
41
  # --- KEY VALIDATION ---
45
- if not key or len(key) < 20 or " " in key:
46
- print("[CrystalAI] Warning: Invalid or missing key → using placeholder. To get a Fixed Key go to 'console.groq.com/keys'")
47
- self.key = self.PLACEHOLDER_KEY
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
48
57
  else:
49
58
  self.key = key
50
-
59
+ self.force_local = False
60
+
51
61
  # --- MODEL VALIDATION ---
52
- if not model or not isinstance(model, str) or len(model) < 3:
53
- print(f"[CrystalAI] Unknown model using default: {self.DEFAULT_MODEL}.")
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.")
54
67
  self.model = self.DEFAULT_MODEL
55
68
  else:
56
69
  self.model = model
57
70
 
58
- # Persona
59
71
  self.personality = self.DEFAULT_PERSONALITY
60
-
61
- # Pure AI knowledge (used in symbolic fallback)
62
- self.knowledge_graph: Dict[str, Any] = self._build_knowledge_graph()
63
-
64
- # Library knowledge (loaded .py files)
65
- self.library_context = ""
66
-
67
- # Memory system
68
- self.memory: List[Dict[str, str]] = []
72
+ self.memory = []
69
73
  self.use_memory = True
70
-
71
- # Force local toggle (currently not used as logic is based on Groq success)
72
- self.force_local = False
74
+ self.library_context = ""
73
75
 
74
76
  # ==========================================================
75
77
  # PERSONALITY
76
78
  # ==========================================================
77
79
  def set_personality(self, txt):
78
80
  if not isinstance(txt, str) or len(txt.strip()) < 10:
79
- print("Oops! that's not how to use it—reverting to default.")
81
+ print("[CrystalAI] Personality too short reverting to default.")
80
82
  self.personality = self.DEFAULT_PERSONALITY
81
83
  return
82
84
 
83
85
  if len(txt) > 3000:
84
- print("Oops, personality too long → using default.")
86
+ print("[CrystalAI] Personality too long → using default.")
85
87
  self.personality = self.DEFAULT_PERSONALITY
86
88
  return
87
89
 
@@ -94,22 +96,24 @@ class AI:
94
96
  """
95
97
  Load all Python files as context for smarter answers.
96
98
  """
97
- out = []
98
99
  if not os.path.exists(folder):
99
100
  print("[CrystalAI] Library folder not found.")
100
101
  return
101
102
 
103
+ collected = []
102
104
  for root, _, files in os.walk(folder):
103
105
  for f in files:
104
106
  if f.endswith(".py"):
105
107
  try:
106
- path = os.path.join(root, f)
107
- with open(path, "r", encoding="utf8") as fp:
108
- out.append(f"# FILE: {path}\n" + fp.read() + "\n\n")
109
- except Exception:
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:
110
114
  pass
111
115
 
112
- self.library_context = "\n".join(out)
116
+ self.library_context = "\n".join(collected)[:120_000] # trimmed
113
117
 
114
118
  # ==========================================================
115
119
  # FILE READER
@@ -122,13 +126,13 @@ class AI:
122
126
  try:
123
127
  with open(path, "r", encoding="utf8") as f:
124
128
  return f.read()
125
- except Exception:
126
- return "[CrystalAI] couldn't read file."
129
+ except:
130
+ return "[CrystalAI] could not read file."
127
131
 
128
132
  # ==========================================================
129
- # PROMPT BUILDER (Unified)
133
+ # PROMPT BUILDER
130
134
  # ==========================================================
131
- def _build_prompt(self, user_text, file_data=None):
135
+ def _build_prompt(self, user_text, file_data):
132
136
  final = (
133
137
  f"[SYSTEM]\n{self.personality}\n\n"
134
138
  f"[USER]\n{user_text}\n\n"
@@ -143,10 +147,10 @@ class AI:
143
147
  if self.library_context:
144
148
  final += f"[LIBRARY]\n{self.library_context}\n\n"
145
149
 
146
- if file_data:
150
+ if file_data and not file_data.startswith("[CrystalAI]"):
147
151
  final += f"[FILE]\n{file_data}\n\n"
148
152
 
149
- return final
153
+ return final[:190_000] # safety limit
150
154
 
151
155
  def _save_memory(self, user, ai):
152
156
  self.memory.append({"user": user, "ai": ai})
@@ -154,98 +158,54 @@ class AI:
154
158
  self.memory.pop(0)
155
159
 
156
160
  # ==========================================================
157
- # PURE AI KNOWLEDGE BASE (Symbolic Core)
158
- # ==========================================================
159
- def _build_knowledge_graph(self) -> Dict[str, Any]:
160
- """
161
- Defines the internal knowledge the symbolic AI can reason with.
162
- """
163
- return {
164
- "python": {
165
- "desc": "A high-level, interpreted programming language.",
166
- "keywords": ["language", "interpreted", "high-level"],
167
- "syntax": {
168
- "if_statement": "if condition: ... else: ...",
169
- "loop": "for item in iterable: ..."
170
- }
171
- },
172
- "ast": {
173
- "desc": "Abstract Syntax Tree. Used for parsing code structure.",
174
- "keywords": ["parsing", "code", "structure", "tree"]
175
- },
176
- "fix_code": {
177
- "rule": "look for SyntaxError, especially missing colons or mismatched brackets",
178
- "keywords": ["fix", "error", "bug", "syntax"]
179
- }
180
- }
181
-
182
- # ==========================================================
183
- # PURE AI 'THINKING' ENGINE (Symbolic Fallback)
161
+ # LOCAL FALLBACK AI
184
162
  # ==========================================================
185
- def _symbolic_engine(self, prompt: str, file_data: Optional[str]) -> str:
163
+ def _local_ai(self, user_text, file_data):
186
164
  """
187
- Fallback logic: Simulates 'thinking' using only internal rules and AST.
165
+ Much cleaner, more reliable fallback engine.
188
166
  """
189
- output = ["[Local/SymbolicEngine] Processing request..."]
190
- lower_prompt = prompt.lower()
191
167
 
192
- # --- Stage 1: File Analysis (Real Python AST) ---
168
+ # --- AST SECTION ---
193
169
  if file_data and not file_data.startswith("[CrystalAI]"):
194
- output.append("\n[Stage 1: Code Parsing]")
195
170
  try:
196
171
  ast.parse(file_data)
197
- output.append("✅ **No Syntax Errors Detected** (via AST).")
198
- output.append("The code is structurally sound. Ask for refactoring or explanation.")
199
- return "\n".join(output)
172
+ return (
173
+ "[LocalAI] File parsed successfully no syntax errors.\n"
174
+ "Ask for refactoring, explanation, or improvements."
175
+ )
200
176
  except SyntaxError as se:
201
- fix_rule = self.knowledge_graph["fix_code"]["rule"]
202
- lineno = se.lineno or 0
203
- msg = (
204
- f" **SyntaxError Detected** (via AST):\n"
205
- f"• Message: {se.msg}\n"
206
- f"• Line: {lineno}\n"
207
- f"• Rule suggestion: {fix_rule}"
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}"
208
183
  )
209
- output.append(msg)
210
- output.append(self._snippet(file_data, lineno))
211
- return "\n".join(output)
212
-
213
- # --- Stage 2: Knowledge Graph Lookup (Rule-Based Reasoning) ---
214
- output.append("\n[Stage 2: Symbolic Lookup]")
215
-
216
- found_concept = False
217
- for key, knowledge in self.knowledge_graph.items():
218
- if key in lower_prompt or any(k in lower_prompt for k in knowledge.get("keywords", [])):
219
- if key == "fix_code": continue
220
-
221
- output.append(f"🧠 Found Concept: **{key.upper()}**")
222
- output.append(f"Description: {knowledge.get('desc', 'No detailed description.')}")
223
-
224
- if 'syntax' in knowledge:
225
- output.append("Related Syntax:")
226
- for syn, code in knowledge['syntax'].items():
227
- output.append(f" - {syn.replace('_', ' ')}: `{code}`")
228
-
229
- found_concept = True
230
- break
231
-
232
- if not found_concept:
233
- output.append("❓ Concept Unknown: I am currently offline and limited to my internal knowledge base (Python, AST, Fix Code).")
234
- output.append("Please provide a file for AST analysis or try again later for a full Groq response.")
235
-
236
- return "\n".join(output)
237
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."
238
194
 
239
195
  # ==========================================================
240
- # ASK (Groq → Symbolic Fallback)
196
+ # MAIN "ASK" FUNCTION
241
197
  # ==========================================================
242
198
  def ask(self, text, file=None):
243
199
  file_data = self._read_file(file)
244
200
  prompt = self._build_prompt(text, file_data)
245
-
246
- resp = None
247
201
 
248
- # --- Attempt 1: Call External API (Groq) ---
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
249
209
  try:
250
210
  url = "https://api.groq.com/openai/v1/chat/completions"
251
211
  headers = {
@@ -254,7 +214,7 @@ class AI:
254
214
  }
255
215
 
256
216
  payload = {
257
- "model": self.model,
217
+ "model": self.model,
258
218
  "messages": [
259
219
  {"role": "system", "content": self.personality},
260
220
  {"role": "user", "content": prompt}
@@ -262,35 +222,28 @@ class AI:
262
222
  "temperature": 0.3
263
223
  }
264
224
 
265
- r = requests.post(url, json=payload, headers=headers, timeout=10)
225
+ r = requests.post(url, json=payload, headers=headers, timeout=8)
266
226
  data = r.json()
267
227
 
268
228
  if "error" in data:
269
- # If the API returns an error (e.g., bad key), we force the fallback
270
- raise RuntimeError(f"API Error: {data['error'].get('message', 'Unknown API Error')}")
229
+ raise RuntimeError(data["error"])
271
230
 
272
231
  resp = data["choices"][0]["message"]["content"]
273
232
 
274
- except Exception as e:
275
- # This block handles API connection failures (timeout) or API-side errors (bad key)
276
- print(f"[CrystalAI] Groq connection failed or returned error: {e.__class__.__name__}")
277
- print("[CrystalAI] Falling back to self-contained Symbolic Engine...")
278
-
279
- # --- Attempt 2: Fallback to Symbolic Engine ---
280
- resp = self._symbolic_engine(prompt, file_data)
281
-
233
+ except Exception:
234
+ resp = self._local_ai(text, file_data)
282
235
 
283
236
  self._save_memory(text, resp)
284
237
  return CrystalAIResponse(resp)
285
238
 
286
239
  # ==========================================================
287
- # ASK (terminal)
240
+ # TERMINAL HELPER
288
241
  # ==========================================================
289
242
  def ask_t(self, text, file=None):
290
243
  return self.ask(f"[TERMINAL] {text}", file)
291
244
 
292
245
  # ==========================================================
293
- # AUTO FIX CODE (v0.4)
246
+ # AUTO FIX CODE
294
247
  # ==========================================================
295
248
  def fix_code(self, file_path):
296
249
  orig = self._read_file(file_path)
@@ -304,47 +257,42 @@ class AI:
304
257
  except SyntaxError as se:
305
258
  fixed, notes = self._simple_fix(orig, se)
306
259
  diff = self._make_diff(orig, fixed)
307
- pretty = "[AI] Auto-fix result:\n" + "\n".join(notes) + "\n\n" + diff
308
- return CrystalAIResponse(pretty, {"diff": diff, "notes": notes})
260
+ msg = "[AI] Auto-fix result:\n" + "\n".join(notes) + "\n\n" + diff
261
+ return CrystalAIResponse(msg, {"diff": diff, "notes": notes})
309
262
 
310
263
  # ==========================================================
311
264
  # SIMPLE AUTO-FIX ENGINE
312
265
  # ==========================================================
313
- def _simple_fix(self, src, syntax_error):
266
+ def _simple_fix(self, src, err):
314
267
  notes = []
315
268
  lines = src.splitlines()
316
- msg = getattr(syntax_error, "msg", "")
317
- lineno = syntax_error.lineno or 0
269
+ msg = getattr(err, "msg", "")
270
+ lineno = err.lineno or 0
318
271
 
319
- # missing colon fix
320
272
  if "expected" in msg and ":" in msg:
321
273
  if 1 <= lineno <= len(lines):
322
- line = lines[lineno - 1].rstrip()
323
- if not line.endswith(":"):
324
- lines[lineno - 1] = line + ":"
274
+ l = lines[lineno - 1].rstrip()
275
+ if not l.endswith(":"):
276
+ lines[lineno - 1] = l + ":"
325
277
  notes.append("[fix] added missing ':'")
326
278
  candidate = "\n".join(lines)
327
279
  try:
328
280
  ast.parse(candidate)
329
281
  return candidate, notes
330
- except Exception:
282
+ except:
331
283
  pass
332
284
 
333
- # fallback
334
- notes.append("[info] auto-fix could not fix everything")
285
+ notes.append("[info] auto-fix could not fix the error")
335
286
  return src, notes
336
287
 
337
288
  # ==========================================================
338
- # DIFF UTIL
289
+ # DIFF HELPER
339
290
  # ==========================================================
340
291
  def _make_diff(self, old, new):
341
292
  return "\n".join(
342
293
  difflib.unified_diff(
343
- old.splitlines(),
344
- new.splitlines(),
345
- fromfile="old",
346
- tofile="new",
347
- lineterm=""
294
+ old.splitlines(), new.splitlines(),
295
+ fromfile="old", tofile="new", lineterm=""
348
296
  )
349
297
  )
350
298
 
@@ -358,9 +306,9 @@ class AI:
358
306
  out = []
359
307
  for i in range(start, end):
360
308
  mark = "->" if (i + 1) == lineno else " "
361
- out.append(f"{mark} {i+1:<4}: {lines[i]}")
309
+ out.append(f"{mark} {i+1:4}: {lines[i]}")
362
310
  return "\n".join(out)
363
311
 
364
312
  # ==========================================================
365
- # END OF HYBRID ENGINE
366
- # ==========================================================
313
+ # END OF ENGINE
314
+ # ==========================================================
crystalwindow/clock.py CHANGED
@@ -1,23 +1,189 @@
1
1
  import time
2
+ from datetime import datetime
3
+ from collections import deque
2
4
 
3
- class Clock:
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:
4
40
  def __init__(self):
5
- self.last = time.time()
6
- self.delta = 0
7
- self.fps = 60
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
8
102
 
9
- def tick(self, fps=None):
10
- now = time.time()
11
- self.delta = now - self.last
103
+ now = time.perf_counter()
104
+ raw_delta = now - self.last
12
105
  self.last = now
13
106
 
14
- if fps:
15
- self.fps = fps
16
- sleep_for = (1 / fps) - self.delta
17
- if sleep_for > 0:
18
- time.sleep(sleep_for)
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()
19
124
 
20
125
  return self.delta
21
126
 
22
127
  def get_fps(self):
23
- return round(1 / self.delta, 2) if self.delta else 0
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)})
@@ -0,0 +1,233 @@
1
+ """
2
+ CrystalWindow WebSearch module — Google (Serper.dev) Edition
3
+ """
4
+
5
+ from __future__ import annotations
6
+ import requests
7
+ import webbrowser
8
+ from dataclasses import dataclass
9
+ from typing import List, Tuple, Optional, Any
10
+
11
+ # Optional Qt integration
12
+ try:
13
+ from PySide6.QtWidgets import (
14
+ QWidget, QVBoxLayout, QLineEdit, QPushButton, QTextEdit,
15
+ QListWidget, QListWidgetItem, QLabel, QHBoxLayout
16
+ )
17
+ from PySide6.QtCore import Qt, QUrl
18
+ from PySide6.QtGui import QDesktopServices
19
+ PYSIDE6_AVAILABLE = True
20
+ except Exception:
21
+ PYSIDE6_AVAILABLE = False
22
+
23
+
24
+ # --------------------------
25
+ # Data result structure
26
+ # --------------------------
27
+
28
+ @dataclass
29
+ class WebSearchResult:
30
+ text: str
31
+ links: List[Tuple[str, str]] # (title, url)
32
+ raw: Any = None
33
+
34
+
35
+ # --------------------------
36
+ # Main WebSearch class
37
+ # --------------------------
38
+
39
+ class WebSearch:
40
+ """ Real Web Search helper for CrystalWindow using Google (Serper.dev). """
41
+
42
+ # --- DuckDuckGo Instant Answer (kept for fallback) ---
43
+ DUCKDUCKGO_INSTANT = "https://api.duckduckgo.com/"
44
+
45
+ # --- Google Search (Serper.dev) ---
46
+ SERPER_URL = "https://google.serper.dev/search"
47
+ SERPER_API_KEY = "a8e02281a0c3b58bb29a3731b6a2aec1d8d8a487" # <<<<<<<< REPLACE THIS
48
+
49
+ # --------------------------------------------------
50
+ # DuckDuckGo Instant Answer (old API)
51
+ # --------------------------------------------------
52
+ @classmethod
53
+ def _duckduckgo_instant(cls, query: str) -> WebSearchResult:
54
+ params = {
55
+ "q": query,
56
+ "format": "json",
57
+ "no_html": 1,
58
+ "no_redirect": 1,
59
+ }
60
+ try:
61
+ r = requests.get(cls.DUCKDUCKGO_INSTANT, params=params, timeout=8)
62
+ r.raise_for_status()
63
+ except Exception as e:
64
+ return WebSearchResult(text=f"DuckDuckGo error: {e}", links=[])
65
+
66
+ data = r.json()
67
+ abstract = data.get("AbstractText") or data.get("Abstract") or ""
68
+ links: List[Tuple[str, str]] = []
69
+
70
+ if data.get("AbstractURL"):
71
+ links.append((data.get("Heading", "Result"), data["AbstractURL"]))
72
+
73
+ def extract_related(rt):
74
+ if isinstance(rt, dict):
75
+ if rt.get("FirstURL") and rt.get("Text"):
76
+ links.append((rt["Text"], rt["FirstURL"]))
77
+ for key in ("Topics", "RelatedTopics"):
78
+ if isinstance(rt.get(key), list):
79
+ for sub in rt[key]:
80
+ extract_related(sub)
81
+ elif isinstance(rt, list):
82
+ for item in rt:
83
+ extract_related(item)
84
+
85
+ extract_related(data.get("RelatedTopics", []))
86
+ if not abstract and links:
87
+ abstract = "\n".join([t for t, _ in links[:3]])
88
+
89
+ return WebSearchResult(text=abstract.strip(), links=links, raw=data)
90
+
91
+ # --------------------------------------------------
92
+ # Google Search via Serper.dev (NEW)
93
+ # --------------------------------------------------
94
+ @classmethod
95
+ def _google_serper(cls, query: str) -> WebSearchResult:
96
+ headers = {
97
+ "X-API-KEY": cls.SERPER_API_KEY,
98
+ "Content-Type": "application/json"
99
+ }
100
+ body = {"q": query}
101
+
102
+ try:
103
+ r = requests.post(cls.SERPER_URL, headers=headers, json=body, timeout=8)
104
+ r.raise_for_status()
105
+ except Exception as e:
106
+ return WebSearchResult(text=f"Google search error: {e}", links=[])
107
+
108
+ data = r.json()
109
+
110
+ links = []
111
+ summary_parts = []
112
+
113
+ for item in data.get("organic", []):
114
+ title = item.get("title", "Untitled")
115
+ url = item.get("link", "")
116
+ snippet = item.get("snippet", "")
117
+
118
+ links.append((title, url))
119
+ if snippet:
120
+ summary_parts.append(snippet)
121
+
122
+ summary = "\n".join(summary_parts[:3]).strip()
123
+ if not summary:
124
+ summary = "(no summary)"
125
+
126
+ return WebSearchResult(summary, links, data)
127
+
128
+ # --------------------------------------------------
129
+ # Main public search()
130
+ # --------------------------------------------------
131
+ @classmethod
132
+ def search(cls, query: str, engine: str = "google") -> WebSearchResult:
133
+ if not query or not query.strip():
134
+ return WebSearchResult(text="", links=[])
135
+
136
+ q = query.strip().lower()
137
+ engine = engine.lower()
138
+
139
+ if engine == "google":
140
+ return cls._google_serper(q)
141
+
142
+ if engine == "duckduckgo":
143
+ return cls._duckduckgo_instant(q)
144
+
145
+ return WebSearchResult(text=f"Unknown engine '{engine}'", links=[])
146
+
147
+ # --------------------------------------------------
148
+ # URL opener
149
+ # --------------------------------------------------
150
+ @staticmethod
151
+ def open_url(url: str) -> None:
152
+ try:
153
+ webbrowser.open(url)
154
+ except Exception:
155
+ pass
156
+
157
+ # --------------------------------------------------
158
+ # Optional PySide6 GUI
159
+ # --------------------------------------------------
160
+ if PYSIDE6_AVAILABLE:
161
+ class _CWWebTab(QWidget):
162
+ def __init__(self, parent=None):
163
+ super().__init__(parent)
164
+ self.setWindowTitle("WebSearch")
165
+
166
+ layout = QVBoxLayout(self)
167
+
168
+ # Search bar
169
+ bar = QHBoxLayout()
170
+ self.qbox = QLineEdit()
171
+ self.qbox.setPlaceholderText("Search the web...")
172
+ btn = QPushButton("Search")
173
+ btn.clicked.connect(self._do_search)
174
+ bar.addWidget(self.qbox)
175
+ bar.addWidget(btn)
176
+ layout.addLayout(bar)
177
+
178
+ # Summary text
179
+ self.summary = QTextEdit()
180
+ self.summary.setReadOnly(True)
181
+ self.summary.setFixedHeight(150)
182
+ layout.addWidget(self.summary)
183
+
184
+ # Links list
185
+ self.links = QListWidget()
186
+ self.links.itemActivated.connect(self._open_item)
187
+ layout.addWidget(self.links)
188
+
189
+ self.note = QLabel("Powered by Google (Serper.dev) API")
190
+ self.note.setAlignment(Qt.AlignCenter)
191
+ layout.addWidget(self.note)
192
+
193
+ def _populate(self, result: WebSearchResult):
194
+ self.summary.setPlainText(result.text)
195
+ self.links.clear()
196
+ for t, u in result.links:
197
+ item = QListWidgetItem(f"{t}\n{u}")
198
+ item.setData(Qt.UserRole, u)
199
+ self.links.addItem(item)
200
+
201
+ def _open_item(self, item: QListWidgetItem):
202
+ url = item.data(Qt.UserRole)
203
+ if url:
204
+ QDesktopServices.openUrl(QUrl(url))
205
+
206
+ def _do_search(self):
207
+ q = self.qbox.text().strip()
208
+ if not q:
209
+ self.summary.setPlainText("Type something to search.")
210
+ return
211
+ self.summary.setPlainText("Searching...")
212
+ res = WebSearch.search(q, engine="google")
213
+ self._populate(res)
214
+
215
+ @classmethod
216
+ def register_cw_tab(cls, window):
217
+ try:
218
+ tab = cls._CWWebTab()
219
+ if hasattr(window, "add_tab"):
220
+ window.add_tab("WebSearch", tab)
221
+ elif hasattr(window, "addTab"):
222
+ window.addTab(tab, "WebSearch")
223
+ else:
224
+ if hasattr(window, "setCentralWidget"):
225
+ window.setCentralWidget(tab)
226
+ return tab
227
+ except Exception:
228
+ return None
229
+
230
+ else:
231
+ @classmethod
232
+ def register_cw_tab(cls, window):
233
+ return None
crystalwindow/window.py CHANGED
@@ -1,6 +1,8 @@
1
1
  # === Window Management (tkinter version) ===
2
2
  import tkinter as tk
3
3
  import base64, io, os, sys, contextlib, time
4
+ from .websearch import WebSearch
5
+
4
6
 
5
7
  # === Boot ===
6
8
  main_file = os.path.basename(sys.argv[0])
@@ -117,6 +119,8 @@ class Window:
117
119
  self._key_last = {} # new dict to store last press time per key
118
120
  self._key_cooldown = 0.00001 # seconds, tweak for faster/slower input
119
121
 
122
+ WebSearch.register_cw_tab(self)
123
+
120
124
  # === Event bindings ===
121
125
  self.root.bind("<KeyPress>", self._on_keydown)
122
126
  self.root.bind("<KeyRelease>", self._on_keyup)
@@ -163,7 +167,8 @@ class Window:
163
167
 
164
168
  def mouse_pressed(self, button=1):
165
169
  return self._mouse_pressed[button - 1]
166
-
170
+ def add_tab(self, title, widget):
171
+ self.tabs.addTab(widget, title)
167
172
  # === Drawing ===
168
173
  def fill(self, color):
169
174
  """Fill background with a color"""
@@ -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
@@ -1,13 +1,14 @@
1
1
  crystalwindow/FileHelper.py,sha256=aUnnRG7UwvzJt-idjWjmpwy3RM6nqLlC3-7Bae6Yb94,5471
2
- crystalwindow/__init__.py,sha256=a2kdMZ29QZ4kSQ3M8jLvCR6g3OUQxNhdaT3ycxoames,2264
3
- crystalwindow/ai.py,sha256=4CN3_Fi5zERxyqoHcUyXPvm4MVWaicA7JGK0lRb-ybQ,14661
2
+ crystalwindow/__init__.py,sha256=Vr7Zd7Wgsu4id-3KaVuIkorRm80srd4zNR8fXWJZuhY,2662
3
+ crystalwindow/ai.py,sha256=YIt6dNe1QljSAlNECCVa3DMUSIqQEJRIAAbQpYqFNNo,11525
4
4
  crystalwindow/animation.py,sha256=zHjrdBXQeyNaLAuaGPldJueX05OZ5j31YR8NizmR0uQ,427
5
5
  crystalwindow/assets.py,sha256=2Cj0zdhMWo3mWjdr9KU5n-9_8iKj_fJ9uShMFA-27HU,5193
6
6
  crystalwindow/camera.py,sha256=tbn4X-jxMIszAUg3Iu-89gJN5nij0mjPMEzGotcLbJI,712
7
- crystalwindow/clock.py,sha256=EAcNvxonD7ktkKSIbuVHqzhPhQqtUQtleMSBB_OuUQY,541
7
+ crystalwindow/clock.py,sha256=P7Hzv6-_sHIwJRFcoMLMjQB2BnVjpQWQhRHotp6OPV0,6105
8
8
  crystalwindow/collision.py,sha256=hpkHTp_KparghVK-itp_rxuYdd2GbQMxICHlUBv0rSw,472
9
9
  crystalwindow/color_handler.py,sha256=ZnXnz8552GPiRAnCkW_8wycwRRMAaFRFLlCcsrv0j2E,4071
10
10
  crystalwindow/crystal3d.py,sha256=-Te9IJgbtzl8Mpuc4BPi2bn2apnvBNTI2GwxSLd8sqg,2006
11
+ crystalwindow/cw_client.py,sha256=KxhvXtsTfrCupB2zi5e35ppKVPrOkVXTuhgCNT6V2eA,3264
11
12
  crystalwindow/draw_helpers.py,sha256=HqjI5fTbdnA55g4LKYEuMUdIjrWaBm2U8RmeUXjcQGw,821
12
13
  crystalwindow/draw_rects.py,sha256=o7siET3y35N2LPeNBGe8QhsQbOH8J-xF6fOUz07rymU,1484
13
14
  crystalwindow/draw_text_helper.py,sha256=qv5fFCuTCKWeGDk9Z_ZpOzrTFP8YYwlgQrrorwrq9Hg,1298
@@ -21,7 +22,8 @@ crystalwindow/player.py,sha256=4wpIdUZLTlRXV8fDRQ11yVJRbx_cv8Ekpn2y7pQGgAQ,3442
21
22
  crystalwindow/sprites.py,sha256=2kYW4QfSnyUwRLedZCkb99wUSxn2a0JdWOHcfkxcG0U,3411
22
23
  crystalwindow/tilemap.py,sha256=PHoKL1eWuNeHIf0w-Jh5MGdQGEgklVsxqqJOS-GNMKI,322
23
24
  crystalwindow/ver_warner.py,sha256=qEN3ulc1NixBy15FFx2R3Zu0DhyJTVJwiESGAPwpynM,3373
24
- crystalwindow/window.py,sha256=e6C8yIdVdjOduIeU0ZmlrNn8GmyvHuzKs473pRggkz0,18180
25
+ crystalwindow/websearch.py,sha256=O98LfYPuovdU6ct8iStNpoEJLTc26av9bz4nqX6lyN4,8027
26
+ crystalwindow/window.py,sha256=umJ8gNGS64N_mVzPBBxZKUNq0mDcGoy7ImJthLlZHSI,18337
25
27
  crystalwindow/Icons/default_icon.png,sha256=Loq27Pxb8Wb3Sz-XwtNF1RmlLNxR4TcfOWfK-1lWcII,7724
26
28
  crystalwindow/docs/getting_started.md,sha256=e_XEhJk8eatS22MX0nRX7hQNkYkwN9both1ObabZSTw,5759
27
29
  crystalwindow/docs/index.md,sha256=bd14uLWtRSeRBm28zngGyfGDI1J6bJRvHkQLDpYwgOE,747
@@ -33,8 +35,8 @@ crystalwindow/gametests/guitesting.py,sha256=SrOssY5peCQEV6TQ1AiOWtjb9phVGdRzW-Q
33
35
  crystalwindow/gametests/sandbox.py,sha256=Oo2tU2N0y3BPVa6T5vs_h9N6islhQrjSrr_78XLut5I,1007
34
36
  crystalwindow/gametests/squaremove.py,sha256=poP2Zjl2oc2HVvIAgIK34H2jVj6otL4jEdvAOR6L9sI,572
35
37
  crystalwindow/gametests/windowtesting.py,sha256=_9X6wnV1-_X_PtNS-0zu-k209NtFIwAc4vpxLPp7V2o,97
36
- crystalwindow-4.4.dist-info/licenses/LICENSE,sha256=Gt5cJRchdNt0guxyQMHKsATN5PM5mjuDhdO6Gzs9qQc,1096
37
- crystalwindow-4.4.dist-info/METADATA,sha256=8j8_X2PPPAo6OmZELS5-KpjARhkwJcnYcC4r2qur-7M,7338
38
- crystalwindow-4.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
39
- crystalwindow-4.4.dist-info/top_level.txt,sha256=PeQSld4b19XWT-zvbYkvE2Xg8sakIMbDzSzSdOSRN8o,14
40
- crystalwindow-4.4.dist-info/RECORD,,
38
+ crystalwindow-4.6.dist-info/licenses/LICENSE,sha256=Gt5cJRchdNt0guxyQMHKsATN5PM5mjuDhdO6Gzs9qQc,1096
39
+ crystalwindow-4.6.dist-info/METADATA,sha256=PSVgwP0RGNqMC7PerNTZkFFH4KzslVZNDBCZ9MHrsww,7338
40
+ crystalwindow-4.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
41
+ crystalwindow-4.6.dist-info/top_level.txt,sha256=PeQSld4b19XWT-zvbYkvE2Xg8sakIMbDzSzSdOSRN8o,14
42
+ crystalwindow-4.6.dist-info/RECORD,,