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 +17 -4
- crystalwindow/ai.py +105 -157
- crystalwindow/clock.py +179 -13
- crystalwindow/cw_client.py +95 -0
- crystalwindow/websearch.py +233 -0
- crystalwindow/window.py +6 -1
- {crystalwindow-4.4.dist-info → crystalwindow-4.6.dist-info}/METADATA +1 -1
- {crystalwindow-4.4.dist-info → crystalwindow-4.6.dist-info}/RECORD +11 -9
- {crystalwindow-4.4.dist-info → crystalwindow-4.6.dist-info}/WHEEL +0 -0
- {crystalwindow-4.4.dist-info → crystalwindow-4.6.dist-info}/licenses/LICENSE +0 -0
- {crystalwindow-4.4.dist-info → crystalwindow-4.6.dist-info}/top_level.txt +0 -0
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
|
-
|
|
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",
|
|
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.
|
|
2
|
+
# CrystalAI v0.7 — Stabilized & 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
|
|
14
|
-
|
|
15
|
-
|
|
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-
|
|
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,
|
|
37
|
-
"
|
|
32
|
+
"debugging, error analysis, documentation, and file analysis. "
|
|
33
|
+
"Be friendly, technical, clear, and precise."
|
|
38
34
|
)
|
|
39
|
-
|
|
40
|
-
PLACEHOLDER_KEY = "
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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("
|
|
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("
|
|
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
|
-
|
|
107
|
-
with open(
|
|
108
|
-
|
|
109
|
-
|
|
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(
|
|
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
|
|
126
|
-
return "[CrystalAI]
|
|
129
|
+
except:
|
|
130
|
+
return "[CrystalAI] could not read file."
|
|
127
131
|
|
|
128
132
|
# ==========================================================
|
|
129
|
-
# PROMPT BUILDER
|
|
133
|
+
# PROMPT BUILDER
|
|
130
134
|
# ==========================================================
|
|
131
|
-
def _build_prompt(self, user_text, file_data
|
|
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
|
-
#
|
|
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
|
|
163
|
+
def _local_ai(self, user_text, file_data):
|
|
186
164
|
"""
|
|
187
|
-
|
|
165
|
+
Much cleaner, more reliable fallback engine.
|
|
188
166
|
"""
|
|
189
|
-
output = ["[Local/SymbolicEngine] Processing request..."]
|
|
190
|
-
lower_prompt = prompt.lower()
|
|
191
167
|
|
|
192
|
-
# ---
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
f"
|
|
205
|
-
f"•
|
|
206
|
-
f"
|
|
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
|
|
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
|
-
#
|
|
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=
|
|
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
|
-
|
|
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
|
|
275
|
-
|
|
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
|
-
#
|
|
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
|
|
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
|
-
|
|
308
|
-
return CrystalAIResponse(
|
|
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,
|
|
266
|
+
def _simple_fix(self, src, err):
|
|
314
267
|
notes = []
|
|
315
268
|
lines = src.splitlines()
|
|
316
|
-
msg = getattr(
|
|
317
|
-
lineno =
|
|
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
|
-
|
|
323
|
-
if not
|
|
324
|
-
lines[lineno - 1] =
|
|
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
|
|
282
|
+
except:
|
|
331
283
|
pass
|
|
332
284
|
|
|
333
|
-
|
|
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
|
|
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
|
|
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
|
|
309
|
+
out.append(f"{mark} {i+1:4}: {lines[i]}")
|
|
362
310
|
return "\n".join(out)
|
|
363
311
|
|
|
364
312
|
# ==========================================================
|
|
365
|
-
# END OF
|
|
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
|
-
|
|
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.
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
10
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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.
|
|
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=
|
|
3
|
-
crystalwindow/ai.py,sha256=
|
|
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=
|
|
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/
|
|
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.
|
|
37
|
-
crystalwindow-4.
|
|
38
|
-
crystalwindow-4.
|
|
39
|
-
crystalwindow-4.
|
|
40
|
-
crystalwindow-4.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|