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/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
@@ -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
@@ -0,0 +1,4 @@
1
+ from pathlib import Path
2
+
3
+ HERE = Path(__file__).resolve().parent
4
+ PROJECT_ROOT = HERE.parent.parent
@@ -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()