adds pio helper script for frequent commands
This commit is contained in:
parent
5fd1da5d2e
commit
9a723fead8
727
scripts/pio_helper.py
Executable file
727
scripts/pio_helper.py
Executable file
@ -0,0 +1,727 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
PlatformIO Helper - An all-in-one interface for common PlatformIO commands.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Interactive menu mode (when run without args)
|
||||||
|
- CLI mode for quick access
|
||||||
|
- fzf integration for fuzzy selection
|
||||||
|
- Custom preset/workflow support
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
./scripts/pio_helper.py # Interactive menu
|
||||||
|
./scripts/pio_helper.py upload # Quick upload
|
||||||
|
./scripts/pio_helper.py upload -e debug_memory
|
||||||
|
./scripts/pio_helper.py clean+upload+debug-monitor -l
|
||||||
|
./scripts/pio_helper.py --preset dev
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Constants
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
SCRIPT_DIR = Path(__file__).parent.resolve()
|
||||||
|
PROJECT_ROOT = SCRIPT_DIR.parent
|
||||||
|
DEBUGGING_MONITOR = SCRIPT_DIR / "debugging_monitor.py"
|
||||||
|
DEFAULT_LOG_PATH = PROJECT_ROOT / ".cursor" / "debug.log"
|
||||||
|
PRESETS_DIR = Path.home() / ".config" / "pio-helper"
|
||||||
|
PRESETS_FILE = PRESETS_DIR / "presets.json"
|
||||||
|
|
||||||
|
# Available environments from platformio.ini
|
||||||
|
ENVIRONMENTS = ["default", "debug_memory", "gh_release"]
|
||||||
|
DEFAULT_ENV = "default"
|
||||||
|
|
||||||
|
# Action definitions
|
||||||
|
ACTIONS = {
|
||||||
|
"build": {
|
||||||
|
"description": "Build without upload",
|
||||||
|
"command": ["pio", "run"],
|
||||||
|
"needs_env": True,
|
||||||
|
},
|
||||||
|
"upload": {
|
||||||
|
"description": "Build and upload to device",
|
||||||
|
"command": ["pio", "run", "-t", "upload"],
|
||||||
|
"needs_env": True,
|
||||||
|
},
|
||||||
|
"clean": {
|
||||||
|
"description": "Clean build files",
|
||||||
|
"command": ["pio", "run", "-t", "clean"],
|
||||||
|
"needs_env": True,
|
||||||
|
},
|
||||||
|
"monitor": {
|
||||||
|
"description": "Basic serial monitor",
|
||||||
|
"command": ["pio", "device", "monitor"],
|
||||||
|
"needs_env": False,
|
||||||
|
},
|
||||||
|
"debug-monitor": {
|
||||||
|
"description": "Enhanced monitor with memory graphs",
|
||||||
|
"command": [sys.executable, str(DEBUGGING_MONITOR)],
|
||||||
|
"needs_env": False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Common workflow shortcuts for the menu
|
||||||
|
MENU_WORKFLOWS = [
|
||||||
|
{"name": "Build only", "actions": ["build"]},
|
||||||
|
{"name": "Upload (default env)", "actions": ["upload"], "env": "default"},
|
||||||
|
{"name": "Upload (debug_memory env)", "actions": ["upload"], "env": "debug_memory"},
|
||||||
|
{"name": "Clean + Upload", "actions": ["clean", "upload"]},
|
||||||
|
{"name": "Upload + Monitor", "actions": ["upload", "monitor"]},
|
||||||
|
{"name": "Upload + Debug Monitor", "actions": ["upload", "debug-monitor"]},
|
||||||
|
{"name": "Clean + Upload + Debug Monitor", "actions": ["clean", "upload", "debug-monitor"]},
|
||||||
|
{"name": "Monitor only", "actions": ["monitor"]},
|
||||||
|
{"name": "Debug Monitor only", "actions": ["debug-monitor"]},
|
||||||
|
]
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Terminal Colors
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class Colors:
|
||||||
|
"""ANSI color codes for terminal output."""
|
||||||
|
RESET = "\033[0m"
|
||||||
|
BOLD = "\033[1m"
|
||||||
|
DIM = "\033[2m"
|
||||||
|
|
||||||
|
RED = "\033[31m"
|
||||||
|
GREEN = "\033[32m"
|
||||||
|
YELLOW = "\033[33m"
|
||||||
|
BLUE = "\033[34m"
|
||||||
|
MAGENTA = "\033[35m"
|
||||||
|
CYAN = "\033[36m"
|
||||||
|
WHITE = "\033[37m"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def disable(cls):
|
||||||
|
"""Disable colors (for non-TTY output)."""
|
||||||
|
for attr in dir(cls):
|
||||||
|
if not attr.startswith('_') and isinstance(getattr(cls, attr), str):
|
||||||
|
setattr(cls, attr, "")
|
||||||
|
|
||||||
|
|
||||||
|
# Disable colors if not a TTY
|
||||||
|
if not sys.stdout.isatty():
|
||||||
|
Colors.disable()
|
||||||
|
|
||||||
|
|
||||||
|
def print_header(text: str):
|
||||||
|
"""Print a styled header."""
|
||||||
|
print(f"\n{Colors.CYAN}{Colors.BOLD}{text}{Colors.RESET}")
|
||||||
|
print(f"{Colors.DIM}{'─' * len(text)}{Colors.RESET}")
|
||||||
|
|
||||||
|
|
||||||
|
def print_success(text: str):
|
||||||
|
"""Print success message."""
|
||||||
|
print(f"{Colors.GREEN}✓ {text}{Colors.RESET}")
|
||||||
|
|
||||||
|
|
||||||
|
def print_error(text: str):
|
||||||
|
"""Print error message."""
|
||||||
|
print(f"{Colors.RED}✗ {text}{Colors.RESET}")
|
||||||
|
|
||||||
|
|
||||||
|
def print_info(text: str):
|
||||||
|
"""Print info message."""
|
||||||
|
print(f"{Colors.BLUE}→ {text}{Colors.RESET}")
|
||||||
|
|
||||||
|
|
||||||
|
def print_warning(text: str):
|
||||||
|
"""Print warning message."""
|
||||||
|
print(f"{Colors.YELLOW}! {text}{Colors.RESET}")
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Preset Management
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def load_presets() -> dict:
|
||||||
|
"""Load presets from the config file."""
|
||||||
|
if not PRESETS_FILE.exists():
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
with open(PRESETS_FILE, "r") as f:
|
||||||
|
return json.load(f)
|
||||||
|
except (json.JSONDecodeError, IOError) as e:
|
||||||
|
print_warning(f"Could not load presets: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def save_presets(presets: dict):
|
||||||
|
"""Save presets to the config file."""
|
||||||
|
PRESETS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(PRESETS_FILE, "w") as f:
|
||||||
|
json.dump(presets, f, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
def save_preset(name: str, actions: list[str], env: str, log: bool, log_path: Optional[str] = None):
|
||||||
|
"""Save a new preset."""
|
||||||
|
presets = load_presets()
|
||||||
|
presets[name] = {
|
||||||
|
"actions": actions,
|
||||||
|
"env": env,
|
||||||
|
"log": log,
|
||||||
|
}
|
||||||
|
if log_path:
|
||||||
|
presets[name]["log_path"] = log_path
|
||||||
|
save_presets(presets)
|
||||||
|
print_success(f"Saved preset '{name}'")
|
||||||
|
|
||||||
|
|
||||||
|
def delete_preset(name: str) -> bool:
|
||||||
|
"""Delete a preset."""
|
||||||
|
presets = load_presets()
|
||||||
|
if name in presets:
|
||||||
|
del presets[name]
|
||||||
|
save_presets(presets)
|
||||||
|
print_success(f"Deleted preset '{name}'")
|
||||||
|
return True
|
||||||
|
print_error(f"Preset '{name}' not found")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def list_presets():
|
||||||
|
"""List all saved presets."""
|
||||||
|
presets = load_presets()
|
||||||
|
if not presets:
|
||||||
|
print_info("No presets saved yet.")
|
||||||
|
print_info(f"Presets are stored in: {PRESETS_FILE}")
|
||||||
|
return
|
||||||
|
|
||||||
|
print_header("Saved Presets")
|
||||||
|
for name, config in presets.items():
|
||||||
|
actions_str = " + ".join(config.get("actions", []))
|
||||||
|
env = config.get("env", DEFAULT_ENV)
|
||||||
|
log = "with logging" if config.get("log") else ""
|
||||||
|
print(f" {Colors.GREEN}{name}{Colors.RESET}: {actions_str} ({env}) {log}")
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# fzf Integration
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def has_fzf() -> bool:
|
||||||
|
"""Check if fzf is available."""
|
||||||
|
return shutil.which("fzf") is not None
|
||||||
|
|
||||||
|
|
||||||
|
def fzf_select(options: list[str], prompt: str = "Select: ", multi: bool = False) -> Optional[list[str]]:
|
||||||
|
"""
|
||||||
|
Use fzf for selection. Returns list of selected options or None if cancelled.
|
||||||
|
Falls back to numbered menu if fzf is not available.
|
||||||
|
"""
|
||||||
|
if not has_fzf():
|
||||||
|
return numbered_select(options, prompt, multi)
|
||||||
|
|
||||||
|
try:
|
||||||
|
fzf_args = ["fzf", "--prompt", prompt, "--height", "40%", "--reverse"]
|
||||||
|
if multi:
|
||||||
|
fzf_args.append("--multi")
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
fzf_args,
|
||||||
|
input="\n".join(options),
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
selected = result.stdout.strip().split("\n")
|
||||||
|
return [s for s in selected if s]
|
||||||
|
return None
|
||||||
|
except (subprocess.SubprocessError, OSError):
|
||||||
|
return numbered_select(options, prompt, multi)
|
||||||
|
|
||||||
|
|
||||||
|
def numbered_select(options: list[str], prompt: str = "Select: ", multi: bool = False) -> Optional[list[str]]:
|
||||||
|
"""Fallback numbered selection menu."""
|
||||||
|
print()
|
||||||
|
for i, opt in enumerate(options, 1):
|
||||||
|
print(f" {Colors.CYAN}{i:2}{Colors.RESET}. {opt}")
|
||||||
|
print(f" {Colors.DIM} 0. Cancel{Colors.RESET}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if multi:
|
||||||
|
print_info("Enter numbers separated by spaces (e.g., '1 3 5')")
|
||||||
|
choice = input(f"{prompt}").strip()
|
||||||
|
|
||||||
|
if not choice or choice == "0":
|
||||||
|
return None
|
||||||
|
|
||||||
|
if multi:
|
||||||
|
indices = [int(c) - 1 for c in choice.split()]
|
||||||
|
return [options[i] for i in indices if 0 <= i < len(options)]
|
||||||
|
else:
|
||||||
|
idx = int(choice) - 1
|
||||||
|
if 0 <= idx < len(options):
|
||||||
|
return [options[idx]]
|
||||||
|
return None
|
||||||
|
except (ValueError, IndexError, KeyboardInterrupt):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Action Execution
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def build_command(action: str, env: str, verbose: bool = False) -> list[str]:
|
||||||
|
"""Build the command for a given action."""
|
||||||
|
if action not in ACTIONS:
|
||||||
|
raise ValueError(f"Unknown action: {action}")
|
||||||
|
|
||||||
|
action_def = ACTIONS[action]
|
||||||
|
cmd = list(action_def["command"])
|
||||||
|
|
||||||
|
# Add environment flag if needed
|
||||||
|
if action_def["needs_env"]:
|
||||||
|
cmd.extend(["-e", env])
|
||||||
|
|
||||||
|
# Add verbose flag
|
||||||
|
if verbose and action in ("build", "upload", "clean"):
|
||||||
|
cmd.append("-v")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
|
||||||
|
|
||||||
|
def run_action(action: str, env: str, log: bool = False, log_path: Optional[Path] = None,
|
||||||
|
verbose: bool = False) -> int:
|
||||||
|
"""
|
||||||
|
Run a single action.
|
||||||
|
|
||||||
|
Returns the exit code of the command.
|
||||||
|
"""
|
||||||
|
cmd = build_command(action, env, verbose)
|
||||||
|
action_def = ACTIONS[action]
|
||||||
|
|
||||||
|
print_info(f"Running: {action} ({action_def['description']})")
|
||||||
|
print(f"{Colors.DIM} $ {' '.join(cmd)}{Colors.RESET}")
|
||||||
|
|
||||||
|
if log:
|
||||||
|
actual_log_path = log_path or DEFAULT_LOG_PATH
|
||||||
|
actual_log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
print_info(f"Logging to: {actual_log_path}")
|
||||||
|
|
||||||
|
# Use tee to both display and log output
|
||||||
|
with open(actual_log_path, "a") as log_file:
|
||||||
|
# Write a separator to the log
|
||||||
|
log_file.write(f"\n{'='*60}\n")
|
||||||
|
log_file.write(f"Action: {action} | Env: {env}\n")
|
||||||
|
log_file.write(f"Command: {' '.join(cmd)}\n")
|
||||||
|
log_file.write(f"{'='*60}\n")
|
||||||
|
log_file.flush()
|
||||||
|
|
||||||
|
# Run with tee-like behavior
|
||||||
|
process = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
text=True,
|
||||||
|
cwd=PROJECT_ROOT,
|
||||||
|
)
|
||||||
|
|
||||||
|
for line in iter(process.stdout.readline, ""):
|
||||||
|
sys.stdout.write(line)
|
||||||
|
sys.stdout.flush()
|
||||||
|
log_file.write(line)
|
||||||
|
log_file.flush()
|
||||||
|
|
||||||
|
process.wait()
|
||||||
|
return process.returncode
|
||||||
|
else:
|
||||||
|
# Run directly without logging
|
||||||
|
result = subprocess.run(cmd, cwd=PROJECT_ROOT)
|
||||||
|
return result.returncode
|
||||||
|
|
||||||
|
|
||||||
|
def run_chain(actions: list[str], env: str, log: bool = False,
|
||||||
|
log_path: Optional[Path] = None, verbose: bool = False) -> int:
|
||||||
|
"""
|
||||||
|
Run a chain of actions sequentially.
|
||||||
|
|
||||||
|
Stops on first failure and returns the exit code.
|
||||||
|
"""
|
||||||
|
print_header(f"Running: {' + '.join(actions)}")
|
||||||
|
print_info(f"Environment: {env}")
|
||||||
|
if log:
|
||||||
|
print_info(f"Logging enabled")
|
||||||
|
print()
|
||||||
|
|
||||||
|
for i, action in enumerate(actions):
|
||||||
|
if i > 0:
|
||||||
|
print() # Spacing between actions
|
||||||
|
|
||||||
|
exit_code = run_action(action, env, log, log_path, verbose)
|
||||||
|
|
||||||
|
if exit_code != 0:
|
||||||
|
print_error(f"Action '{action}' failed with exit code {exit_code}")
|
||||||
|
return exit_code
|
||||||
|
|
||||||
|
print_success(f"Action '{action}' completed successfully")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print_success(f"All actions completed successfully!")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Interactive Menu
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def select_environment() -> Optional[str]:
|
||||||
|
"""Prompt user to select an environment."""
|
||||||
|
result = fzf_select(ENVIRONMENTS, "Select environment: ")
|
||||||
|
return result[0] if result else None
|
||||||
|
|
||||||
|
|
||||||
|
def interactive_menu():
|
||||||
|
"""Run the interactive menu interface."""
|
||||||
|
while True:
|
||||||
|
print_header("PlatformIO Helper")
|
||||||
|
|
||||||
|
# Build menu options
|
||||||
|
options = []
|
||||||
|
for wf in MENU_WORKFLOWS:
|
||||||
|
env_info = f" [{wf.get('env', 'select env')}]" if 'env' in wf else ""
|
||||||
|
options.append(f"{wf['name']}{env_info}")
|
||||||
|
|
||||||
|
options.append("─" * 30) # Separator
|
||||||
|
options.append("📋 Run preset...")
|
||||||
|
options.append("💾 Save workflow as preset...")
|
||||||
|
options.append("📝 List presets")
|
||||||
|
options.append("🗑️ Delete preset...")
|
||||||
|
options.append("─" * 30) # Separator
|
||||||
|
options.append("⚙️ Custom: Select actions...")
|
||||||
|
options.append("❌ Quit")
|
||||||
|
|
||||||
|
# Use fzf or numbered menu
|
||||||
|
if has_fzf():
|
||||||
|
result = fzf_select(options, "Select workflow: ")
|
||||||
|
if not result:
|
||||||
|
continue
|
||||||
|
choice = result[0]
|
||||||
|
else:
|
||||||
|
print()
|
||||||
|
for i, opt in enumerate(options, 1):
|
||||||
|
if opt.startswith("─"):
|
||||||
|
print(f" {Colors.DIM}{opt}{Colors.RESET}")
|
||||||
|
else:
|
||||||
|
print(f" {Colors.CYAN}{i:2}{Colors.RESET}. {opt}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
try:
|
||||||
|
choice_input = input(f"Select (1-{len(options)}, q to quit): ").strip().lower()
|
||||||
|
if choice_input in ("q", "quit", "exit", ""):
|
||||||
|
break
|
||||||
|
idx = int(choice_input) - 1
|
||||||
|
if 0 <= idx < len(options):
|
||||||
|
choice = options[idx]
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
except (ValueError, KeyboardInterrupt):
|
||||||
|
break
|
||||||
|
|
||||||
|
# Handle choice
|
||||||
|
if choice.startswith("─") or not choice:
|
||||||
|
continue
|
||||||
|
elif "Quit" in choice:
|
||||||
|
break
|
||||||
|
elif "Run preset" in choice:
|
||||||
|
run_preset_menu()
|
||||||
|
elif "Save workflow" in choice:
|
||||||
|
save_preset_menu()
|
||||||
|
elif "List presets" in choice:
|
||||||
|
list_presets()
|
||||||
|
input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
|
||||||
|
elif "Delete preset" in choice:
|
||||||
|
delete_preset_menu()
|
||||||
|
elif "Custom" in choice:
|
||||||
|
custom_workflow_menu()
|
||||||
|
else:
|
||||||
|
# Find matching workflow
|
||||||
|
for wf in MENU_WORKFLOWS:
|
||||||
|
env_info = f" [{wf.get('env', 'select env')}]" if 'env' in wf else ""
|
||||||
|
if choice == f"{wf['name']}{env_info}":
|
||||||
|
env = wf.get("env")
|
||||||
|
if env is None:
|
||||||
|
env = select_environment()
|
||||||
|
if env is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Ask about logging
|
||||||
|
log = ask_yes_no("Enable logging?", default=False)
|
||||||
|
|
||||||
|
run_chain(wf["actions"], env, log)
|
||||||
|
input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def ask_yes_no(prompt: str, default: bool = False) -> bool:
|
||||||
|
"""Ask a yes/no question."""
|
||||||
|
default_str = "Y/n" if default else "y/N"
|
||||||
|
try:
|
||||||
|
answer = input(f"{prompt} [{default_str}]: ").strip().lower()
|
||||||
|
if not answer:
|
||||||
|
return default
|
||||||
|
return answer in ("y", "yes", "1", "true")
|
||||||
|
except (KeyboardInterrupt, EOFError):
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def run_preset_menu():
|
||||||
|
"""Menu to run a saved preset."""
|
||||||
|
presets = load_presets()
|
||||||
|
if not presets:
|
||||||
|
print_info("No presets saved yet.")
|
||||||
|
return
|
||||||
|
|
||||||
|
options = list(presets.keys())
|
||||||
|
result = fzf_select(options, "Select preset: ")
|
||||||
|
if not result:
|
||||||
|
return
|
||||||
|
|
||||||
|
name = result[0]
|
||||||
|
config = presets[name]
|
||||||
|
|
||||||
|
actions = config.get("actions", [])
|
||||||
|
env = config.get("env", DEFAULT_ENV)
|
||||||
|
log = config.get("log", False)
|
||||||
|
log_path = Path(config["log_path"]) if config.get("log_path") else None
|
||||||
|
|
||||||
|
run_chain(actions, env, log, log_path)
|
||||||
|
input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
|
||||||
|
|
||||||
|
|
||||||
|
def save_preset_menu():
|
||||||
|
"""Menu to save a new preset."""
|
||||||
|
# Select actions
|
||||||
|
action_names = list(ACTIONS.keys())
|
||||||
|
print_info("Select actions to include in the preset:")
|
||||||
|
selected = fzf_select(action_names, "Select actions (multi): ", multi=True)
|
||||||
|
if not selected:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Select environment
|
||||||
|
env = select_environment()
|
||||||
|
if not env:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Ask about logging
|
||||||
|
log = ask_yes_no("Enable logging by default?", default=False)
|
||||||
|
|
||||||
|
# Get preset name
|
||||||
|
try:
|
||||||
|
name = input("Preset name: ").strip()
|
||||||
|
if not name:
|
||||||
|
print_error("Preset name cannot be empty")
|
||||||
|
return
|
||||||
|
except (KeyboardInterrupt, EOFError):
|
||||||
|
return
|
||||||
|
|
||||||
|
save_preset(name, selected, env, log)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_preset_menu():
|
||||||
|
"""Menu to delete a preset."""
|
||||||
|
presets = load_presets()
|
||||||
|
if not presets:
|
||||||
|
print_info("No presets to delete.")
|
||||||
|
return
|
||||||
|
|
||||||
|
options = list(presets.keys())
|
||||||
|
result = fzf_select(options, "Select preset to delete: ")
|
||||||
|
if not result:
|
||||||
|
return
|
||||||
|
|
||||||
|
name = result[0]
|
||||||
|
if ask_yes_no(f"Delete preset '{name}'?", default=False):
|
||||||
|
delete_preset(name)
|
||||||
|
|
||||||
|
|
||||||
|
def custom_workflow_menu():
|
||||||
|
"""Menu to create a custom workflow."""
|
||||||
|
# Select actions
|
||||||
|
action_names = list(ACTIONS.keys())
|
||||||
|
print_info("Select actions to run (in order):")
|
||||||
|
selected = fzf_select(action_names, "Select actions (multi): ", multi=True)
|
||||||
|
if not selected:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Select environment
|
||||||
|
env = select_environment()
|
||||||
|
if not env:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Ask about logging
|
||||||
|
log = ask_yes_no("Enable logging?", default=False)
|
||||||
|
|
||||||
|
run_chain(selected, env, log)
|
||||||
|
|
||||||
|
# Offer to save as preset
|
||||||
|
if ask_yes_no("\nSave this as a preset?", default=False):
|
||||||
|
try:
|
||||||
|
name = input("Preset name: ").strip()
|
||||||
|
if name:
|
||||||
|
save_preset(name, selected, env, log)
|
||||||
|
except (KeyboardInterrupt, EOFError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# CLI Interface
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def parse_actions(action_str: str) -> list[str]:
|
||||||
|
"""Parse action string like 'clean+upload+monitor' into list of actions."""
|
||||||
|
actions = [a.strip() for a in action_str.split("+")]
|
||||||
|
|
||||||
|
# Validate actions
|
||||||
|
for action in actions:
|
||||||
|
if action not in ACTIONS:
|
||||||
|
print_error(f"Unknown action: {action}")
|
||||||
|
print_info(f"Available actions: {', '.join(ACTIONS.keys())}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
return actions
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="PlatformIO Helper - An all-in-one interface for common PlatformIO commands.",
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
epilog="""
|
||||||
|
Examples:
|
||||||
|
%(prog)s Interactive menu
|
||||||
|
%(prog)s upload Upload with default env
|
||||||
|
%(prog)s upload -e debug_memory Upload with debug_memory env
|
||||||
|
%(prog)s clean+upload+monitor Chain multiple actions
|
||||||
|
%(prog)s upload+debug-monitor -l Upload and monitor with logging
|
||||||
|
%(prog)s --preset dev Run saved preset 'dev'
|
||||||
|
%(prog)s --list-presets List all saved presets
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
build Build without upload
|
||||||
|
upload Build and upload to device
|
||||||
|
clean Clean build files
|
||||||
|
monitor Basic serial monitor (pio device monitor)
|
||||||
|
debug-monitor Enhanced monitor with memory graphs
|
||||||
|
|
||||||
|
Environments:
|
||||||
|
default Standard development build
|
||||||
|
debug_memory Debug build with memory tracking
|
||||||
|
gh_release Release build
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"actions",
|
||||||
|
nargs="?",
|
||||||
|
help="Action(s) to run, can be chained with '+' (e.g., 'clean+upload+monitor')"
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"-e", "--env",
|
||||||
|
choices=ENVIRONMENTS,
|
||||||
|
default=DEFAULT_ENV,
|
||||||
|
help=f"PlatformIO environment (default: {DEFAULT_ENV})"
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"-l", "--log",
|
||||||
|
nargs="?",
|
||||||
|
const=str(DEFAULT_LOG_PATH),
|
||||||
|
metavar="PATH",
|
||||||
|
help=f"Log output to file (default: {DEFAULT_LOG_PATH})"
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"-v", "--verbose",
|
||||||
|
action="store_true",
|
||||||
|
help="Verbose output"
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--preset",
|
||||||
|
metavar="NAME",
|
||||||
|
help="Run a saved preset"
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--list-presets",
|
||||||
|
action="store_true",
|
||||||
|
help="List all saved presets"
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--save-preset",
|
||||||
|
metavar="NAME",
|
||||||
|
help="Save the current command as a preset"
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--delete-preset",
|
||||||
|
metavar="NAME",
|
||||||
|
help="Delete a saved preset"
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Handle preset listing
|
||||||
|
if args.list_presets:
|
||||||
|
list_presets()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Handle preset deletion
|
||||||
|
if args.delete_preset:
|
||||||
|
return 0 if delete_preset(args.delete_preset) else 1
|
||||||
|
|
||||||
|
# Handle preset execution
|
||||||
|
if args.preset:
|
||||||
|
presets = load_presets()
|
||||||
|
if args.preset not in presets:
|
||||||
|
print_error(f"Preset '{args.preset}' not found")
|
||||||
|
list_presets()
|
||||||
|
return 1
|
||||||
|
|
||||||
|
config = presets[args.preset]
|
||||||
|
actions = config.get("actions", [])
|
||||||
|
env = config.get("env", DEFAULT_ENV)
|
||||||
|
log = config.get("log", False)
|
||||||
|
log_path = Path(config["log_path"]) if config.get("log_path") else None
|
||||||
|
|
||||||
|
return run_chain(actions, env, log, log_path, args.verbose)
|
||||||
|
|
||||||
|
# Interactive mode if no actions specified
|
||||||
|
if not args.actions:
|
||||||
|
try:
|
||||||
|
interactive_menu()
|
||||||
|
return 0
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Parse and run actions
|
||||||
|
actions = parse_actions(args.actions)
|
||||||
|
log = args.log is not None
|
||||||
|
log_path = Path(args.log) if args.log else None
|
||||||
|
|
||||||
|
# Handle save preset
|
||||||
|
if args.save_preset:
|
||||||
|
save_preset(args.save_preset, actions, args.env, log, args.log)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
return run_chain(actions, args.env, log, log_path, args.verbose)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Loading…
x
Reference in New Issue
Block a user