fastled 1.1.45__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 +128 -0
- fastled/app.py +67 -0
- fastled/assets/example.txt +1 -0
- fastled/cli.py +16 -0
- fastled/client_server.py +351 -0
- fastled/compile_server.py +87 -0
- fastled/compile_server_impl.py +251 -0
- fastled/docker_manager.py +665 -0
- fastled/filewatcher.py +202 -0
- fastled/keyboard.py +113 -0
- fastled/live_client.py +69 -0
- fastled/open_browser.py +59 -0
- fastled/parse_args.py +172 -0
- fastled/paths.py +4 -0
- fastled/project_init.py +76 -0
- fastled/select_sketch_directory.py +35 -0
- fastled/settings.py +9 -0
- fastled/sketch.py +97 -0
- fastled/spinner.py +34 -0
- fastled/string_diff.py +42 -0
- fastled/test/examples.py +31 -0
- fastled/types.py +61 -0
- fastled/util.py +10 -0
- fastled/web_compile.py +285 -0
- fastled-1.1.45.dist-info/LICENSE +21 -0
- fastled-1.1.45.dist-info/METADATA +323 -0
- fastled-1.1.45.dist-info/RECORD +30 -0
- fastled-1.1.45.dist-info/WHEEL +5 -0
- fastled-1.1.45.dist-info/entry_points.txt +4 -0
- fastled-1.1.45.dist-info/top_level.txt +1 -0
fastled/filewatcher.py
ADDED
@@ -0,0 +1,202 @@
|
|
1
|
+
"""File system watcher implementation using watchdog
|
2
|
+
"""
|
3
|
+
|
4
|
+
import hashlib
|
5
|
+
import os
|
6
|
+
import queue
|
7
|
+
import threading
|
8
|
+
import time
|
9
|
+
from contextlib import redirect_stdout
|
10
|
+
from multiprocessing import Process, Queue
|
11
|
+
from pathlib import Path
|
12
|
+
from queue import Empty
|
13
|
+
from typing import Dict, Set
|
14
|
+
|
15
|
+
from watchdog.events import FileSystemEvent, FileSystemEventHandler
|
16
|
+
from watchdog.observers import Observer
|
17
|
+
from watchdog.observers.api import BaseObserver
|
18
|
+
|
19
|
+
|
20
|
+
class MyEventHandler(FileSystemEventHandler):
|
21
|
+
def __init__(
|
22
|
+
self,
|
23
|
+
change_queue: queue.Queue,
|
24
|
+
excluded_patterns: Set[str],
|
25
|
+
file_hashes: Dict[str, str],
|
26
|
+
) -> None:
|
27
|
+
super().__init__()
|
28
|
+
self.change_queue = change_queue
|
29
|
+
self.excluded_patterns = excluded_patterns
|
30
|
+
self.file_hashes = file_hashes
|
31
|
+
|
32
|
+
def _get_file_hash(self, filepath: str) -> str:
|
33
|
+
try:
|
34
|
+
with open(filepath, "rb") as f:
|
35
|
+
return hashlib.md5(f.read()).hexdigest()
|
36
|
+
except Exception: # pylint: disable=broad-except
|
37
|
+
return ""
|
38
|
+
|
39
|
+
def on_modified(self, event: FileSystemEvent) -> None:
|
40
|
+
if not event.is_directory:
|
41
|
+
# Convert src_path to str if it's bytes
|
42
|
+
src_path = (
|
43
|
+
event.src_path.decode()
|
44
|
+
if isinstance(event.src_path, bytes)
|
45
|
+
else event.src_path
|
46
|
+
)
|
47
|
+
path = Path(src_path)
|
48
|
+
# Check if any part of the path matches excluded patterns
|
49
|
+
if not any(part in self.excluded_patterns for part in path.parts):
|
50
|
+
new_hash = self._get_file_hash(src_path)
|
51
|
+
if new_hash and new_hash != self.file_hashes.get(src_path):
|
52
|
+
self.file_hashes[src_path] = new_hash
|
53
|
+
self.change_queue.put(src_path)
|
54
|
+
|
55
|
+
|
56
|
+
class FileChangedNotifier(threading.Thread):
|
57
|
+
"""Watches a directory for file changes and queues notifications"""
|
58
|
+
|
59
|
+
def __init__(
|
60
|
+
self,
|
61
|
+
path: str,
|
62
|
+
debounce_seconds: float = 1.0,
|
63
|
+
excluded_patterns: list[str] | None = None,
|
64
|
+
) -> None:
|
65
|
+
"""Initialize the notifier with a path to watch
|
66
|
+
|
67
|
+
Args:
|
68
|
+
path: Directory path to watch for changes
|
69
|
+
debounce_seconds: Minimum time between notifications for the same file
|
70
|
+
excluded_patterns: List of directory/file patterns to exclude from watching
|
71
|
+
"""
|
72
|
+
super().__init__(daemon=True)
|
73
|
+
self.path = path
|
74
|
+
self.observer: BaseObserver | None = None
|
75
|
+
self.event_handler: MyEventHandler | None = None
|
76
|
+
|
77
|
+
# Combine default and user-provided patterns
|
78
|
+
self.excluded_patterns = (
|
79
|
+
set(excluded_patterns) if excluded_patterns is not None else set()
|
80
|
+
)
|
81
|
+
self.stopped = False
|
82
|
+
self.change_queue: queue.Queue = queue.Queue()
|
83
|
+
self.last_notification: Dict[str, float] = {}
|
84
|
+
self.file_hashes: Dict[str, str] = {}
|
85
|
+
self.debounce_seconds = debounce_seconds
|
86
|
+
|
87
|
+
def stop(self) -> None:
|
88
|
+
"""Stop watching for changes"""
|
89
|
+
print("watcher stop")
|
90
|
+
self.stopped = True
|
91
|
+
if self.observer:
|
92
|
+
self.observer.stop()
|
93
|
+
self.observer.join()
|
94
|
+
self.observer = None
|
95
|
+
self.event_handler = None
|
96
|
+
|
97
|
+
def run(self) -> None:
|
98
|
+
"""Thread main loop - starts watching for changes"""
|
99
|
+
self.event_handler = MyEventHandler(
|
100
|
+
self.change_queue, self.excluded_patterns, self.file_hashes
|
101
|
+
)
|
102
|
+
self.observer = Observer()
|
103
|
+
self.observer.schedule(self.event_handler, self.path, recursive=True)
|
104
|
+
self.observer.start()
|
105
|
+
|
106
|
+
try:
|
107
|
+
while not self.stopped:
|
108
|
+
time.sleep(0.1)
|
109
|
+
except KeyboardInterrupt:
|
110
|
+
print("File watcher stopped by user.")
|
111
|
+
finally:
|
112
|
+
self.stop()
|
113
|
+
|
114
|
+
def get_next_change(self, timeout: float = 0.001) -> str | None:
|
115
|
+
"""Get the next file change event from the queue
|
116
|
+
|
117
|
+
Args:
|
118
|
+
timeout: How long to wait for next change in seconds
|
119
|
+
|
120
|
+
Returns:
|
121
|
+
Changed filepath or None if no change within timeout
|
122
|
+
"""
|
123
|
+
try:
|
124
|
+
filepath = self.change_queue.get(timeout=timeout)
|
125
|
+
current_time = time.time()
|
126
|
+
|
127
|
+
# Check if we've seen this file recently
|
128
|
+
last_time = self.last_notification.get(filepath, 0)
|
129
|
+
if current_time - last_time < self.debounce_seconds:
|
130
|
+
return None
|
131
|
+
|
132
|
+
self.last_notification[filepath] = current_time
|
133
|
+
return filepath
|
134
|
+
except KeyboardInterrupt:
|
135
|
+
raise
|
136
|
+
except queue.Empty:
|
137
|
+
return None
|
138
|
+
|
139
|
+
def get_all_changes(self, timeout: float = 0.001) -> list[str]:
|
140
|
+
"""Get all file change events from the queue
|
141
|
+
|
142
|
+
Args:
|
143
|
+
timeout: How long to wait for next change in seconds
|
144
|
+
|
145
|
+
Returns:
|
146
|
+
List of changed filepaths
|
147
|
+
"""
|
148
|
+
changed_files = []
|
149
|
+
while True:
|
150
|
+
changed_file = self.get_next_change(timeout=timeout)
|
151
|
+
if changed_file is None:
|
152
|
+
break
|
153
|
+
changed_files.append(changed_file)
|
154
|
+
# clear all the changes from the queue
|
155
|
+
self.change_queue.queue.clear()
|
156
|
+
return changed_files
|
157
|
+
|
158
|
+
|
159
|
+
def _process_wrapper(root: Path, excluded_patterns: list[str], queue: Queue):
|
160
|
+
with open(os.devnull, "w") as fnull: # Redirect to /dev/null
|
161
|
+
with redirect_stdout(fnull):
|
162
|
+
watcher = FileChangedNotifier(
|
163
|
+
str(root), excluded_patterns=excluded_patterns
|
164
|
+
)
|
165
|
+
watcher.start()
|
166
|
+
while True:
|
167
|
+
try:
|
168
|
+
changed_files = watcher.get_all_changes()
|
169
|
+
for file in changed_files:
|
170
|
+
queue.put(file)
|
171
|
+
except KeyboardInterrupt:
|
172
|
+
break
|
173
|
+
watcher.stop()
|
174
|
+
|
175
|
+
|
176
|
+
class FileWatcherProcess:
|
177
|
+
def __init__(self, root: Path, excluded_patterns: list[str]) -> None:
|
178
|
+
self.queue: Queue = Queue()
|
179
|
+
self.process = Process(
|
180
|
+
target=_process_wrapper,
|
181
|
+
args=(root, excluded_patterns, self.queue),
|
182
|
+
daemon=True,
|
183
|
+
)
|
184
|
+
self.process.start()
|
185
|
+
|
186
|
+
def stop(self):
|
187
|
+
self.process.terminate()
|
188
|
+
self.process.join()
|
189
|
+
self.queue.close()
|
190
|
+
self.queue.join_thread()
|
191
|
+
|
192
|
+
def get_all_changes(self, timeout: float | None = None) -> list[str]:
|
193
|
+
changed_files = []
|
194
|
+
block = timeout is not None
|
195
|
+
|
196
|
+
while True:
|
197
|
+
try:
|
198
|
+
changed_file = self.queue.get(block=block, timeout=timeout)
|
199
|
+
changed_files.append(changed_file)
|
200
|
+
except Empty:
|
201
|
+
break
|
202
|
+
return changed_files
|
fastled/keyboard.py
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
import os
|
2
|
+
import select
|
3
|
+
import sys
|
4
|
+
import time
|
5
|
+
from queue import Empty, Queue
|
6
|
+
from threading import Thread
|
7
|
+
|
8
|
+
_WHITE_SPACE = [" ", "\r", "\n"]
|
9
|
+
|
10
|
+
|
11
|
+
# Original space bar, but now also enter key.
|
12
|
+
class SpaceBarWatcher:
|
13
|
+
|
14
|
+
@classmethod
|
15
|
+
def watch_space_bar_pressed(cls, timeout: float = 0) -> bool:
|
16
|
+
watcher = cls()
|
17
|
+
try:
|
18
|
+
start_time = time.time()
|
19
|
+
while True:
|
20
|
+
if watcher.space_bar_pressed():
|
21
|
+
return True
|
22
|
+
if time.time() - start_time > timeout:
|
23
|
+
return False
|
24
|
+
finally:
|
25
|
+
watcher.stop()
|
26
|
+
|
27
|
+
def __init__(self) -> None:
|
28
|
+
self.queue: Queue = Queue()
|
29
|
+
self.queue_cancel: Queue = Queue()
|
30
|
+
self.process = Thread(target=self._watch_for_space, daemon=True)
|
31
|
+
self.process.start()
|
32
|
+
|
33
|
+
def _watch_for_space(self) -> None:
|
34
|
+
# Set stdin to non-blocking mode
|
35
|
+
fd = sys.stdin.fileno()
|
36
|
+
|
37
|
+
if os.name == "nt": # Windows
|
38
|
+
import msvcrt
|
39
|
+
|
40
|
+
while True:
|
41
|
+
# Check for cancel signal
|
42
|
+
try:
|
43
|
+
self.queue_cancel.get(timeout=0.1)
|
44
|
+
break
|
45
|
+
except Empty:
|
46
|
+
pass
|
47
|
+
|
48
|
+
# Check if there's input ready
|
49
|
+
if msvcrt.kbhit(): # type: ignore
|
50
|
+
char = msvcrt.getch().decode() # type: ignore
|
51
|
+
if char in _WHITE_SPACE:
|
52
|
+
self.queue.put(ord(" "))
|
53
|
+
|
54
|
+
else: # Unix-like systems
|
55
|
+
import termios
|
56
|
+
import tty
|
57
|
+
|
58
|
+
old_settings = termios.tcgetattr(fd) # type: ignore
|
59
|
+
try:
|
60
|
+
tty.setraw(fd) # type: ignore
|
61
|
+
while True:
|
62
|
+
# Check for cancel signal
|
63
|
+
try:
|
64
|
+
self.queue_cancel.get(timeout=0.1)
|
65
|
+
break
|
66
|
+
except Empty:
|
67
|
+
pass
|
68
|
+
|
69
|
+
# Check if there's input ready
|
70
|
+
if select.select([sys.stdin], [], [], 0.1)[0]:
|
71
|
+
char = sys.stdin.read(1)
|
72
|
+
if char in _WHITE_SPACE:
|
73
|
+
self.queue.put(ord(" "))
|
74
|
+
finally:
|
75
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) # type: ignore
|
76
|
+
|
77
|
+
def space_bar_pressed(self) -> bool:
|
78
|
+
found = False
|
79
|
+
while not self.queue.empty():
|
80
|
+
try:
|
81
|
+
key = self.queue.get(block=False, timeout=0.1)
|
82
|
+
if key == ord(" "):
|
83
|
+
found = True
|
84
|
+
self.queue.task_done()
|
85
|
+
except Empty:
|
86
|
+
break
|
87
|
+
return found
|
88
|
+
|
89
|
+
def stop(self) -> None:
|
90
|
+
self.queue_cancel.put(True)
|
91
|
+
self.process.join()
|
92
|
+
|
93
|
+
|
94
|
+
def main() -> None:
|
95
|
+
watcher = SpaceBarWatcher()
|
96
|
+
try:
|
97
|
+
while True:
|
98
|
+
if watcher.space_bar_pressed():
|
99
|
+
print("Space bar hit!")
|
100
|
+
break
|
101
|
+
time.sleep(1)
|
102
|
+
finally:
|
103
|
+
print("Stopping watcher.")
|
104
|
+
watcher.stop()
|
105
|
+
print("Watcher stopped.")
|
106
|
+
return
|
107
|
+
|
108
|
+
|
109
|
+
if __name__ == "__main__":
|
110
|
+
try:
|
111
|
+
main()
|
112
|
+
except KeyboardInterrupt:
|
113
|
+
print("Keyboard interrupt detected.")
|
fastled/live_client.py
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
import threading
|
2
|
+
from pathlib import Path
|
3
|
+
|
4
|
+
from fastled.compile_server import CompileServer
|
5
|
+
from fastled.types import BuildMode
|
6
|
+
|
7
|
+
|
8
|
+
class LiveClient:
|
9
|
+
"""LiveClient class watches for changes and auto-triggeres rebuild."""
|
10
|
+
|
11
|
+
def __init__(
|
12
|
+
self,
|
13
|
+
sketch_directory: Path,
|
14
|
+
host: str | CompileServer | None = None,
|
15
|
+
auto_start: bool = True,
|
16
|
+
open_web_browser: bool = True,
|
17
|
+
keep_running: bool = True,
|
18
|
+
build_mode: BuildMode = BuildMode.QUICK,
|
19
|
+
profile: bool = False,
|
20
|
+
) -> None:
|
21
|
+
self.sketch_directory = sketch_directory
|
22
|
+
self.host = host
|
23
|
+
self.open_web_browser = open_web_browser
|
24
|
+
self.keep_running = keep_running
|
25
|
+
self.build_mode = build_mode
|
26
|
+
self.profile = profile
|
27
|
+
self.auto_start = auto_start
|
28
|
+
self.shutdown = threading.Event()
|
29
|
+
self.thread: threading.Thread | None = None
|
30
|
+
if auto_start:
|
31
|
+
self.start()
|
32
|
+
|
33
|
+
def run(self) -> int:
|
34
|
+
"""Run the client."""
|
35
|
+
from fastled.client_server import run_client # avoid circular import
|
36
|
+
|
37
|
+
rtn = run_client(
|
38
|
+
directory=self.sketch_directory,
|
39
|
+
host=self.host,
|
40
|
+
open_web_browser=self.open_web_browser,
|
41
|
+
keep_running=self.keep_running,
|
42
|
+
build_mode=self.build_mode,
|
43
|
+
profile=self.profile,
|
44
|
+
shutdown=self.shutdown,
|
45
|
+
)
|
46
|
+
return rtn
|
47
|
+
|
48
|
+
@property
|
49
|
+
def running(self) -> bool:
|
50
|
+
return self.thread is not None and self.thread.is_alive()
|
51
|
+
|
52
|
+
def start(self) -> None:
|
53
|
+
"""Start the client."""
|
54
|
+
assert not self.running, "LiveClient is already running"
|
55
|
+
self.shutdown.clear()
|
56
|
+
self.thread = threading.Thread(target=self.run, daemon=True)
|
57
|
+
self.thread.start()
|
58
|
+
|
59
|
+
def stop(self) -> None:
|
60
|
+
"""Stop the client."""
|
61
|
+
self.shutdown.set()
|
62
|
+
if self.thread:
|
63
|
+
self.thread.join()
|
64
|
+
self.thread = None
|
65
|
+
|
66
|
+
def finalize(self) -> None:
|
67
|
+
"""Finalize the client."""
|
68
|
+
self.stop()
|
69
|
+
self.thread = None
|
fastled/open_browser.py
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
import os
|
2
|
+
import socket
|
3
|
+
import sys
|
4
|
+
from multiprocessing import Process
|
5
|
+
from pathlib import Path
|
6
|
+
|
7
|
+
from livereload import Server
|
8
|
+
|
9
|
+
DEFAULT_PORT = 8081
|
10
|
+
|
11
|
+
|
12
|
+
def _open_browser_python(fastled_js: Path, port: int) -> Server:
|
13
|
+
"""Start livereload server in the fastled_js directory using API"""
|
14
|
+
print(f"\nStarting livereload server in {fastled_js} on port {port}")
|
15
|
+
|
16
|
+
# server = Server()
|
17
|
+
# server.watch(str(fastled_js / "index.html"), delay=0.1)
|
18
|
+
# server.setHeader("Cache-Control", "no-cache")
|
19
|
+
# server.serve(root=str(fastled_js), port=port, open_url_delay=0.5)
|
20
|
+
# return server
|
21
|
+
os.system(f"cd {fastled_js} && live-server")
|
22
|
+
|
23
|
+
|
24
|
+
def _find_open_port(start_port: int) -> int:
|
25
|
+
"""Find an open port starting from start_port."""
|
26
|
+
port = start_port
|
27
|
+
while True:
|
28
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
29
|
+
if sock.connect_ex(("localhost", port)) != 0:
|
30
|
+
return port
|
31
|
+
port += 1
|
32
|
+
|
33
|
+
|
34
|
+
def _run_server(fastled_js: Path, port: int) -> None:
|
35
|
+
"""Function to run in separate process that starts the livereload server"""
|
36
|
+
sys.stderr = open(os.devnull, "w") # Suppress stderr output
|
37
|
+
_open_browser_python(fastled_js, port)
|
38
|
+
try:
|
39
|
+
# Keep the process running
|
40
|
+
while True:
|
41
|
+
pass
|
42
|
+
except KeyboardInterrupt:
|
43
|
+
print("\nShutting down livereload server...")
|
44
|
+
|
45
|
+
|
46
|
+
def open_browser_process(fastled_js: Path, port: int | None = None) -> Process:
|
47
|
+
"""Start livereload server in the fastled_js directory and return the process"""
|
48
|
+
if port is None:
|
49
|
+
port = DEFAULT_PORT
|
50
|
+
|
51
|
+
port = _find_open_port(port)
|
52
|
+
|
53
|
+
process = Process(
|
54
|
+
target=_run_server,
|
55
|
+
args=(fastled_js, port),
|
56
|
+
daemon=True,
|
57
|
+
)
|
58
|
+
process.start()
|
59
|
+
return process
|
fastled/parse_args.py
ADDED
@@ -0,0 +1,172 @@
|
|
1
|
+
import argparse
|
2
|
+
import os
|
3
|
+
import sys
|
4
|
+
from pathlib import Path
|
5
|
+
|
6
|
+
from fastled import __version__
|
7
|
+
from fastled.docker_manager import DockerManager
|
8
|
+
from fastled.project_init import project_init
|
9
|
+
from fastled.select_sketch_directory import select_sketch_directory
|
10
|
+
from fastled.settings import DEFAULT_URL
|
11
|
+
from fastled.sketch import (
|
12
|
+
find_sketch_directories,
|
13
|
+
looks_like_fastled_repo,
|
14
|
+
looks_like_sketch_directory,
|
15
|
+
)
|
16
|
+
|
17
|
+
|
18
|
+
def parse_args() -> argparse.Namespace:
|
19
|
+
"""Parse command-line arguments."""
|
20
|
+
parser = argparse.ArgumentParser(description=f"FastLED WASM Compiler {__version__}")
|
21
|
+
parser.add_argument(
|
22
|
+
"--version", action="version", version=f"{__version__}"
|
23
|
+
)
|
24
|
+
parser.add_argument(
|
25
|
+
"directory",
|
26
|
+
type=str,
|
27
|
+
nargs="?",
|
28
|
+
default=None,
|
29
|
+
help="Directory containing the FastLED sketch to compile",
|
30
|
+
)
|
31
|
+
parser.add_argument(
|
32
|
+
"--init",
|
33
|
+
action="store_true",
|
34
|
+
help="Initialize the FastLED sketch in the current directory",
|
35
|
+
)
|
36
|
+
parser.add_argument(
|
37
|
+
"--just-compile",
|
38
|
+
action="store_true",
|
39
|
+
help="Just compile, skip opening the browser and watching for changes.",
|
40
|
+
)
|
41
|
+
parser.add_argument(
|
42
|
+
"--web",
|
43
|
+
"-w",
|
44
|
+
type=str,
|
45
|
+
nargs="?",
|
46
|
+
# const does not seem to be working as expected
|
47
|
+
const=DEFAULT_URL, # Default value when --web is specified without value
|
48
|
+
help="Use web compiler. Optional URL can be provided (default: https://fastled.onrender.com)",
|
49
|
+
)
|
50
|
+
parser.add_argument(
|
51
|
+
"-i",
|
52
|
+
"--interactive",
|
53
|
+
action="store_true",
|
54
|
+
help="Run in interactive mode (Not available with --web)",
|
55
|
+
)
|
56
|
+
parser.add_argument(
|
57
|
+
"--profile",
|
58
|
+
action="store_true",
|
59
|
+
help="Enable profiling for web compilation",
|
60
|
+
)
|
61
|
+
parser.add_argument(
|
62
|
+
"--force-compile",
|
63
|
+
action="store_true",
|
64
|
+
help="Skips the test to see if the current directory is a valid FastLED sketch directory",
|
65
|
+
)
|
66
|
+
parser.add_argument(
|
67
|
+
"--no-auto-updates",
|
68
|
+
action="store_true",
|
69
|
+
help="Disable automatic updates of the wasm compiler image when using docker.",
|
70
|
+
)
|
71
|
+
parser.add_argument(
|
72
|
+
"--update",
|
73
|
+
"--upgrade",
|
74
|
+
action="store_true",
|
75
|
+
help="Update the wasm compiler (if necessary) before running",
|
76
|
+
)
|
77
|
+
parser.add_argument(
|
78
|
+
"--localhost",
|
79
|
+
"--local",
|
80
|
+
"-l",
|
81
|
+
action="store_true",
|
82
|
+
help="Use localhost for web compilation from an instance of fastled --server, creating it if necessary",
|
83
|
+
)
|
84
|
+
parser.add_argument(
|
85
|
+
"--server",
|
86
|
+
"-s",
|
87
|
+
action="store_true",
|
88
|
+
help="Run the server in the current directory, volume mapping fastled if we are in the repo",
|
89
|
+
)
|
90
|
+
|
91
|
+
build_mode = parser.add_mutually_exclusive_group()
|
92
|
+
build_mode.add_argument("--debug", action="store_true", help="Build in debug mode")
|
93
|
+
build_mode.add_argument(
|
94
|
+
"--quick",
|
95
|
+
action="store_true",
|
96
|
+
default=True,
|
97
|
+
help="Build in quick mode (default)",
|
98
|
+
)
|
99
|
+
build_mode.add_argument(
|
100
|
+
"--release", action="store_true", help="Build in release mode"
|
101
|
+
)
|
102
|
+
|
103
|
+
cwd_is_fastled = looks_like_fastled_repo(Path(os.getcwd()))
|
104
|
+
|
105
|
+
args = parser.parse_args()
|
106
|
+
|
107
|
+
if args.init:
|
108
|
+
args.directory = project_init()
|
109
|
+
print("\nInitialized FastLED project in", args.directory)
|
110
|
+
print(f"Use 'fastled {args.directory}' to compile the project.")
|
111
|
+
sys.exit(0)
|
112
|
+
|
113
|
+
if not args.update:
|
114
|
+
if args.no_auto_updates:
|
115
|
+
args.auto_update = False
|
116
|
+
else:
|
117
|
+
args.auto_update = None
|
118
|
+
|
119
|
+
if (
|
120
|
+
not cwd_is_fastled
|
121
|
+
and not args.localhost
|
122
|
+
and not args.web
|
123
|
+
and not args.server
|
124
|
+
):
|
125
|
+
if DockerManager.is_docker_installed():
|
126
|
+
if not DockerManager.ensure_linux_containers_for_windows():
|
127
|
+
print(
|
128
|
+
f"Windows must be in linux containers mode, but is in Windows container mode, Using web compiler at {DEFAULT_URL}."
|
129
|
+
)
|
130
|
+
args.web = DEFAULT_URL
|
131
|
+
else:
|
132
|
+
print(
|
133
|
+
"Docker is installed. Defaulting to --local mode, use --web to override and use the web compiler instead."
|
134
|
+
)
|
135
|
+
args.localhost = True
|
136
|
+
else:
|
137
|
+
print(f"Docker is not installed. Using web compiler at {DEFAULT_URL}.")
|
138
|
+
args.web = DEFAULT_URL
|
139
|
+
if cwd_is_fastled and not args.web and not args.server:
|
140
|
+
print("Forcing --local mode because we are in the FastLED repo")
|
141
|
+
args.localhost = True
|
142
|
+
if args.localhost:
|
143
|
+
args.web = "localhost"
|
144
|
+
if args.interactive and not args.server:
|
145
|
+
print("--interactive forces --server mode")
|
146
|
+
args.server = True
|
147
|
+
if args.directory is None and not args.server:
|
148
|
+
# does current directory look like a sketch?
|
149
|
+
maybe_sketch_dir = Path(os.getcwd())
|
150
|
+
if looks_like_sketch_directory(maybe_sketch_dir):
|
151
|
+
args.directory = str(maybe_sketch_dir)
|
152
|
+
else:
|
153
|
+
print("Searching for sketch directories...")
|
154
|
+
sketch_directories = find_sketch_directories(maybe_sketch_dir)
|
155
|
+
selected_dir = select_sketch_directory(
|
156
|
+
sketch_directories, cwd_is_fastled
|
157
|
+
)
|
158
|
+
if selected_dir:
|
159
|
+
print(f"Using sketch directory: {selected_dir}")
|
160
|
+
args.directory = selected_dir
|
161
|
+
else:
|
162
|
+
print(
|
163
|
+
"\nYou either need to specify a sketch directory or run in --server mode."
|
164
|
+
)
|
165
|
+
sys.exit(1)
|
166
|
+
elif args.directory is not None and os.path.isfile(args.directory):
|
167
|
+
dir_path = Path(args.directory).parent
|
168
|
+
if looks_like_sketch_directory(dir_path):
|
169
|
+
print(f"Using sketch directory: {dir_path}")
|
170
|
+
args.directory = str(dir_path)
|
171
|
+
|
172
|
+
return args
|
fastled/paths.py
ADDED
fastled/project_init.py
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
import zipfile
|
2
|
+
from pathlib import Path
|
3
|
+
|
4
|
+
import httpx
|
5
|
+
|
6
|
+
from fastled.settings import DEFAULT_URL
|
7
|
+
|
8
|
+
ENDPOINT_PROJECT_INIT = f"{DEFAULT_URL}/project/init"
|
9
|
+
ENDPOINT_INFO = f"{DEFAULT_URL}/info"
|
10
|
+
DEFAULT_EXAMPLE = "wasm"
|
11
|
+
|
12
|
+
|
13
|
+
def get_examples() -> list[str]:
|
14
|
+
response = httpx.get(ENDPOINT_INFO, timeout=4)
|
15
|
+
response.raise_for_status()
|
16
|
+
out: list[str] = response.json()["examples"]
|
17
|
+
return sorted(out)
|
18
|
+
|
19
|
+
|
20
|
+
def _prompt_for_example() -> str:
|
21
|
+
examples = get_examples()
|
22
|
+
while True:
|
23
|
+
print("Available examples:")
|
24
|
+
for i, example in enumerate(examples):
|
25
|
+
print(f" [{i+1}]: {example}")
|
26
|
+
answer = input("Enter the example number or name: ").strip()
|
27
|
+
if answer.isdigit():
|
28
|
+
example_num = int(answer) - 1
|
29
|
+
if example_num < 0 or example_num >= len(examples):
|
30
|
+
print("Invalid example number")
|
31
|
+
continue
|
32
|
+
return examples[example_num]
|
33
|
+
elif answer in examples:
|
34
|
+
return answer
|
35
|
+
|
36
|
+
|
37
|
+
def project_init(
|
38
|
+
example: str | None = "PROMPT", # prompt for example
|
39
|
+
outputdir: Path | None = None,
|
40
|
+
host: str | None = None,
|
41
|
+
) -> Path:
|
42
|
+
"""
|
43
|
+
Initialize a new FastLED project.
|
44
|
+
"""
|
45
|
+
host = host or DEFAULT_URL
|
46
|
+
outputdir = Path(outputdir) if outputdir is not None else Path("fastled")
|
47
|
+
if example == "PROMPT" or example is None:
|
48
|
+
try:
|
49
|
+
example = _prompt_for_example()
|
50
|
+
except httpx.HTTPStatusError:
|
51
|
+
print(
|
52
|
+
f"Failed to fetch examples, using default example '{DEFAULT_EXAMPLE}'"
|
53
|
+
)
|
54
|
+
example = DEFAULT_EXAMPLE
|
55
|
+
assert example is not None
|
56
|
+
endpoint_url = f"{host}/project/init/{example}"
|
57
|
+
response = httpx.get(endpoint_url, timeout=20)
|
58
|
+
response.raise_for_status()
|
59
|
+
content = response.content
|
60
|
+
tmpzip = outputdir / "fastled.zip"
|
61
|
+
outputdir.mkdir(exist_ok=True)
|
62
|
+
tmpzip.write_bytes(content)
|
63
|
+
with zipfile.ZipFile(tmpzip, "r") as zip_ref:
|
64
|
+
zip_ref.extractall(outputdir)
|
65
|
+
tmpzip.unlink()
|
66
|
+
out = outputdir / example
|
67
|
+
assert out.exists()
|
68
|
+
return out
|
69
|
+
|
70
|
+
|
71
|
+
def unit_test() -> None:
|
72
|
+
project_init()
|
73
|
+
|
74
|
+
|
75
|
+
if __name__ == "__main__":
|
76
|
+
unit_test()
|