fastled 1.2.23__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.
- fastled/__init__.py +352 -0
- fastled/app.py +107 -0
- fastled/assets/example.txt +1 -0
- fastled/cli.py +19 -0
- fastled/client_server.py +401 -0
- fastled/compile_server.py +92 -0
- fastled/compile_server_impl.py +247 -0
- fastled/docker_manager.py +784 -0
- fastled/filewatcher.py +202 -0
- fastled/keyboard.py +116 -0
- fastled/live_client.py +86 -0
- fastled/open_browser.py +161 -0
- fastled/open_browser2.py +111 -0
- fastled/parse_args.py +195 -0
- fastled/paths.py +4 -0
- fastled/project_init.py +129 -0
- fastled/select_sketch_directory.py +35 -0
- fastled/settings.py +13 -0
- fastled/site/build.py +457 -0
- fastled/sketch.py +97 -0
- fastled/spinner.py +34 -0
- fastled/string_diff.py +42 -0
- fastled/test/can_run_local_docker_tests.py +13 -0
- fastled/test/examples.py +49 -0
- fastled/types.py +61 -0
- fastled/util.py +10 -0
- fastled/web_compile.py +285 -0
- fastled-1.2.23.dist-info/LICENSE +21 -0
- fastled-1.2.23.dist-info/METADATA +382 -0
- fastled-1.2.23.dist-info/RECORD +33 -0
- fastled-1.2.23.dist-info/WHEEL +5 -0
- fastled-1.2.23.dist-info/entry_points.txt +4 -0
- fastled-1.2.23.dist-info/top_level.txt +1 -0
fastled/site/build.py
ADDED
@@ -0,0 +1,457 @@
|
|
1
|
+
import argparse
|
2
|
+
import subprocess
|
3
|
+
from pathlib import Path
|
4
|
+
from shutil import copytree, rmtree, which
|
5
|
+
|
6
|
+
CSS_CONTENT = """
|
7
|
+
/* CSS Reset & Variables */
|
8
|
+
*, *::before, *::after {
|
9
|
+
box-sizing: border-box;
|
10
|
+
margin: 0;
|
11
|
+
padding: 0;
|
12
|
+
}
|
13
|
+
|
14
|
+
:root {
|
15
|
+
--color-background: #121212;
|
16
|
+
--color-surface: #252525;
|
17
|
+
--color-surface-transparent: rgba(30, 30, 30, 0.95);
|
18
|
+
--color-text: #E0E0E0;
|
19
|
+
--spacing-sm: 5px;
|
20
|
+
--spacing-md: 10px;
|
21
|
+
--spacing-lg: 15px;
|
22
|
+
--transition-speed: 0.3s;
|
23
|
+
--font-family: 'Roboto Condensed', sans-serif;
|
24
|
+
--nav-width: 250px;
|
25
|
+
--border-radius: 5px;
|
26
|
+
}
|
27
|
+
|
28
|
+
/* Base Styles */
|
29
|
+
body {
|
30
|
+
background-color: var(--color-background);
|
31
|
+
color: var(--color-text);
|
32
|
+
margin: 0;
|
33
|
+
padding: 0;
|
34
|
+
font-family: var(--font-family);
|
35
|
+
min-height: 100vh;
|
36
|
+
display: grid;
|
37
|
+
grid-template-rows: 1fr;
|
38
|
+
}
|
39
|
+
|
40
|
+
/* Splash Screen */
|
41
|
+
.splash-screen {
|
42
|
+
position: fixed;
|
43
|
+
inset: 0;
|
44
|
+
background-color: var(--color-background);
|
45
|
+
display: flex;
|
46
|
+
justify-content: center;
|
47
|
+
align-items: center;
|
48
|
+
z-index: 2000;
|
49
|
+
transition: opacity var(--transition-speed) ease-out;
|
50
|
+
}
|
51
|
+
|
52
|
+
.splash-text {
|
53
|
+
font-size: 14vw;
|
54
|
+
color: var(--color-text);
|
55
|
+
font-weight: 300;
|
56
|
+
font-family: var(--font-family);
|
57
|
+
opacity: 0;
|
58
|
+
transition: opacity var(--transition-speed) ease-in;
|
59
|
+
}
|
60
|
+
|
61
|
+
/* Layout */
|
62
|
+
.content-wrapper {
|
63
|
+
position: relative;
|
64
|
+
width: 100%;
|
65
|
+
height: 100vh;
|
66
|
+
overflow-x: hidden;
|
67
|
+
}
|
68
|
+
|
69
|
+
/* Navigation */
|
70
|
+
.nav-trigger {
|
71
|
+
position: fixed;
|
72
|
+
left: var(--spacing-md);
|
73
|
+
top: var(--spacing-md);
|
74
|
+
padding: 15px 30px;
|
75
|
+
z-index: 1001;
|
76
|
+
background-color: var(--color-surface);
|
77
|
+
border-radius: var(--border-radius);
|
78
|
+
display: flex;
|
79
|
+
align-items: center;
|
80
|
+
justify-content: center;
|
81
|
+
cursor: pointer;
|
82
|
+
color: var(--color-text);
|
83
|
+
font-size: 24px;
|
84
|
+
transition: background-color var(--transition-speed) ease;
|
85
|
+
}
|
86
|
+
|
87
|
+
.nav-trigger:hover {
|
88
|
+
background-color: var(--color-surface-transparent);
|
89
|
+
}
|
90
|
+
|
91
|
+
.nav-trigger .fa-chevron-down {
|
92
|
+
margin-left: 10px;
|
93
|
+
transition: transform var(--transition-speed) ease;
|
94
|
+
}
|
95
|
+
|
96
|
+
.nav-pane.visible + .nav-trigger .fa-chevron-down {
|
97
|
+
transform: rotate(180deg);
|
98
|
+
}
|
99
|
+
|
100
|
+
.nav-pane {
|
101
|
+
position: fixed;
|
102
|
+
left: var(--spacing-md);
|
103
|
+
top: 60px;
|
104
|
+
width: var(--nav-width);
|
105
|
+
height: auto;
|
106
|
+
background-color: var(--color-surface-transparent);
|
107
|
+
border-radius: var(--border-radius);
|
108
|
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
|
109
|
+
transform: translateY(-20px);
|
110
|
+
opacity: 0;
|
111
|
+
pointer-events: none;
|
112
|
+
transition: transform var(--transition-speed) ease,
|
113
|
+
opacity var(--transition-speed) ease;
|
114
|
+
}
|
115
|
+
|
116
|
+
.nav-pane.visible {
|
117
|
+
transform: translateY(0);
|
118
|
+
opacity: 1;
|
119
|
+
pointer-events: auto;
|
120
|
+
}
|
121
|
+
|
122
|
+
/* Main Content */
|
123
|
+
.main-content {
|
124
|
+
width: 100%;
|
125
|
+
height: 100%;
|
126
|
+
padding: 0;
|
127
|
+
overflow: hidden;
|
128
|
+
}
|
129
|
+
|
130
|
+
#example-frame {
|
131
|
+
width: 100%;
|
132
|
+
height: 100%;
|
133
|
+
border: none;
|
134
|
+
background-color: var(--color-background);
|
135
|
+
overflow: auto;
|
136
|
+
}
|
137
|
+
|
138
|
+
/* Example Links */
|
139
|
+
.example-link {
|
140
|
+
margin: var(--spacing-sm) var(--spacing-md);
|
141
|
+
padding: var(--spacing-lg) var(--spacing-md);
|
142
|
+
border-radius: var(--border-radius);
|
143
|
+
display: block;
|
144
|
+
text-decoration: none;
|
145
|
+
color: var(--color-text);
|
146
|
+
background-color: var(--color-surface);
|
147
|
+
transition: background-color var(--transition-speed) ease-in-out,
|
148
|
+
box-shadow var(--transition-speed) ease-in-out;
|
149
|
+
position: relative;
|
150
|
+
padding-right: 35px;
|
151
|
+
}
|
152
|
+
|
153
|
+
.example-link:hover {
|
154
|
+
background-color: var(--color-surface-transparent);
|
155
|
+
box-shadow: var(--shadow-hover, 0 0 10px rgba(255, 255, 255, 0.1));
|
156
|
+
}
|
157
|
+
|
158
|
+
.example-link:last-child {
|
159
|
+
margin-bottom: var(--spacing-md);
|
160
|
+
}
|
161
|
+
|
162
|
+
/* Accessibility */
|
163
|
+
@media (prefers-reduced-motion: reduce) {
|
164
|
+
*, *::before, *::after {
|
165
|
+
animation-duration: 0.01ms !important;
|
166
|
+
animation-iteration-count: 1 !important;
|
167
|
+
transition-duration: 0.01ms !important;
|
168
|
+
scroll-behavior: auto !important;
|
169
|
+
}
|
170
|
+
}
|
171
|
+
"""
|
172
|
+
|
173
|
+
INDEX_TEMPLATE = """<!DOCTYPE html>
|
174
|
+
<html lang="en">
|
175
|
+
<head>
|
176
|
+
<meta charset="UTF-8">
|
177
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
178
|
+
<title>FastLED Examples</title>
|
179
|
+
<link href="https://fonts.googleapis.com/css2?family=Roboto+Condensed:wght@300&display=swap" rel="stylesheet">
|
180
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
181
|
+
<link rel="stylesheet" href="index.css">
|
182
|
+
</head>
|
183
|
+
<body>
|
184
|
+
<div class="splash-screen">
|
185
|
+
<div class="splash-text">FastLED</div>
|
186
|
+
</div>
|
187
|
+
<div class="content-wrapper">
|
188
|
+
<div class="nav-trigger">Examples <i class="fas fa-chevron-down"></i></div>
|
189
|
+
<nav class="nav-pane">
|
190
|
+
{example_links}
|
191
|
+
</nav>
|
192
|
+
<main class="main-content">
|
193
|
+
<iframe id="example-frame" title="Example Content"></iframe>
|
194
|
+
</main>
|
195
|
+
</div>
|
196
|
+
<script>
|
197
|
+
document.addEventListener('DOMContentLoaded', function() {{
|
198
|
+
const splashScreen = document.querySelector('.splash-screen');
|
199
|
+
const splashText = document.querySelector('.splash-text');
|
200
|
+
|
201
|
+
// Ensure splash screen always gets removed
|
202
|
+
const removeSplashScreen = () => {{
|
203
|
+
splashScreen.style.opacity = '0';
|
204
|
+
setTimeout(() => {{
|
205
|
+
splashScreen.style.display = 'none';
|
206
|
+
}}, 500);
|
207
|
+
}};
|
208
|
+
|
209
|
+
// Set a maximum time the splash screen can stay
|
210
|
+
const maxSplashTime = setTimeout(removeSplashScreen, 2000); // Reduced from 5000ms to 2000ms
|
211
|
+
|
212
|
+
// Try to do nice fade-in/fade-out when possible
|
213
|
+
try {{
|
214
|
+
// Add a fallback timer in case font loading fails silently
|
215
|
+
const fontTimeout = setTimeout(() => {{
|
216
|
+
splashText.style.opacity = '1';
|
217
|
+
setTimeout(removeSplashScreen, 1000);
|
218
|
+
}}, 1000);
|
219
|
+
|
220
|
+
Promise.all([
|
221
|
+
// Wrap font loading in a timeout promise
|
222
|
+
Promise.race([
|
223
|
+
document.fonts.ready,
|
224
|
+
new Promise((_, reject) => setTimeout(reject, 1500))
|
225
|
+
]),
|
226
|
+
new Promise(resolve => {{
|
227
|
+
if (document.readyState === 'complete') {{
|
228
|
+
resolve();
|
229
|
+
}} else {{
|
230
|
+
window.addEventListener('load', resolve);
|
231
|
+
}}
|
232
|
+
}})
|
233
|
+
]).then(() => {{
|
234
|
+
clearTimeout(maxSplashTime);
|
235
|
+
clearTimeout(fontTimeout);
|
236
|
+
splashText.style.opacity = '1';
|
237
|
+
setTimeout(removeSplashScreen, 1500);
|
238
|
+
}}).catch(() => {{
|
239
|
+
// If either promise fails, ensure splash screen is removed
|
240
|
+
clearTimeout(maxSplashTime);
|
241
|
+
removeSplashScreen();
|
242
|
+
}});
|
243
|
+
}} catch (e) {{
|
244
|
+
// Final fallback if anything goes wrong
|
245
|
+
console.warn('Splash screen error:', e);
|
246
|
+
removeSplashScreen();
|
247
|
+
}}
|
248
|
+
const links = document.querySelectorAll('.example-link');
|
249
|
+
const iframe = document.getElementById('example-frame');
|
250
|
+
const navPane = document.querySelector('.nav-pane');
|
251
|
+
const navTrigger = document.querySelector('.nav-trigger');
|
252
|
+
|
253
|
+
// First add checkmarks to all links
|
254
|
+
links.forEach(link => {{
|
255
|
+
// Add the checkmark span to each link
|
256
|
+
const checkmark = document.createElement('i');
|
257
|
+
checkmark.className = 'fas fa-check';
|
258
|
+
checkmark.style.display = 'none';
|
259
|
+
checkmark.style.position = 'absolute';
|
260
|
+
checkmark.style.right = '10px';
|
261
|
+
checkmark.style.top = '50%';
|
262
|
+
checkmark.style.transform = 'translateY(-50%)';
|
263
|
+
checkmark.style.color = '#E0E0E0';
|
264
|
+
link.appendChild(checkmark);
|
265
|
+
}});
|
266
|
+
|
267
|
+
// Now load first example and show its checkmark
|
268
|
+
if (links.length > 0) {{
|
269
|
+
// Try to find SdCard example first
|
270
|
+
let startLink = Array.from(links).find(link => link.textContent === 'SdCard') || links[0];
|
271
|
+
iframe.src = startLink.getAttribute('href');
|
272
|
+
startLink.classList.add('active');
|
273
|
+
startLink.querySelector('.fa-check').style.display = 'inline-block';
|
274
|
+
}}
|
275
|
+
|
276
|
+
// Add click handlers
|
277
|
+
links.forEach(link => {{
|
278
|
+
link.addEventListener('click', function(e) {{
|
279
|
+
e.preventDefault();
|
280
|
+
// Hide all checkmarks
|
281
|
+
links.forEach(l => {{
|
282
|
+
l.querySelector('.fa-check').style.display = 'none';
|
283
|
+
l.classList.remove('active');
|
284
|
+
}});
|
285
|
+
// Show this checkmark
|
286
|
+
this.querySelector('.fa-check').style.display = 'inline-block';
|
287
|
+
this.classList.add('active');
|
288
|
+
iframe.src = this.getAttribute('href');
|
289
|
+
hideNav(); // Hide nav after selection
|
290
|
+
}});
|
291
|
+
}});
|
292
|
+
|
293
|
+
function showNav() {{
|
294
|
+
navPane.classList.add('visible');
|
295
|
+
navPane.style.opacity = '1';
|
296
|
+
}}
|
297
|
+
|
298
|
+
function hideNav() {{
|
299
|
+
navPane.style.opacity = '0'; // Start fade out
|
300
|
+
setTimeout(() => {{
|
301
|
+
navPane.classList.remove('visible');
|
302
|
+
}}, 300);
|
303
|
+
}}
|
304
|
+
|
305
|
+
// Click handlers for nav
|
306
|
+
navTrigger.addEventListener('click', (e) => {{
|
307
|
+
e.stopPropagation();
|
308
|
+
if (navPane.classList.contains('visible')) {{
|
309
|
+
hideNav();
|
310
|
+
}} else {{
|
311
|
+
showNav();
|
312
|
+
}}
|
313
|
+
}});
|
314
|
+
|
315
|
+
// Close menu when clicking anywhere in the document
|
316
|
+
document.addEventListener('click', (e) => {{
|
317
|
+
if (navPane.classList.contains('visible') &&
|
318
|
+
!navPane.contains(e.target) &&
|
319
|
+
!navTrigger.contains(e.target)) {{
|
320
|
+
hideNav();
|
321
|
+
}}
|
322
|
+
}});
|
323
|
+
|
324
|
+
// Close when clicking iframe
|
325
|
+
iframe.addEventListener('load', () => {{
|
326
|
+
iframe.contentDocument?.addEventListener('click', () => {{
|
327
|
+
if (navPane.classList.contains('visible')) {{
|
328
|
+
hideNav();
|
329
|
+
}}
|
330
|
+
}});
|
331
|
+
}});
|
332
|
+
|
333
|
+
// Initial state
|
334
|
+
hideNav();
|
335
|
+
}});
|
336
|
+
</script>
|
337
|
+
</body>
|
338
|
+
</html>
|
339
|
+
"""
|
340
|
+
|
341
|
+
|
342
|
+
EXAMPLES = [
|
343
|
+
"wasm",
|
344
|
+
"Chromancer",
|
345
|
+
"LuminescentGrand",
|
346
|
+
"FxSdCard",
|
347
|
+
"FxNoiseRing",
|
348
|
+
"FxWater",
|
349
|
+
]
|
350
|
+
|
351
|
+
|
352
|
+
def _exec(cmd: str) -> None:
|
353
|
+
subprocess.run(cmd, shell=True, check=True)
|
354
|
+
|
355
|
+
|
356
|
+
def build_example(example: str, outputdir: Path) -> None:
|
357
|
+
if not which("fastled"):
|
358
|
+
raise FileNotFoundError("fastled executable not found")
|
359
|
+
src_dir = outputdir / example / "src"
|
360
|
+
_exec(f"fastled --init={example} {src_dir}")
|
361
|
+
assert src_dir.exists()
|
362
|
+
_exec(f"fastled {src_dir / example} --just-compile")
|
363
|
+
fastled_dir = src_dir / example / "fastled_js"
|
364
|
+
assert fastled_dir.exists(), f"fastled dir {fastled_dir} not found"
|
365
|
+
# now copy it to the example dir
|
366
|
+
example_dir = outputdir / example
|
367
|
+
copytree(fastled_dir, example_dir, dirs_exist_ok=True)
|
368
|
+
# now remove the src dir
|
369
|
+
rmtree(src_dir, ignore_errors=True)
|
370
|
+
print(f"Built {example} example in {example_dir}")
|
371
|
+
assert (example_dir / "fastled.wasm").exists()
|
372
|
+
|
373
|
+
|
374
|
+
def generate_css(outputdir: Path) -> None:
|
375
|
+
css_file = outputdir / "index.css"
|
376
|
+
# with open(css_file, "w") as f:
|
377
|
+
# f.write(CSS_CONTENT, encoding="utf-8")
|
378
|
+
css_file.write_text(CSS_CONTENT, encoding="utf-8")
|
379
|
+
|
380
|
+
|
381
|
+
def build_index_html(outputdir: Path) -> None:
|
382
|
+
outputdir = outputdir
|
383
|
+
assert (
|
384
|
+
outputdir.exists()
|
385
|
+
), f"Output directory {outputdir} not found, you should run build_example first"
|
386
|
+
index_html = outputdir / "index.html"
|
387
|
+
|
388
|
+
examples = [f for f in outputdir.iterdir() if f.is_dir()]
|
389
|
+
examples = sorted(examples)
|
390
|
+
|
391
|
+
example_links = "\n".join(
|
392
|
+
f' <a class="example-link" href="{example.name}/index.html">{example.name}</a>'
|
393
|
+
for example in examples
|
394
|
+
)
|
395
|
+
|
396
|
+
with open(index_html, "w") as f:
|
397
|
+
f.write(INDEX_TEMPLATE.format(example_links=example_links))
|
398
|
+
|
399
|
+
|
400
|
+
def parse_args() -> argparse.Namespace:
|
401
|
+
parser = argparse.ArgumentParser(description="Build FastLED example site")
|
402
|
+
parser.add_argument(
|
403
|
+
"--outputdir", type=Path, help="Output directory", required=True
|
404
|
+
)
|
405
|
+
parser.add_argument(
|
406
|
+
"--fast",
|
407
|
+
action="store_true",
|
408
|
+
help="Skip regenerating existing examples, only rebuild index.html and CSS",
|
409
|
+
)
|
410
|
+
return parser.parse_args()
|
411
|
+
|
412
|
+
|
413
|
+
def build(outputdir: Path, fast: bool | None = None, check=False) -> list[Exception]:
|
414
|
+
outputdir = outputdir
|
415
|
+
fast = fast or False
|
416
|
+
errors: list[Exception] = []
|
417
|
+
|
418
|
+
for example in EXAMPLES:
|
419
|
+
example_dir = outputdir / example
|
420
|
+
if not fast or not example_dir.exists():
|
421
|
+
try:
|
422
|
+
build_example(example=example, outputdir=outputdir)
|
423
|
+
except Exception as e:
|
424
|
+
if check:
|
425
|
+
raise
|
426
|
+
errors.append(e)
|
427
|
+
|
428
|
+
try:
|
429
|
+
generate_css(outputdir=outputdir)
|
430
|
+
except Exception as e:
|
431
|
+
if check:
|
432
|
+
raise
|
433
|
+
errors.append(e)
|
434
|
+
|
435
|
+
try:
|
436
|
+
build_index_html(outputdir=outputdir)
|
437
|
+
except Exception as e:
|
438
|
+
if check:
|
439
|
+
raise
|
440
|
+
errors.append(e)
|
441
|
+
|
442
|
+
return errors
|
443
|
+
|
444
|
+
|
445
|
+
def main() -> int:
|
446
|
+
args = parse_args()
|
447
|
+
outputdir = args.outputdir
|
448
|
+
fast = args.fast
|
449
|
+
build(outputdir=outputdir, fast=fast)
|
450
|
+
return 0
|
451
|
+
|
452
|
+
|
453
|
+
if __name__ == "__main__":
|
454
|
+
import sys
|
455
|
+
|
456
|
+
sys.argv.append("--fast")
|
457
|
+
main()
|
fastled/sketch.py
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
import os
|
2
|
+
from pathlib import Path
|
3
|
+
|
4
|
+
_MAX_FILES_SEARCH_LIMIT = 10000
|
5
|
+
|
6
|
+
|
7
|
+
def find_sketch_directories(directory: Path) -> list[Path]:
|
8
|
+
file_count = 0
|
9
|
+
sketch_directories: list[Path] = []
|
10
|
+
# search all the paths one level deep
|
11
|
+
for path in directory.iterdir():
|
12
|
+
if path.is_dir():
|
13
|
+
dir_name = path.name
|
14
|
+
if str(dir_name).startswith("."):
|
15
|
+
continue
|
16
|
+
file_count += 1
|
17
|
+
if file_count > _MAX_FILES_SEARCH_LIMIT:
|
18
|
+
print(
|
19
|
+
f"More than {_MAX_FILES_SEARCH_LIMIT} files found. Stopping search."
|
20
|
+
)
|
21
|
+
break
|
22
|
+
|
23
|
+
if looks_like_sketch_directory(path, quick=True):
|
24
|
+
sketch_directories.append(path)
|
25
|
+
if dir_name.lower() == "examples":
|
26
|
+
for example in path.iterdir():
|
27
|
+
if example.is_dir():
|
28
|
+
if looks_like_sketch_directory(example, quick=True):
|
29
|
+
sketch_directories.append(example)
|
30
|
+
# make relative to cwd
|
31
|
+
sketch_directories = [p.relative_to(directory) for p in sketch_directories]
|
32
|
+
return sketch_directories
|
33
|
+
|
34
|
+
|
35
|
+
def get_sketch_files(directory: Path) -> list[Path]:
|
36
|
+
file_count = 0
|
37
|
+
files: list[Path] = []
|
38
|
+
for root, dirs, filenames in os.walk(directory):
|
39
|
+
# ignore hidden directories
|
40
|
+
dirs[:] = [d for d in dirs if not d.startswith(".")]
|
41
|
+
# ignore fastled_js directory
|
42
|
+
dirs[:] = [d for d in dirs if "fastled_js" not in d]
|
43
|
+
# ignore hidden files
|
44
|
+
filenames = [f for f in filenames if not f.startswith(".")]
|
45
|
+
outer_break = False
|
46
|
+
for filename in filenames:
|
47
|
+
if "platformio.ini" in filename:
|
48
|
+
continue
|
49
|
+
file_count += 1
|
50
|
+
if file_count > _MAX_FILES_SEARCH_LIMIT:
|
51
|
+
print(
|
52
|
+
f"More than {_MAX_FILES_SEARCH_LIMIT} files found. Stopping search."
|
53
|
+
)
|
54
|
+
outer_break = True
|
55
|
+
break
|
56
|
+
files.append(Path(root) / filename)
|
57
|
+
if outer_break:
|
58
|
+
break
|
59
|
+
|
60
|
+
return files
|
61
|
+
|
62
|
+
|
63
|
+
def looks_like_fastled_repo(directory: Path) -> bool:
|
64
|
+
libprops = directory / "library.properties"
|
65
|
+
if not libprops.exists():
|
66
|
+
return False
|
67
|
+
txt = libprops.read_text(encoding="utf-8", errors="ignore")
|
68
|
+
return "FastLED" in txt
|
69
|
+
|
70
|
+
|
71
|
+
def _lots_and_lots_of_files(directory: Path) -> bool:
|
72
|
+
return len(get_sketch_files(directory)) > 100
|
73
|
+
|
74
|
+
|
75
|
+
def looks_like_sketch_directory(directory: Path, quick=False) -> bool:
|
76
|
+
if looks_like_fastled_repo(directory):
|
77
|
+
print("Directory looks like the FastLED repo")
|
78
|
+
return False
|
79
|
+
|
80
|
+
if not quick:
|
81
|
+
if _lots_and_lots_of_files(directory):
|
82
|
+
return False
|
83
|
+
|
84
|
+
# walk the path and if there are over 30 files, return False
|
85
|
+
# at the root of the directory there should either be an ino file or a src directory
|
86
|
+
# or some cpp files
|
87
|
+
# if there is a platformio.ini file, return True
|
88
|
+
ino_file_at_root = list(directory.glob("*.ino"))
|
89
|
+
if ino_file_at_root:
|
90
|
+
return True
|
91
|
+
cpp_file_at_root = list(directory.glob("*.cpp"))
|
92
|
+
if cpp_file_at_root:
|
93
|
+
return True
|
94
|
+
platformini_file = list(directory.glob("platformio.ini"))
|
95
|
+
if platformini_file:
|
96
|
+
return True
|
97
|
+
return False
|
fastled/spinner.py
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
import _thread
|
2
|
+
import threading
|
3
|
+
import time
|
4
|
+
import warnings
|
5
|
+
|
6
|
+
from progress.spinner import Spinner as SpinnerImpl
|
7
|
+
|
8
|
+
|
9
|
+
class Spinner:
|
10
|
+
def __init__(self, message: str = ""):
|
11
|
+
self.spinner = SpinnerImpl(message)
|
12
|
+
self.event = threading.Event()
|
13
|
+
self.thread = threading.Thread(target=self._spin, daemon=True)
|
14
|
+
self.thread.start()
|
15
|
+
|
16
|
+
def _spin(self) -> None:
|
17
|
+
try:
|
18
|
+
while not self.event.is_set():
|
19
|
+
self.spinner.next()
|
20
|
+
time.sleep(0.1)
|
21
|
+
except KeyboardInterrupt:
|
22
|
+
_thread.interrupt_main()
|
23
|
+
except Exception as e:
|
24
|
+
warnings.warn(f"Spinner thread failed: {e}")
|
25
|
+
|
26
|
+
def stop(self) -> None:
|
27
|
+
self.event.set()
|
28
|
+
self.thread.join()
|
29
|
+
|
30
|
+
def __enter__(self):
|
31
|
+
return self
|
32
|
+
|
33
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
34
|
+
self.stop()
|
fastled/string_diff.py
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
|
3
|
+
from rapidfuzz import fuzz
|
4
|
+
|
5
|
+
|
6
|
+
# Returns the min distance strings. If there is a tie, it returns
|
7
|
+
# all the strings that have the same min distance.
|
8
|
+
# Returns a tuple of index and string.
|
9
|
+
def string_diff(
|
10
|
+
input_string: str, string_list: list[str], ignore_case=True
|
11
|
+
) -> list[tuple[float, str]]:
|
12
|
+
|
13
|
+
def normalize(s: str) -> str:
|
14
|
+
return s.lower() if ignore_case else s
|
15
|
+
|
16
|
+
# distances = [
|
17
|
+
# #Levenshtein.distance(normalize(input_string), normalize(s)) for s in string_list
|
18
|
+
# fuzz.partial_ratio(normalize(input_string), normalize(s)) for s in string_list
|
19
|
+
# ]
|
20
|
+
distances: list[float] = []
|
21
|
+
for s in string_list:
|
22
|
+
dist = fuzz.token_sort_ratio(normalize(input_string), normalize(s))
|
23
|
+
distances.append(1.0 / (dist + 1.0))
|
24
|
+
min_distance = min(distances)
|
25
|
+
out: list[tuple[float, str]] = []
|
26
|
+
for i, d in enumerate(distances):
|
27
|
+
if d == min_distance:
|
28
|
+
out.append((i, string_list[i]))
|
29
|
+
|
30
|
+
return out
|
31
|
+
|
32
|
+
|
33
|
+
def string_diff_paths(
|
34
|
+
input_string: str | Path, path_list: list[Path], ignore_case=True
|
35
|
+
) -> list[tuple[float, Path]]:
|
36
|
+
string_list = [str(p) for p in path_list]
|
37
|
+
tmp = string_diff(str(input_string), string_list, ignore_case)
|
38
|
+
out: list[tuple[float, Path]] = []
|
39
|
+
for i, j in tmp:
|
40
|
+
p = Path(j)
|
41
|
+
out.append((i, p))
|
42
|
+
return out
|
@@ -0,0 +1,13 @@
|
|
1
|
+
import os
|
2
|
+
import platform
|
3
|
+
|
4
|
+
|
5
|
+
def can_run_local_docker_tests() -> bool:
|
6
|
+
"""Check if this system can run Docker Tests"""
|
7
|
+
is_github_runner = "GITHUB_ACTIONS" in os.environ
|
8
|
+
if not is_github_runner:
|
9
|
+
from fastled.docker_manager import DockerManager
|
10
|
+
|
11
|
+
return DockerManager.is_docker_installed()
|
12
|
+
# this only works in ubuntu at the moment
|
13
|
+
return platform.system() == "Linux"
|
fastled/test/examples.py
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
from tempfile import TemporaryDirectory
|
2
|
+
from time import time
|
3
|
+
from warnings import warn
|
4
|
+
|
5
|
+
_FILTER = True
|
6
|
+
|
7
|
+
|
8
|
+
def test_examples(
|
9
|
+
examples: list[str] | None = None, host: str | None = None
|
10
|
+
) -> dict[str, Exception]:
|
11
|
+
"""Test the examples in the given directory."""
|
12
|
+
from fastled import Api
|
13
|
+
|
14
|
+
out: dict[str, Exception] = {}
|
15
|
+
examples = Api.get_examples(host=host) if examples is None else examples
|
16
|
+
if host is None and _FILTER:
|
17
|
+
examples.remove("Chromancer") # Brutal
|
18
|
+
examples.remove("LuminescentGrand")
|
19
|
+
with TemporaryDirectory() as tmpdir:
|
20
|
+
for example in examples:
|
21
|
+
print(f"Initializing example: {example}")
|
22
|
+
try:
|
23
|
+
sketch_dir = Api.project_init(example, outputdir=tmpdir, host=host)
|
24
|
+
except Exception as e:
|
25
|
+
warn(f"Failed to initialize example: {example}, error: {e}")
|
26
|
+
out[example] = e
|
27
|
+
continue
|
28
|
+
print(f"Project initialized at: {sketch_dir}")
|
29
|
+
start = time()
|
30
|
+
print(f"Compiling example: {example}")
|
31
|
+
diff = time() - start
|
32
|
+
print(f"Compilation took: {diff:.2f} seconds")
|
33
|
+
result = Api.web_compile(sketch_dir, host=host)
|
34
|
+
if not result.success:
|
35
|
+
out[example] = Exception(result.stdout)
|
36
|
+
return out
|
37
|
+
|
38
|
+
|
39
|
+
def unit_test() -> None:
|
40
|
+
from fastled import Api
|
41
|
+
|
42
|
+
with Api.server(auto_updates=True) as server:
|
43
|
+
out = test_examples(host=server.url())
|
44
|
+
if out:
|
45
|
+
raise RuntimeError(f"Failed tests: {out}")
|
46
|
+
|
47
|
+
|
48
|
+
if __name__ == "__main__":
|
49
|
+
unit_test()
|