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/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,116 @@
|
|
1
|
+
import _thread
|
2
|
+
import os
|
3
|
+
import select
|
4
|
+
import sys
|
5
|
+
import time
|
6
|
+
from queue import Empty, Queue
|
7
|
+
from threading import Thread
|
8
|
+
|
9
|
+
_WHITE_SPACE = {" ", "\n", "\r"} # Including Enter key as whitespace
|
10
|
+
|
11
|
+
|
12
|
+
class SpaceBarWatcher:
|
13
|
+
@classmethod
|
14
|
+
def watch_space_bar_pressed(cls, timeout: float = 0) -> bool:
|
15
|
+
watcher = cls()
|
16
|
+
try:
|
17
|
+
start_time = time.time()
|
18
|
+
while True:
|
19
|
+
if watcher.space_bar_pressed():
|
20
|
+
return True
|
21
|
+
if time.time() - start_time > timeout:
|
22
|
+
return False
|
23
|
+
finally:
|
24
|
+
watcher.stop()
|
25
|
+
|
26
|
+
def __init__(self) -> None:
|
27
|
+
self.queue: Queue = Queue()
|
28
|
+
self.queue_cancel: Queue = Queue()
|
29
|
+
self.process = Thread(target=self._watch_for_space, daemon=True)
|
30
|
+
self.process.start()
|
31
|
+
|
32
|
+
def _watch_for_space(self) -> None:
|
33
|
+
fd = sys.stdin.fileno()
|
34
|
+
if os.name == "nt": # Windows
|
35
|
+
import msvcrt
|
36
|
+
|
37
|
+
while True:
|
38
|
+
# Check for cancel signal
|
39
|
+
try:
|
40
|
+
self.queue_cancel.get(timeout=0.1)
|
41
|
+
break
|
42
|
+
except Empty:
|
43
|
+
pass
|
44
|
+
|
45
|
+
# Check if there's input ready
|
46
|
+
if msvcrt.kbhit(): # type: ignore
|
47
|
+
char = msvcrt.getch().decode() # type: ignore
|
48
|
+
if char in _WHITE_SPACE:
|
49
|
+
self.queue.put(ord(" "))
|
50
|
+
else: # Unix-like systems
|
51
|
+
import termios
|
52
|
+
import tty
|
53
|
+
|
54
|
+
old_settings = termios.tcgetattr(fd) # type: ignore
|
55
|
+
try:
|
56
|
+
tty.setcbreak( # type: ignore
|
57
|
+
fd
|
58
|
+
) # Use cbreak mode to avoid console issues # type: ignore
|
59
|
+
while True:
|
60
|
+
# Check for cancel signal
|
61
|
+
try:
|
62
|
+
self.queue_cancel.get(timeout=0.1)
|
63
|
+
break
|
64
|
+
except Empty:
|
65
|
+
pass
|
66
|
+
|
67
|
+
# Check if there's input ready
|
68
|
+
if select.select([sys.stdin], [], [], 0.1)[0]:
|
69
|
+
char = sys.stdin.read(1)
|
70
|
+
if ord(char) == 3: # Ctrl+C
|
71
|
+
_thread.interrupt_main()
|
72
|
+
break
|
73
|
+
if char in _WHITE_SPACE:
|
74
|
+
self.queue.put(ord(" "))
|
75
|
+
finally:
|
76
|
+
termios.tcsetattr( # type: ignore
|
77
|
+
fd, termios.TCSADRAIN, old_settings # type: ignore
|
78
|
+
) # Restore terminal settings
|
79
|
+
|
80
|
+
def space_bar_pressed(self) -> bool:
|
81
|
+
found = False
|
82
|
+
while not self.queue.empty():
|
83
|
+
try:
|
84
|
+
key = self.queue.get(block=False)
|
85
|
+
if key == ord(" "): # Spacebar
|
86
|
+
found = True
|
87
|
+
self.queue.task_done()
|
88
|
+
except Empty:
|
89
|
+
break
|
90
|
+
return found
|
91
|
+
|
92
|
+
def stop(self) -> None:
|
93
|
+
self.queue_cancel.put(True)
|
94
|
+
self.process.join()
|
95
|
+
|
96
|
+
|
97
|
+
def main() -> None:
|
98
|
+
watcher = SpaceBarWatcher()
|
99
|
+
try:
|
100
|
+
while True:
|
101
|
+
if watcher.space_bar_pressed():
|
102
|
+
print("Space bar hit!")
|
103
|
+
break
|
104
|
+
time.sleep(1)
|
105
|
+
finally:
|
106
|
+
print("Stopping watcher.")
|
107
|
+
watcher.stop()
|
108
|
+
print("Watcher stopped.")
|
109
|
+
return
|
110
|
+
|
111
|
+
|
112
|
+
if __name__ == "__main__":
|
113
|
+
try:
|
114
|
+
main()
|
115
|
+
except KeyboardInterrupt:
|
116
|
+
print("Keyboard interrupt detected.")
|
fastled/live_client.py
ADDED
@@ -0,0 +1,86 @@
|
|
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
|
+
def url(self) -> str:
|
49
|
+
"""Get the URL of the server."""
|
50
|
+
if isinstance(self.host, CompileServer):
|
51
|
+
return self.host.url()
|
52
|
+
if self.host is None:
|
53
|
+
import warnings
|
54
|
+
|
55
|
+
warnings.warn("TODO: use the actual host.")
|
56
|
+
return "http://localhost:9021"
|
57
|
+
return self.host
|
58
|
+
|
59
|
+
@property
|
60
|
+
def running(self) -> bool:
|
61
|
+
return self.thread is not None and self.thread.is_alive()
|
62
|
+
|
63
|
+
def start(self) -> None:
|
64
|
+
"""Start the client."""
|
65
|
+
assert not self.running, "LiveClient is already running"
|
66
|
+
self.shutdown.clear()
|
67
|
+
self.thread = threading.Thread(target=self.run, daemon=True)
|
68
|
+
self.thread.start()
|
69
|
+
|
70
|
+
def stop(self) -> None:
|
71
|
+
"""Stop the client."""
|
72
|
+
self.shutdown.set()
|
73
|
+
if self.thread:
|
74
|
+
self.thread.join()
|
75
|
+
self.thread = None
|
76
|
+
|
77
|
+
def finalize(self) -> None:
|
78
|
+
"""Finalize the client."""
|
79
|
+
self.stop()
|
80
|
+
self.thread = None
|
81
|
+
|
82
|
+
def __enter__(self) -> "LiveClient":
|
83
|
+
return self
|
84
|
+
|
85
|
+
def __exit__(self, exc_type, exc_value, traceback) -> None:
|
86
|
+
self.finalize()
|
fastled/open_browser.py
ADDED
@@ -0,0 +1,161 @@
|
|
1
|
+
import subprocess
|
2
|
+
import sys
|
3
|
+
import time
|
4
|
+
import webbrowser
|
5
|
+
from multiprocessing import Process
|
6
|
+
from pathlib import Path
|
7
|
+
|
8
|
+
DEFAULT_PORT = 8089 # different than live version.
|
9
|
+
|
10
|
+
PYTHON_EXE = sys.executable
|
11
|
+
|
12
|
+
|
13
|
+
def open_http_server_subprocess(
|
14
|
+
fastled_js: Path, port: int, open_browser: bool
|
15
|
+
) -> None:
|
16
|
+
"""Start livereload server in the fastled_js directory and return the process"""
|
17
|
+
import shutil
|
18
|
+
|
19
|
+
try:
|
20
|
+
if shutil.which("live-server") is not None:
|
21
|
+
cmd = [
|
22
|
+
"live-server",
|
23
|
+
f"--port={port}",
|
24
|
+
"--host=localhost",
|
25
|
+
".",
|
26
|
+
]
|
27
|
+
if not open_browser:
|
28
|
+
cmd.append("--no-browser")
|
29
|
+
subprocess.run(cmd, shell=True, cwd=fastled_js)
|
30
|
+
return
|
31
|
+
|
32
|
+
cmd = [
|
33
|
+
PYTHON_EXE,
|
34
|
+
"-m",
|
35
|
+
"fastled.open_browser2",
|
36
|
+
str(fastled_js),
|
37
|
+
"--port",
|
38
|
+
str(port),
|
39
|
+
]
|
40
|
+
# return subprocess.Popen(cmd) # type ignore
|
41
|
+
# pipe stderr and stdout to null
|
42
|
+
subprocess.run(
|
43
|
+
cmd,
|
44
|
+
stdout=subprocess.DEVNULL,
|
45
|
+
stderr=subprocess.DEVNULL,
|
46
|
+
) # type ignore
|
47
|
+
except KeyboardInterrupt:
|
48
|
+
import _thread
|
49
|
+
|
50
|
+
_thread.interrupt_main()
|
51
|
+
|
52
|
+
|
53
|
+
def is_port_free(port: int) -> bool:
|
54
|
+
"""Check if a port is free"""
|
55
|
+
import httpx
|
56
|
+
|
57
|
+
try:
|
58
|
+
response = httpx.get(f"http://localhost:{port}", timeout=1)
|
59
|
+
response.raise_for_status()
|
60
|
+
return False
|
61
|
+
except (httpx.HTTPError, httpx.ConnectError):
|
62
|
+
return True
|
63
|
+
|
64
|
+
|
65
|
+
def find_free_port(start_port: int) -> int:
|
66
|
+
"""Find a free port starting at start_port"""
|
67
|
+
for port in range(start_port, start_port + 100):
|
68
|
+
if is_port_free(port):
|
69
|
+
print(f"Found free port: {port}")
|
70
|
+
return port
|
71
|
+
else:
|
72
|
+
print(f"Port {port} is in use, finding next")
|
73
|
+
raise ValueError("Could not find a free port")
|
74
|
+
|
75
|
+
|
76
|
+
def wait_for_server(port: int, timeout: int = 10) -> None:
|
77
|
+
"""Wait for the server to start."""
|
78
|
+
from httpx import get
|
79
|
+
|
80
|
+
future_time = time.time() + timeout
|
81
|
+
while future_time > time.time():
|
82
|
+
try:
|
83
|
+
response = get(f"http://localhost:{port}", timeout=1)
|
84
|
+
if response.status_code == 200:
|
85
|
+
return
|
86
|
+
except Exception:
|
87
|
+
continue
|
88
|
+
raise TimeoutError("Could not connect to server")
|
89
|
+
|
90
|
+
|
91
|
+
def _background_npm_install_live_server() -> None:
|
92
|
+
import shutil
|
93
|
+
import time
|
94
|
+
|
95
|
+
if shutil.which("npm") is None:
|
96
|
+
return
|
97
|
+
|
98
|
+
if shutil.which("live-server") is not None:
|
99
|
+
return
|
100
|
+
|
101
|
+
time.sleep(3)
|
102
|
+
subprocess.run(
|
103
|
+
["npm", "install", "-g", "live-server"],
|
104
|
+
stdout=subprocess.DEVNULL,
|
105
|
+
stderr=subprocess.DEVNULL,
|
106
|
+
)
|
107
|
+
|
108
|
+
|
109
|
+
def open_browser_process(
|
110
|
+
fastled_js: Path, port: int | None = None, open_browser: bool = True
|
111
|
+
) -> Process:
|
112
|
+
import shutil
|
113
|
+
|
114
|
+
"""Start livereload server in the fastled_js directory and return the process"""
|
115
|
+
if port is not None:
|
116
|
+
if not is_port_free(port):
|
117
|
+
raise ValueError(f"Port {port} was specified but in use")
|
118
|
+
else:
|
119
|
+
port = find_free_port(DEFAULT_PORT)
|
120
|
+
out: Process = Process(
|
121
|
+
target=open_http_server_subprocess,
|
122
|
+
args=(fastled_js, port, False),
|
123
|
+
daemon=True,
|
124
|
+
)
|
125
|
+
out.start()
|
126
|
+
wait_for_server(port)
|
127
|
+
if open_browser:
|
128
|
+
print(f"Opening browser to http://localhost:{port}")
|
129
|
+
webbrowser.open(url=f"http://localhost:{port}", new=1, autoraise=True)
|
130
|
+
|
131
|
+
# start a deamon thread to install live-server
|
132
|
+
if shutil.which("live-server") is None:
|
133
|
+
import threading
|
134
|
+
|
135
|
+
t = threading.Thread(target=_background_npm_install_live_server)
|
136
|
+
t.daemon = True
|
137
|
+
t.start()
|
138
|
+
return out
|
139
|
+
|
140
|
+
|
141
|
+
if __name__ == "__main__":
|
142
|
+
import argparse
|
143
|
+
|
144
|
+
parser = argparse.ArgumentParser(
|
145
|
+
description="Open a browser to the fastled_js directory"
|
146
|
+
)
|
147
|
+
parser.add_argument(
|
148
|
+
"fastled_js",
|
149
|
+
type=Path,
|
150
|
+
help="Path to the fastled_js directory",
|
151
|
+
)
|
152
|
+
parser.add_argument(
|
153
|
+
"--port",
|
154
|
+
type=int,
|
155
|
+
default=DEFAULT_PORT,
|
156
|
+
help=f"Port to run the server on (default: {DEFAULT_PORT})",
|
157
|
+
)
|
158
|
+
args = parser.parse_args()
|
159
|
+
|
160
|
+
proc = open_browser_process(args.fastled_js, args.port, open_browser=True)
|
161
|
+
proc.join()
|
fastled/open_browser2.py
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
import argparse
|
2
|
+
from pathlib import Path
|
3
|
+
|
4
|
+
from livereload import Server
|
5
|
+
|
6
|
+
|
7
|
+
def _run_flask_server(fastled_js: Path, port: int) -> None:
|
8
|
+
"""Run Flask server with live reload in a subprocess"""
|
9
|
+
try:
|
10
|
+
from flask import Flask, send_from_directory
|
11
|
+
|
12
|
+
app = Flask(__name__)
|
13
|
+
|
14
|
+
# Must be a full path or flask will fail to find the file.
|
15
|
+
fastled_js = fastled_js.resolve()
|
16
|
+
|
17
|
+
@app.route("/")
|
18
|
+
def serve_index():
|
19
|
+
return send_from_directory(fastled_js, "index.html")
|
20
|
+
|
21
|
+
@app.route("/<path:path>")
|
22
|
+
def serve_files(path):
|
23
|
+
response = send_from_directory(fastled_js, path)
|
24
|
+
# Some servers don't set the Content-Type header for a bunch of files.
|
25
|
+
if path.endswith(".js"):
|
26
|
+
response.headers["Content-Type"] = "application/javascript"
|
27
|
+
if path.endswith(".css"):
|
28
|
+
response.headers["Content-Type"] = "text/css"
|
29
|
+
if path.endswith(".wasm"):
|
30
|
+
response.headers["Content-Type"] = "application/wasm"
|
31
|
+
if path.endswith(".json"):
|
32
|
+
response.headers["Content-Type"] = "application/json"
|
33
|
+
if path.endswith(".png"):
|
34
|
+
response.headers["Content-Type"] = "image/png"
|
35
|
+
if path.endswith(".jpg"):
|
36
|
+
response.headers["Content-Type"] = "image/jpeg"
|
37
|
+
if path.endswith(".jpeg"):
|
38
|
+
response.headers["Content-Type"] = "image/jpeg"
|
39
|
+
if path.endswith(".gif"):
|
40
|
+
response.headers["Content-Type"] = "image/gif"
|
41
|
+
if path.endswith(".svg"):
|
42
|
+
response.headers["Content-Type"] = "image/svg+xml"
|
43
|
+
if path.endswith(".ico"):
|
44
|
+
response.headers["Content-Type"] = "image/x-icon"
|
45
|
+
if path.endswith(".html"):
|
46
|
+
response.headers["Content-Type"] = "text/html"
|
47
|
+
|
48
|
+
# now also add headers to force no caching
|
49
|
+
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
50
|
+
response.headers["Pragma"] = "no-cache"
|
51
|
+
response.headers["Expires"] = "0"
|
52
|
+
return response
|
53
|
+
|
54
|
+
server = Server(app.wsgi_app)
|
55
|
+
# Watch index.html for changes
|
56
|
+
server.watch(str(fastled_js / "index.html"))
|
57
|
+
# server.watch(str(fastled_js / "index.js"))
|
58
|
+
# server.watch(str(fastled_js / "index.css"))
|
59
|
+
# Start the server
|
60
|
+
server.serve(port=port, debug=True)
|
61
|
+
except KeyboardInterrupt:
|
62
|
+
import _thread
|
63
|
+
|
64
|
+
_thread.interrupt_main()
|
65
|
+
except Exception as e:
|
66
|
+
print(f"Failed to run Flask server: {e}")
|
67
|
+
import _thread
|
68
|
+
|
69
|
+
_thread.interrupt_main()
|
70
|
+
|
71
|
+
|
72
|
+
def run(path: Path, port: int) -> None:
|
73
|
+
"""Run the Flask server."""
|
74
|
+
try:
|
75
|
+
_run_flask_server(path, port)
|
76
|
+
import warnings
|
77
|
+
|
78
|
+
warnings.warn("Flask server has stopped")
|
79
|
+
except KeyboardInterrupt:
|
80
|
+
import _thread
|
81
|
+
|
82
|
+
_thread.interrupt_main()
|
83
|
+
pass
|
84
|
+
|
85
|
+
|
86
|
+
def parse_args() -> argparse.Namespace:
|
87
|
+
"""Parse the command line arguments."""
|
88
|
+
parser = argparse.ArgumentParser(
|
89
|
+
description="Open a browser to the fastled_js directory"
|
90
|
+
)
|
91
|
+
parser.add_argument(
|
92
|
+
"fastled_js", type=Path, help="Path to the fastled_js directory"
|
93
|
+
)
|
94
|
+
parser.add_argument(
|
95
|
+
"--port",
|
96
|
+
"-p",
|
97
|
+
type=int,
|
98
|
+
required=True,
|
99
|
+
help="Port to run the server on (default: %(default)s)",
|
100
|
+
)
|
101
|
+
return parser.parse_args()
|
102
|
+
|
103
|
+
|
104
|
+
def main() -> None:
|
105
|
+
"""Main function."""
|
106
|
+
args = parse_args()
|
107
|
+
run(args.fastled_js, args.port)
|
108
|
+
|
109
|
+
|
110
|
+
if __name__ == "__main__":
|
111
|
+
main()
|