crystalwindow 4.3__py3-none-any.whl → 4.5__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
@@ -45,6 +45,7 @@ from .draw_tool import CrystalDraw
45
45
  from .fun_helpers import random_name, DebugOverlay, random_color, random_palette, lerp
46
46
  from .camera import Camera
47
47
  from .color_handler import Colors, Color
48
+ from .websearch import WebSearchResult, WebSearch
48
49
 
49
50
  # === 3D Engine ===
50
51
  from .crystal3d import CW3D
@@ -52,6 +53,9 @@ from .crystal3d import CW3D
52
53
  # === AI Engine ===
53
54
  from .ai import AI
54
55
 
56
+ # === Server Manager ===
57
+ from .cw_client import CWClient
58
+
55
59
  __all__ = [
56
60
  # --- Core ---
57
61
  "Window", "Sprite", "TileMap", "Player", "Gravity", "FileHelper", "Mathematics",
@@ -84,11 +88,14 @@ __all__ = [
84
88
  "gradient_rect", "CameraShake", "DrawHelper", "DrawTextManager", "CrystalDraw",
85
89
 
86
90
  # --- Misc ---
87
- "random_name", "DebugOverlay", "Camera", "Colors", "Color", "random_palette", "lerp"
91
+ "random_name", "DebugOverlay", "Camera", "Colors", "Color", "random_palette", "lerp", "WebSearchResult", "WebSearch",
88
92
 
89
93
  # --- 3D ---
90
94
  "CW3D",
91
95
 
92
96
  # --- AI ---
93
97
  "AI",
98
+
99
+ # --- Server Client ---
100
+ "CWClient",
94
101
  ]
crystalwindow/ai.py CHANGED
@@ -1,20 +1,14 @@
1
1
  # ==========================================================
2
- # CrystalAI v0.7 — Self-Contained Symbolic Engine
2
+ # CrystalAI v0.7 — Stabilized & Corrected Engine
3
3
  # ----------------------------------------------------------
4
- # Focus: Pure Python 'thinking' (Knowledge Graph/Rule-Based)
5
- # Changes:
6
- # - Refactored _local_ai to _symbolic_engine
7
- # - Added a simple knowledge base (K-Graph)
8
- # - Added context/intent matching for 'thinking'
9
- # - Removed Groq/External API integration
10
- # ==========================================================
11
4
 
12
5
  import os
13
6
  import ast
14
7
  import difflib
15
- from typing import Optional, Dict, Any, List
16
- # Removed requests and groq imports as they are no longer needed
17
- # Removed import groq (if it was present)
8
+ import requests
9
+ import json
10
+ from typing import Optional, Dict, Any
11
+
18
12
 
19
13
  # ==========================================================
20
14
  # Response Wrapper
@@ -32,33 +26,51 @@ class CrystalAIResponse:
32
26
  # MAIN ENGINE
33
27
  # ==========================================================
34
28
  class AI:
35
- DEFAULT_MODEL = "PURE_SYMBOLIC"
29
+ DEFAULT_MODEL = "llama-3.1-70b-versatile"
36
30
  DEFAULT_PERSONALITY = (
37
- "You are CrystalMind AI, a completely self-coded, symbolic engine. "
38
- "I process information using an internal rule-base and knowledge graph. "
39
- "I can reason about simple code and defined concepts."
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."
40
34
  )
41
- # Key is ignored in this self-contained mode, but kept for signature consistency
42
- PLACEHOLDER_KEY = "PURE_SYMBOLIC_KEY_IGNORED"
35
+
36
+ PLACEHOLDER_KEY = "NO_KEY_PROVIDED"
43
37
 
44
38
  # ------------------------------------------------------
45
39
  def __init__(self, key=None, model=None):
46
- # --- KEY/MODEL SETUP (for consistency, though ignored) ---
47
- self.key = self.PLACEHOLDER_KEY
48
- self.model = self.DEFAULT_MODEL
49
- self.personality = self.DEFAULT_PERSONALITY
50
40
 
51
- # --- PURE AI KNOWLEDGE BASE (The 'Thinking' Core) ---
52
- self.knowledge_graph: Dict[str, Any] = self._build_knowledge_graph()
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
53
70
 
54
- # v0.4 memory system (optional)
55
- self.memory: List[Dict[str, str]] = []
71
+ self.personality = self.DEFAULT_PERSONALITY
72
+ self.memory = []
56
73
  self.use_memory = True
57
-
58
- # v0.4 toggle for forcing local engine
59
- self.force_local = True # Always True for a self-contained AI
60
-
61
- # Library knowledge (loaded .py files)
62
74
  self.library_context = ""
63
75
 
64
76
  # ==========================================================
@@ -66,12 +78,12 @@ class AI:
66
78
  # ==========================================================
67
79
  def set_personality(self, txt):
68
80
  if not isinstance(txt, str) or len(txt.strip()) < 10:
69
- print("Oops! thats not how to use it—reverting to default.")
81
+ print("[CrystalAI] Personality too short reverting to default.")
70
82
  self.personality = self.DEFAULT_PERSONALITY
71
83
  return
72
84
 
73
85
  if len(txt) > 3000:
74
- print("Oops, personality too long → using default.")
86
+ print("[CrystalAI] Personality too long → using default.")
75
87
  self.personality = self.DEFAULT_PERSONALITY
76
88
  return
77
89
 
@@ -83,24 +95,25 @@ class AI:
83
95
  def index_library(self, folder):
84
96
  """
85
97
  Load all Python files as context for smarter answers.
86
- (Context is used in prompt but processed by local engine's rules)
87
98
  """
88
- out = []
89
99
  if not os.path.exists(folder):
90
100
  print("[CrystalAI] Library folder not found.")
91
101
  return
92
102
 
103
+ collected = []
93
104
  for root, _, files in os.walk(folder):
94
105
  for f in files:
95
106
  if f.endswith(".py"):
96
107
  try:
97
- path = os.path.join(root, f)
98
- with open(path, "r", encoding="utf8") as fp:
99
- out.append(f"# FILE: {path}\n" + fp.read() + "\n\n")
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
+ )
100
113
  except:
101
114
  pass
102
115
 
103
- self.library_context = "\n".join(out)
116
+ self.library_context = "\n".join(collected)[:120_000] # trimmed
104
117
 
105
118
  # ==========================================================
106
119
  # FILE READER
@@ -114,12 +127,12 @@ class AI:
114
127
  with open(path, "r", encoding="utf8") as f:
115
128
  return f.read()
116
129
  except:
117
- return "[CrystalAI] couldnt read file."
130
+ return "[CrystalAI] could not read file."
118
131
 
119
132
  # ==========================================================
120
- # PROMPT BUILDER (Unified)
133
+ # PROMPT BUILDER
121
134
  # ==========================================================
122
- def _build_prompt(self, user_text, file_data=None):
135
+ def _build_prompt(self, user_text, file_data):
123
136
  final = (
124
137
  f"[SYSTEM]\n{self.personality}\n\n"
125
138
  f"[USER]\n{user_text}\n\n"
@@ -134,10 +147,10 @@ class AI:
134
147
  if self.library_context:
135
148
  final += f"[LIBRARY]\n{self.library_context}\n\n"
136
149
 
137
- if file_data:
150
+ if file_data and not file_data.startswith("[CrystalAI]"):
138
151
  final += f"[FILE]\n{file_data}\n\n"
139
152
 
140
- return final
153
+ return final[:190_000] # safety limit
141
154
 
142
155
  def _save_memory(self, user, ai):
143
156
  self.memory.append({"user": user, "ai": ai})
@@ -145,114 +158,92 @@ class AI:
145
158
  self.memory.pop(0)
146
159
 
147
160
  # ==========================================================
148
- # PURE AI KNOWLEDGE BASE (Self-Contained 'Knowledge')
149
- # ==========================================================
150
- def _build_knowledge_graph(self) -> Dict[str, Any]:
151
- """
152
- Defines the internal knowledge the pure AI can reason with.
153
- """
154
- return {
155
- "python": {
156
- "desc": "A high-level, interpreted programming language.",
157
- "keywords": ["language", "interpreted", "high-level"],
158
- "syntax": {
159
- "if_statement": "if condition: ... else: ...",
160
- "loop": "for item in iterable: ..."
161
- }
162
- },
163
- "ast": {
164
- "desc": "Abstract Syntax Tree. Used for parsing code structure.",
165
- "keywords": ["parsing", "code", "structure", "tree"]
166
- },
167
- "fix_code": {
168
- "rule": "look for SyntaxError, especially missing colons or mismatched brackets",
169
- "keywords": ["fix", "error", "bug", "syntax"]
170
- }
171
- }
172
-
173
- # ==========================================================
174
- # PURE AI 'THINKING' ENGINE (Replaces _local_ai)
161
+ # LOCAL FALLBACK AI
175
162
  # ==========================================================
176
- def _symbolic_engine(self, prompt: str, file_data: Optional[str]) -> str:
163
+ def _local_ai(self, user_text, file_data):
177
164
  """
178
- Simulates 'thinking' using only internal rules and the knowledge graph.
165
+ Much cleaner, more reliable fallback engine.
179
166
  """
180
- output = ["[SymbolicEngine] Processing request..."]
181
- lower_prompt = prompt.lower()
182
167
 
183
- # --- Stage 1: File Analysis (Real Python AST) ---
168
+ # --- AST SECTION ---
184
169
  if file_data and not file_data.startswith("[CrystalAI]"):
185
- output.append("\n[Stage 1: Code Parsing]")
186
170
  try:
187
171
  ast.parse(file_data)
188
- output.append("✅ **No Syntax Errors Detected** (via AST).")
189
- output.append("The code is structurally sound. Ask for refactoring or explanation.")
190
- return "\n".join(output)
172
+ return (
173
+ "[LocalAI] File parsed successfully no syntax errors.\n"
174
+ "Ask for refactoring, explanation, or improvements."
175
+ )
191
176
  except SyntaxError as se:
192
- # Use the built-in fix rule from the knowledge graph
193
- fix_rule = self.knowledge_graph["fix_code"]["rule"]
194
- lineno = se.lineno or 0
195
- offset = se.offset or 0
196
- msg = (
197
- f"❌ **SyntaxError Detected** (via AST):\n"
198
- f"• Message: {se.msg}\n"
199
- f"• Line: {lineno}\n"
200
- f"• Column: {offset}\n"
201
- 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}"
202
183
  )
203
- output.append(msg)
204
- output.append(self._snippet(file_data, lineno))
205
- return "\n".join(output)
206
-
207
- # --- Stage 2: Knowledge Graph Lookup (Rule-Based Reasoning) ---
208
- output.append("\n[Stage 2: Symbolic Lookup]")
209
-
210
- # Check for concepts the AI 'knows'
211
- found_concept = False
212
- for key, knowledge in self.knowledge_graph.items():
213
- if key in lower_prompt or any(k in lower_prompt for k in knowledge.get("keywords", [])):
214
- if key == "fix_code": continue # Already handled in Stage 1
215
-
216
- output.append(f"🧠 Found Concept: **{key.upper()}**")
217
- output.append(f"Description: {knowledge.get('desc', 'No detailed description.')}")
218
-
219
- # Simple reasoning about related syntax
220
- if 'syntax' in knowledge:
221
- output.append("Related Syntax:")
222
- for syn, code in knowledge['syntax'].items():
223
- output.append(f" - {syn.replace('_', ' ')}: `{code}`")
224
-
225
- found_concept = True
226
- break
227
-
228
- if not found_concept:
229
- output.append("❓ Concept Unknown: I am limited to my internal knowledge base (Python, AST, Fix Code).")
230
- output.append("Please ask about a defined concept or provide a file for AST analysis.")
231
-
232
-
233
- return "\n".join(output)
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."
234
194
 
235
195
  # ==========================================================
236
- # ASK (Pure Symbolic Engine)
196
+ # MAIN "ASK" FUNCTION
237
197
  # ==========================================================
238
198
  def ask(self, text, file=None):
239
199
  file_data = self._read_file(file)
240
200
  prompt = self._build_prompt(text, file_data)
241
-
242
- # We skip the external API call entirely, relying on the symbolic engine
243
- resp = self._symbolic_engine(prompt, 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)
244
235
 
245
236
  self._save_memory(text, resp)
246
237
  return CrystalAIResponse(resp)
247
238
 
248
239
  # ==========================================================
249
- # ASK (terminal)
240
+ # TERMINAL HELPER
250
241
  # ==========================================================
251
242
  def ask_t(self, text, file=None):
252
243
  return self.ask(f"[TERMINAL] {text}", file)
253
244
 
254
245
  # ==========================================================
255
- # AUTO FIX CODE (v0.4)
246
+ # AUTO FIX CODE
256
247
  # ==========================================================
257
248
  def fix_code(self, file_path):
258
249
  orig = self._read_file(file_path)
@@ -266,24 +257,23 @@ class AI:
266
257
  except SyntaxError as se:
267
258
  fixed, notes = self._simple_fix(orig, se)
268
259
  diff = self._make_diff(orig, fixed)
269
- pretty = "[AI] Auto-fix result:\n" + "\n".join(notes) + "\n\n" + diff
270
- 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})
271
262
 
272
263
  # ==========================================================
273
264
  # SIMPLE AUTO-FIX ENGINE
274
265
  # ==========================================================
275
- def _simple_fix(self, src, syntax_error):
266
+ def _simple_fix(self, src, err):
276
267
  notes = []
277
268
  lines = src.splitlines()
278
- msg = getattr(syntax_error, "msg", "")
279
- lineno = syntax_error.lineno or 0
269
+ msg = getattr(err, "msg", "")
270
+ lineno = err.lineno or 0
280
271
 
281
- # missing colon
282
272
  if "expected" in msg and ":" in msg:
283
273
  if 1 <= lineno <= len(lines):
284
- line = lines[lineno - 1].rstrip()
285
- if not line.endswith(":"):
286
- lines[lineno - 1] = line + ":"
274
+ l = lines[lineno - 1].rstrip()
275
+ if not l.endswith(":"):
276
+ lines[lineno - 1] = l + ":"
287
277
  notes.append("[fix] added missing ':'")
288
278
  candidate = "\n".join(lines)
289
279
  try:
@@ -292,21 +282,17 @@ class AI:
292
282
  except:
293
283
  pass
294
284
 
295
- # fallback
296
- notes.append("[info] auto-fix could not fix everything")
285
+ notes.append("[info] auto-fix could not fix the error")
297
286
  return src, notes
298
287
 
299
288
  # ==========================================================
300
- # DIFF UTIL
289
+ # DIFF HELPER
301
290
  # ==========================================================
302
291
  def _make_diff(self, old, new):
303
292
  return "\n".join(
304
293
  difflib.unified_diff(
305
- old.splitlines(),
306
- new.splitlines(),
307
- fromfile="old",
308
- tofile="new",
309
- lineterm=""
294
+ old.splitlines(), new.splitlines(),
295
+ fromfile="old", tofile="new", lineterm=""
310
296
  )
311
297
  )
312
298
 
@@ -320,10 +306,9 @@ class AI:
320
306
  out = []
321
307
  for i in range(start, end):
322
308
  mark = "->" if (i + 1) == lineno else " "
323
- # Adjusted line formatting for clarity
324
- out.append(f"{mark} {i+1:<4}: {lines[i]}")
309
+ out.append(f"{mark} {i+1:4}: {lines[i]}")
325
310
  return "\n".join(out)
326
311
 
327
312
  # ==========================================================
328
- # END OF PURE SYMBOLIC ENGINE
329
- # ==========================================================
313
+ # END OF ENGINE
314
+ # ==========================================================
@@ -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.3
3
+ Version: 4.5
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,6 +1,6 @@
1
1
  crystalwindow/FileHelper.py,sha256=aUnnRG7UwvzJt-idjWjmpwy3RM6nqLlC3-7Bae6Yb94,5471
2
- crystalwindow/__init__.py,sha256=a2kdMZ29QZ4kSQ3M8jLvCR6g3OUQxNhdaT3ycxoames,2264
3
- crystalwindow/ai.py,sha256=k-3MVOKZwAIMLB6ifyR0Gcuo4Fh0-JvoDVA9_j-PJSQ,13293
2
+ crystalwindow/__init__.py,sha256=nOEUvoB4OpcYq_le1Kee0Uef3Jq38bYYapOI1u1CN2E,2457
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
@@ -8,6 +8,7 @@ crystalwindow/clock.py,sha256=EAcNvxonD7ktkKSIbuVHqzhPhQqtUQtleMSBB_OuUQY,541
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.3.dist-info/licenses/LICENSE,sha256=Gt5cJRchdNt0guxyQMHKsATN5PM5mjuDhdO6Gzs9qQc,1096
37
- crystalwindow-4.3.dist-info/METADATA,sha256=gYbWholGWviaRGXpVc1zaOZ0wQ6KWB9lvw2ZdWbBDrY,7338
38
- crystalwindow-4.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
39
- crystalwindow-4.3.dist-info/top_level.txt,sha256=PeQSld4b19XWT-zvbYkvE2Xg8sakIMbDzSzSdOSRN8o,14
40
- crystalwindow-4.3.dist-info/RECORD,,
38
+ crystalwindow-4.5.dist-info/licenses/LICENSE,sha256=Gt5cJRchdNt0guxyQMHKsATN5PM5mjuDhdO6Gzs9qQc,1096
39
+ crystalwindow-4.5.dist-info/METADATA,sha256=nWirrrZxkZMaUYGi8BbZQ3fGFCh0d0OZTtLReJHzYls,7338
40
+ crystalwindow-4.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
41
+ crystalwindow-4.5.dist-info/top_level.txt,sha256=PeQSld4b19XWT-zvbYkvE2Xg8sakIMbDzSzSdOSRN8o,14
42
+ crystalwindow-4.5.dist-info/RECORD,,