fastled 1.2.33__py3-none-any.whl → 1.4.50__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.
Files changed (58) hide show
  1. fastled/__init__.py +51 -192
  2. fastled/__main__.py +14 -0
  3. fastled/__version__.py +6 -0
  4. fastled/app.py +124 -27
  5. fastled/args.py +124 -0
  6. fastled/assets/localhost-key.pem +28 -0
  7. fastled/assets/localhost.pem +27 -0
  8. fastled/cli.py +10 -2
  9. fastled/cli_test.py +21 -0
  10. fastled/cli_test_interactive.py +21 -0
  11. fastled/client_server.py +334 -55
  12. fastled/compile_server.py +12 -1
  13. fastled/compile_server_impl.py +115 -42
  14. fastled/docker_manager.py +392 -69
  15. fastled/emoji_util.py +27 -0
  16. fastled/filewatcher.py +100 -8
  17. fastled/find_good_connection.py +105 -0
  18. fastled/header_dump.py +63 -0
  19. fastled/install/__init__.py +1 -0
  20. fastled/install/examples_manager.py +62 -0
  21. fastled/install/extension_manager.py +113 -0
  22. fastled/install/main.py +156 -0
  23. fastled/install/project_detection.py +167 -0
  24. fastled/install/test_install.py +373 -0
  25. fastled/install/vscode_config.py +344 -0
  26. fastled/interruptible_http.py +148 -0
  27. fastled/keyboard.py +1 -0
  28. fastled/keyz.py +84 -0
  29. fastled/live_client.py +26 -1
  30. fastled/open_browser.py +133 -89
  31. fastled/parse_args.py +219 -15
  32. fastled/playwright/chrome_extension_downloader.py +207 -0
  33. fastled/playwright/playwright_browser.py +773 -0
  34. fastled/playwright/resize_tracking.py +127 -0
  35. fastled/print_filter.py +52 -0
  36. fastled/project_init.py +20 -13
  37. fastled/select_sketch_directory.py +142 -17
  38. fastled/server_flask.py +487 -0
  39. fastled/server_start.py +21 -0
  40. fastled/settings.py +53 -4
  41. fastled/site/build.py +2 -10
  42. fastled/site/examples.py +10 -0
  43. fastled/sketch.py +129 -7
  44. fastled/string_diff.py +218 -9
  45. fastled/test/examples.py +7 -5
  46. fastled/types.py +22 -2
  47. fastled/util.py +78 -0
  48. fastled/version.py +41 -0
  49. fastled/web_compile.py +401 -218
  50. fastled/zip_files.py +76 -0
  51. {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info}/METADATA +533 -382
  52. fastled-1.4.50.dist-info/RECORD +60 -0
  53. {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info}/WHEEL +1 -1
  54. fastled/open_browser2.py +0 -111
  55. fastled-1.2.33.dist-info/RECORD +0 -33
  56. {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info}/entry_points.txt +0 -0
  57. {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info/licenses}/LICENSE +0 -0
  58. {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info}/top_level.txt +0 -0
fastled/open_browser.py CHANGED
@@ -1,53 +1,55 @@
1
- import subprocess
1
+ import atexit
2
+ import random
2
3
  import sys
3
4
  import time
4
- import webbrowser
5
+ import weakref
5
6
  from multiprocessing import Process
6
7
  from pathlib import Path
7
8
 
8
- DEFAULT_PORT = 8089 # different than live version.
9
+ from fastled.playwright.playwright_browser import open_with_playwright
10
+ from fastled.server_flask import run_flask_in_thread
11
+
12
+ # Global reference to keep Playwright browser alive
13
+ _playwright_browser_proxy = None
14
+
15
+
16
+ def cleanup_playwright_browser() -> None:
17
+ """Clean up the Playwright browser on exit."""
18
+ try:
19
+ global _playwright_browser_proxy
20
+ if _playwright_browser_proxy:
21
+ _playwright_browser_proxy.close()
22
+ _playwright_browser_proxy = None
23
+ except Exception:
24
+ pass
9
25
 
26
+
27
+ # Register cleanup function
28
+ atexit.register(cleanup_playwright_browser)
29
+
30
+ DEFAULT_PORT = 8089 # different than live version.
10
31
  PYTHON_EXE = sys.executable
11
32
 
33
+ # Use a weak reference set to track processes without preventing garbage collection
34
+ _WEAK_CLEANUP_SET = weakref.WeakSet()
12
35
 
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
36
 
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()
37
+ def add_cleanup(proc: Process) -> None:
38
+ """Add a process to the cleanup list using weak references"""
39
+ _WEAK_CLEANUP_SET.add(proc)
40
+
41
+ # Register a cleanup function that checks if the process is still alive
42
+ def cleanup_if_alive():
43
+ if proc.is_alive():
44
+ try:
45
+ proc.terminate()
46
+ proc.join(timeout=1.0)
47
+ if proc.is_alive():
48
+ proc.kill()
49
+ except Exception:
50
+ pass
51
+
52
+ atexit.register(cleanup_if_alive)
51
53
 
52
54
 
53
55
  def is_port_free(port: int) -> bool:
@@ -55,16 +57,22 @@ def is_port_free(port: int) -> bool:
55
57
  import httpx
56
58
 
57
59
  try:
58
- response = httpx.get(f"http://localhost:{port}", timeout=1)
59
- response.raise_for_status()
60
- return False
60
+ # Try HTTPS first, then fall back to HTTP
61
+ try:
62
+ response = httpx.get(f"https://localhost:{port}", timeout=1, verify=False)
63
+ response.raise_for_status()
64
+ return False
65
+ except (httpx.HTTPError, httpx.ConnectError):
66
+ response = httpx.get(f"http://localhost:{port}", timeout=1)
67
+ response.raise_for_status()
68
+ return False
61
69
  except (httpx.HTTPError, httpx.ConnectError):
62
70
  return True
63
71
 
64
72
 
65
73
  def find_free_port(start_port: int) -> int:
66
74
  """Find a free port starting at start_port"""
67
- for port in range(start_port, start_port + 100):
75
+ for port in range(start_port, start_port + 100, 2):
68
76
  if is_port_free(port):
69
77
  print(f"Found free port: {port}")
70
78
  return port
@@ -73,14 +81,21 @@ def find_free_port(start_port: int) -> int:
73
81
  raise ValueError("Could not find a free port")
74
82
 
75
83
 
76
- def wait_for_server(port: int, timeout: int = 10) -> None:
84
+ def wait_for_server(port: int, timeout: int = 10, enable_https: bool = True) -> None:
77
85
  """Wait for the server to start."""
78
86
  from httpx import get
79
87
 
80
88
  future_time = time.time() + timeout
89
+ protocol = "https" if enable_https else "http"
81
90
  while future_time > time.time():
82
91
  try:
83
- response = get(f"http://localhost:{port}", timeout=1)
92
+ # Try the specified protocol (HTTPS with SSL verification disabled for self-signed certs)
93
+ url = f"{protocol}://localhost:{port}"
94
+ # print(f"Waiting for server to start at {url}")
95
+ verify = (
96
+ False if enable_https else True
97
+ ) # Only disable SSL verification for HTTPS
98
+ response = get(url, timeout=1, verify=verify)
84
99
  if response.status_code == 200:
85
100
  return
86
101
  except Exception:
@@ -88,54 +103,85 @@ def wait_for_server(port: int, timeout: int = 10) -> None:
88
103
  raise TimeoutError("Could not connect to server")
89
104
 
90
105
 
91
- def _background_npm_install_live_server() -> None:
92
- import shutil
93
- import time
106
+ def spawn_http_server(
107
+ fastled_js: Path,
108
+ compile_server_port: int,
109
+ port: int | None = None,
110
+ open_browser: bool = True,
111
+ app: bool = False,
112
+ enable_https: bool = True,
113
+ ) -> Process:
94
114
 
95
- if shutil.which("npm") is None:
96
- return
115
+ if port is not None and not is_port_free(port):
116
+ raise ValueError(f"Port {port} was specified but in use")
117
+ if port is None:
118
+ offset = random.randint(0, 100)
119
+ port = find_free_port(DEFAULT_PORT + offset)
97
120
 
98
- if shutil.which("live-server") is not None:
99
- return
121
+ # Get SSL certificate paths from the fastled assets directory if HTTPS is enabled
122
+ certfile: Path | None = None
123
+ keyfile: Path | None = None
100
124
 
101
- time.sleep(3)
102
- subprocess.run(
103
- ["npm", "install", "-g", "live-server"],
104
- stdout=subprocess.DEVNULL,
105
- stderr=subprocess.DEVNULL,
106
- )
125
+ if enable_https:
126
+ import fastled
107
127
 
128
+ assets_dir = Path(fastled.__file__).parent / "assets"
129
+ certfile = assets_dir / "localhost.pem"
130
+ keyfile = assets_dir / "localhost-key.pem"
108
131
 
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),
132
+ # port: int,
133
+ # cwd: Path,
134
+ # compile_server_port: int,
135
+ # certfile: Path | None = None,
136
+ # keyfile: Path | None = None,
137
+
138
+ proc = Process(
139
+ target=run_flask_in_thread,
140
+ args=(port, fastled_js, compile_server_port, certfile, keyfile),
123
141
  daemon=True,
124
142
  )
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)
143
+ add_cleanup(proc)
144
+ proc.start()
145
+
146
+ # Add to cleanup set with weak reference
147
+ add_cleanup(proc)
130
148
 
131
- # start a deamon thread to install live-server
132
- if shutil.which("live-server") is None:
133
- import threading
149
+ wait_for_server(port, enable_https=enable_https)
150
+ if open_browser:
151
+ protocol = "https" if enable_https else "http"
152
+ url = f"{protocol}://localhost:{port}"
153
+ should_use_playwright = app
154
+
155
+ if should_use_playwright:
156
+ if app:
157
+ # For --app mode, try to install browsers if needed
158
+ from fastled.playwright.playwright_browser import (
159
+ install_playwright_browsers,
160
+ )
161
+
162
+ install_playwright_browsers()
163
+
164
+ print(f"Opening FastLED sketch in Playwright browser: {url}")
165
+ print(
166
+ "Auto-resize enabled: Browser window will automatically adjust to content size"
167
+ )
168
+ print(
169
+ "🔧 C++ DevTools Support extension will be loaded for DWARF debugging"
170
+ )
171
+ global _playwright_browser_proxy
172
+ _playwright_browser_proxy = open_with_playwright(
173
+ url, enable_extensions=True
174
+ )
175
+ else:
176
+ print(f"Opening browser to {url}")
177
+ import webbrowser
134
178
 
135
- t = threading.Thread(target=_background_npm_install_live_server)
136
- t.daemon = True
137
- t.start()
138
- return out
179
+ webbrowser.open(
180
+ url=url,
181
+ new=1,
182
+ autoraise=True,
183
+ )
184
+ return proc
139
185
 
140
186
 
141
187
  if __name__ == "__main__":
@@ -145,9 +191,7 @@ if __name__ == "__main__":
145
191
  description="Open a browser to the fastled_js directory"
146
192
  )
147
193
  parser.add_argument(
148
- "fastled_js",
149
- type=Path,
150
- help="Path to the fastled_js directory",
194
+ "fastled_js", type=Path, help="Path to the fastled_js directory"
151
195
  )
152
196
  parser.add_argument(
153
197
  "--port",
@@ -157,5 +201,5 @@ if __name__ == "__main__":
157
201
  )
158
202
  args = parser.parse_args()
159
203
 
160
- proc = open_browser_process(args.fastled_js, args.port, open_browser=True)
204
+ proc = spawn_http_server(args.fastled_js, args.port, open_browser=True)
161
205
  proc.join()
fastled/parse_args.py CHANGED
@@ -3,19 +3,66 @@ import os
3
3
  import sys
4
4
  from pathlib import Path
5
5
 
6
- from fastled import __version__
6
+ from fastled.args import Args
7
7
  from fastled.project_init import project_init
8
8
  from fastled.select_sketch_directory import select_sketch_directory
9
9
  from fastled.settings import DEFAULT_URL, IMAGE_NAME
10
10
  from fastled.sketch import (
11
+ find_sketch_by_partial_name,
11
12
  find_sketch_directories,
12
13
  looks_like_fastled_repo,
13
14
  looks_like_sketch_directory,
14
15
  )
15
16
 
16
17
 
17
- def parse_args() -> argparse.Namespace:
18
+ def _find_fastled_repo(start: Path) -> Path | None:
19
+ """Find the FastLED repo directory by searching upwards from the current directory."""
20
+ current = start
21
+ while current != current.parent:
22
+ if looks_like_fastled_repo(current):
23
+ return current
24
+ current = current.parent
25
+ return None
26
+
27
+
28
+ _DEFAULT_HELP_TEXT = """
29
+ FastLED WASM Compiler - Useful options:
30
+ <directory> Directory containing the FastLED sketch to compile
31
+ --init [example] Initialize one of the top tier WASM examples
32
+ --web [url] Use web compiler
33
+ --server Run the compiler server
34
+ --no-platformio Bypass PlatformIO constraints using local Docker compilation
35
+ --quick Build in quick mode (default)
36
+ --profile Enable profiling the C++ build system
37
+ --update Update the docker image for the wasm compiler
38
+ --background-update Update the docker image in the background after compilation
39
+ --purge Remove all FastLED containers and images
40
+ --emsdk-headers <path> Export EMSDK headers ZIP to specified path
41
+ --version Show version information
42
+ --help Show detailed help
43
+ Examples:
44
+ fastled (will auto detect the sketch directory and prompt you)
45
+ fastled my_sketch
46
+ fastled my_sketch --web (compiles using the web compiler only)
47
+ fastled my_sketch --background-update (compiles and updates docker image in background)
48
+ fastled --init Blink (initializes a new sketch directory with the Blink example)
49
+ fastled --server (runs the compiler server in the current directory)
50
+
51
+ For those using Docker:
52
+ --debug Build with debug symbols for dev-tools debugging
53
+
54
+ --release Build in optimized release mode
55
+ """
56
+
57
+
58
+ def parse_args() -> Args:
18
59
  """Parse command-line arguments."""
60
+ from fastled import __version__
61
+
62
+ # Check if no arguments were provided
63
+ if len(sys.argv) == 1:
64
+ print(_DEFAULT_HELP_TEXT)
65
+
19
66
  parser = argparse.ArgumentParser(description=f"FastLED WASM Compiler {__version__}")
20
67
  parser.add_argument("--version", action="version", version=f"{__version__}")
21
68
  parser.add_argument(
@@ -36,6 +83,12 @@ def parse_args() -> argparse.Namespace:
36
83
  action="store_true",
37
84
  help="Just compile, skip opening the browser and watching for changes.",
38
85
  )
86
+ parser.add_argument(
87
+ "--ram-disk-size",
88
+ type=str,
89
+ default="1gb",
90
+ help="Size of the RAM disk for compilation (e.g., '1gb', '512mb')",
91
+ )
39
92
  parser.add_argument(
40
93
  "--web",
41
94
  "-w",
@@ -54,7 +107,7 @@ def parse_args() -> argparse.Namespace:
54
107
  parser.add_argument(
55
108
  "--profile",
56
109
  action="store_true",
57
- help="Enable profiling for web compilation",
110
+ help="Enable profiling of the C++ build system used for wasm compilation.",
58
111
  )
59
112
  parser.add_argument(
60
113
  "--force-compile",
@@ -64,20 +117,36 @@ def parse_args() -> argparse.Namespace:
64
117
  parser.add_argument(
65
118
  "--no-auto-updates",
66
119
  action="store_true",
67
- help="Disable automatic updates of the wasm compiler image when using docker.",
120
+ help="Disable automatic updates of the wasm compiler image when using docker. (Default: False)",
121
+ )
122
+ parser.add_argument(
123
+ "--no-platformio",
124
+ action="store_true",
125
+ help="Bypass PlatformIO constraints by using local Docker compilation with custom build environment",
126
+ )
127
+ parser.add_argument(
128
+ "--app",
129
+ action="store_true",
130
+ help="Use Playwright app-like browser experience (will download browsers if needed)",
68
131
  )
69
132
  parser.add_argument(
133
+ "-u",
70
134
  "--update",
71
135
  "--upgrade",
72
136
  action="store_true",
73
137
  help="Update the wasm compiler (if necessary) before running",
74
138
  )
139
+ parser.add_argument(
140
+ "--background-update",
141
+ action="store_true",
142
+ help="Update the docker image in the background after compilation (user doesn't have to wait)",
143
+ )
75
144
  parser.add_argument(
76
145
  "--localhost",
77
146
  "--local",
78
147
  "-l",
79
148
  action="store_true",
80
- help="Use localhost for web compilation from an instance of fastled --server, creating it if necessary",
149
+ help="(Default): Use localhost for web compilation from an instance of fastled --server, creating it if necessary",
81
150
  )
82
151
  parser.add_argument(
83
152
  "--build",
@@ -97,12 +166,41 @@ def parse_args() -> argparse.Namespace:
97
166
  help="Remove all FastLED containers and images",
98
167
  )
99
168
 
169
+ parser.add_argument(
170
+ "--clear",
171
+ action="store_true",
172
+ help="Remove all FastLED containers and images",
173
+ )
174
+
175
+ parser.add_argument(
176
+ "--install",
177
+ action="store_true",
178
+ help="Install FastLED development environment with VSCode configuration and Auto Debug extension",
179
+ )
180
+
181
+ parser.add_argument(
182
+ "--dry-run",
183
+ action="store_true",
184
+ help="Run in dry-run mode (simulate actions without making changes)",
185
+ )
186
+
187
+ parser.add_argument(
188
+ "--no-interactive",
189
+ action="store_true",
190
+ help="Run in non-interactive mode (fail instead of prompting for input)",
191
+ )
192
+ parser.add_argument(
193
+ "--emsdk-headers",
194
+ type=str,
195
+ default=None,
196
+ help="Export EMSDK headers ZIP to specified path",
197
+ )
198
+
100
199
  build_mode = parser.add_mutually_exclusive_group()
101
200
  build_mode.add_argument("--debug", action="store_true", help="Build in debug mode")
102
201
  build_mode.add_argument(
103
202
  "--quick",
104
203
  action="store_true",
105
- default=True,
106
204
  help="Build in quick mode (default)",
107
205
  )
108
206
  build_mode.add_argument(
@@ -113,6 +211,62 @@ def parse_args() -> argparse.Namespace:
113
211
 
114
212
  args = parser.parse_args()
115
213
 
214
+ # Handle --emsdk-headers early before other processing
215
+ if args.emsdk_headers:
216
+ from fastled.header_dump import dump_emsdk_headers
217
+
218
+ out_path = args.emsdk_headers
219
+ dump_emsdk_headers(out_path)
220
+ sys.exit(0)
221
+
222
+ # Auto-enable app mode if debug is used and Playwright cache exists
223
+ if args.debug and not args.app:
224
+ playwright_dir = Path.home() / ".fastled" / "playwright"
225
+ if playwright_dir.exists() and any(playwright_dir.iterdir()):
226
+ from fastled.emoji_util import EMO
227
+
228
+ print(
229
+ f"{EMO('⚠️', 'WARNING:')} Debug mode detected with Playwright installed - automatically enabling app mode"
230
+ )
231
+ args.app = True
232
+ elif not args.no_interactive:
233
+ # Prompt user to install Playwright only if not in no-interactive mode
234
+ answer = (
235
+ input("Would you like to install the FastLED debugger? [y/n] ")
236
+ .strip()
237
+ .lower()
238
+ )
239
+ if answer in ["y", "yes"]:
240
+ print(
241
+ "📦 To install Playwright, run: pip install playwright && python -m playwright install"
242
+ )
243
+ print("Then run your command again with --app flag")
244
+ sys.exit(0)
245
+
246
+ # TODO: propagate the library.
247
+ # from fastled.docker_manager import force_remove_previous
248
+
249
+ # if force_remove_previous():
250
+ # print("Removing previous containers...")
251
+ # do itinfront he camer
252
+ # nonw invoke via the
253
+ #
254
+ # Work in progress.
255
+ # set_ramdisk_size("50mb")
256
+
257
+ # if args.ram_disk_size != "0":
258
+ # from fastled.docker_manager import set_ramdisk_size
259
+ # from fastled.util import banner_string
260
+
261
+ # msg = banner_string(f"Setting tmpfs size to {args.ram_disk_size}")
262
+ # print(msg)
263
+ # set_ramdisk_size(args.ram_disk_size)
264
+
265
+ # Handle --install early before other processing
266
+ if args.install:
267
+ # Don't process other arguments when --install is used
268
+ return Args.from_namespace(args)
269
+
116
270
  if args.purge:
117
271
  from fastled.docker_manager import DockerManager
118
272
 
@@ -131,8 +285,38 @@ def parse_args() -> argparse.Namespace:
131
285
  print(f"Use 'fastled {args.directory}' to compile the project.")
132
286
  sys.exit(0)
133
287
 
134
- if args.build:
135
- return args
288
+ cwd: Path = Path(os.getcwd())
289
+ fastled_dir: Path | None = _find_fastled_repo(cwd)
290
+ is_fastled_dir: bool = fastled_dir is not None
291
+
292
+ if not (args.debug or args.quick or args.release):
293
+ if is_fastled_dir:
294
+ # if --quick, --debug, --release are not specified then default to --debug
295
+ args.quick = True
296
+ print("Defaulting to --quick mode in fastled repo")
297
+ else:
298
+ args.quick = True
299
+ print("Defaulting to --quick mode")
300
+
301
+ if args.build or args.interactive:
302
+ if args.directory is not None:
303
+ args.directory = str(Path(args.directory).absolute())
304
+ if not is_fastled_dir:
305
+ print("This command must be run from within the FastLED repo. Exiting...")
306
+ sys.exit(1)
307
+ if cwd != fastled_dir and fastled_dir is not None:
308
+ print(f"Switching to FastLED repo at {fastled_dir}")
309
+ os.chdir(fastled_dir)
310
+ if args.directory is None:
311
+ args.directory = str(Path("examples/wasm").absolute())
312
+ if args.interactive:
313
+ if not args.build:
314
+ print("Adding --build flag when using --interactive")
315
+ args.build = True
316
+ user_wants_update = args.update
317
+ if user_wants_update is not True:
318
+ args.no_auto_updates = True
319
+ return Args.from_namespace(args)
136
320
 
137
321
  if not args.update:
138
322
  if args.no_auto_updates:
@@ -165,6 +349,12 @@ def parse_args() -> argparse.Namespace:
165
349
  if cwd_is_fastled and not args.web and not args.server:
166
350
  print("Forcing --local mode because we are in the FastLED repo")
167
351
  args.localhost = True
352
+ if args.no_platformio:
353
+ print(
354
+ "--no-platformio mode enabled: forcing local Docker compilation to bypass PlatformIO constraints"
355
+ )
356
+ args.localhost = True
357
+ args.web = None # Clear web flag to ensure local compilation
168
358
  if args.localhost:
169
359
  args.web = "localhost"
170
360
  if args.interactive and not args.server:
@@ -179,7 +369,7 @@ def parse_args() -> argparse.Namespace:
179
369
  print("Searching for sketch directories...")
180
370
  sketch_directories = find_sketch_directories(maybe_sketch_dir)
181
371
  selected_dir = select_sketch_directory(
182
- sketch_directories, cwd_is_fastled
372
+ sketch_directories, cwd_is_fastled, is_followup=True
183
373
  )
184
374
  if selected_dir:
185
375
  print(f"Using sketch directory: {selected_dir}")
@@ -189,10 +379,24 @@ def parse_args() -> argparse.Namespace:
189
379
  "\nYou either need to specify a sketch directory or run in --server mode."
190
380
  )
191
381
  sys.exit(1)
192
- elif args.directory is not None and os.path.isfile(args.directory):
193
- dir_path = Path(args.directory).parent
194
- if looks_like_sketch_directory(dir_path):
195
- print(f"Using sketch directory: {dir_path}")
196
- args.directory = str(dir_path)
382
+ elif args.directory is not None:
383
+ # Check if directory is a file path
384
+ if os.path.isfile(args.directory):
385
+ dir_path = Path(args.directory).parent
386
+ if looks_like_sketch_directory(dir_path):
387
+ print(f"Using sketch directory: {dir_path}")
388
+ args.directory = str(dir_path)
389
+ # Check if directory exists as a path
390
+ elif not os.path.exists(args.directory):
391
+ # Directory doesn't exist - try partial name matching
392
+ try:
393
+ matched_dir = find_sketch_by_partial_name(args.directory)
394
+ print(
395
+ f"Matched '{args.directory}' to sketch directory: {matched_dir}"
396
+ )
397
+ args.directory = str(matched_dir)
398
+ except ValueError as e:
399
+ print(f"Error: {e}")
400
+ sys.exit(1)
197
401
 
198
- return args
402
+ return Args.from_namespace(args)