1 import { Uri, commands, window } from "vscode";
2 import { Command } from "./command.mjs";
3 import Logger from "../logger.mjs";
4 import { dirname, join } from "path";
5 import { fileURLToPath } from "url";
6 import { type ExecOptions, exec } from "child_process";
7 import { detectInstalledSDKs } from "../utils/picoSDKUtil.mjs";
8 import type Settings from "../settings.mjs";
11 showRquirementsNotMetErrorMessage,
12 } from "../utils/requirementsUtil.mjs";
13 import { compare } from "semver";
14 import { SettingsKey } from "../settings.mjs";
16 generateNewEnvVarSuffix,
18 } from "../utils/globalEnvironmentUtil.mjs";
26 consoleOverUART = "Console over UART",
27 consoleOverUSB = "Console over USB (disables other USB use)",
35 interp = "HW interpolation",
37 watch = "HW watchdog",
41 function enumToParam(e: BoardType | ConsoleOptions | Libraries): string {
46 return "-board pico_w";
47 case ConsoleOptions.consoleOverUART:
49 case ConsoleOptions.consoleOverUSB:
59 case Libraries.interp:
65 case Libraries.clocks:
68 throw new Error(`Unknown enum value: ${e as string}`);
72 interface NewProjectOptions {
76 consoleOptions: ConsoleOptions[];
77 libraries: Libraries[];
80 function getScriptsRoot(): string {
81 return join(dirname(fileURLToPath(import.meta.url)), "..", "scripts");
84 export default class NewProjectCommand extends Command {
85 private readonly _logger: Logger = new Logger("NewProjectCommand");
86 private readonly _settings: Settings;
88 constructor(settings: Settings) {
91 this._settings = settings;
94 async execute(): Promise<void> {
95 // check if all requirements are met
96 if (!(await checkForRequirements(this._settings))) {
97 void showRquirementsNotMetErrorMessage();
102 // TODO: maybe make it posible to also select a folder and
103 // not always create a new one with selectedName
104 const projectRoot: Uri[] | undefined = await window.showOpenDialog({
105 canSelectFiles: false,
106 canSelectFolders: true,
107 canSelectMany: false,
108 openLabel: "Select project root",
111 if (!projectRoot || projectRoot.length !== 1) {
116 const selectedName: string | undefined = await window.showInputBox({
117 placeHolder: "Enter a project name",
118 title: "New Pico Project",
125 // get board type (single selection)
126 const selectedBoardType: BoardType | undefined =
127 (await window.showQuickPick(Object.values(BoardType), {
128 placeHolder: "Select a board type",
129 title: "New Pico Project",
130 })) as BoardType | undefined;
132 if (!selectedBoardType) {
136 // [optional] get console options (multi selection)
137 const selectedConsoleOptions: ConsoleOptions[] | undefined =
138 (await window.showQuickPick(Object.values(ConsoleOptions), {
139 placeHolder: "Would you like to enable the USB console?",
140 title: "New Pico Project",
142 })) as ConsoleOptions[] | undefined;
144 if (!selectedConsoleOptions) {
148 // [optional] get libraries (multi selection)
149 const selectedLibraries: Libraries[] | undefined =
150 (await window.showQuickPick(Object.values(Libraries), {
151 placeHolder: "Select libraries to include",
152 title: "New Pico Project",
154 })) as Libraries[] | undefined;
156 if (!selectedLibraries) {
160 await this.executePicoProjectGenerator({
162 projectRoot: projectRoot[0].fsPath,
163 boardType: selectedBoardType,
164 consoleOptions: selectedConsoleOptions,
165 libraries: selectedLibraries,
169 private runGenerator(
172 ): Promise<number | null> {
173 return new Promise<number | null>(resolve => {
174 const generatorProcess = exec(command, options, error => {
176 console.error(`Error: ${error.message}`);
177 resolve(null); // Indicate error
181 generatorProcess.on("exit", code => {
182 // Resolve with exit code or -1 if code is undefined
189 * Executes the Pico Project Generator with the given options
191 * @param options {@link NewProjectOptions} to pass to the Pico Project Generator
193 private async executePicoProjectGenerator(
194 options: NewProjectOptions
196 /*const [PICO_SDK_PATH, COMPILER_PATH] = (await getSDKAndToolchainPath(
199 const installedSDKs = detectInstalledSDKs().sort((a, b) =>
200 compare(a.version.replace("v", ""), b.version.replace("v", ""))
204 installedSDKs.length === 0 ||
205 // "protection" against empty settings
206 installedSDKs[0].sdkPath === "" ||
207 installedSDKs[0].toolchainPath === ""
209 void window.showErrorMessage(
210 "Could not find Pico SDK or Toolchain. Please check the wiki."
216 const PICO_SDK_PATH = installedSDKs[0].sdkPath;
217 const TOOLCHAIN_PATH = installedSDKs[0].toolchainPath;
218 const ENV_SUFFIX = generateNewEnvVarSuffix();
219 setGlobalEnvVar(`PICO_SDK_PATH_${ENV_SUFFIX}`, PICO_SDK_PATH);
220 setGlobalEnvVar(`PICO_TOOLCHAIN_PATH_${ENV_SUFFIX}`, TOOLCHAIN_PATH);
222 const customEnv: { [key: string]: string } = {
223 ...(process.env as { [key: string]: string }),
224 // eslint-disable-next-line @typescript-eslint/naming-convention
225 [`PICO_SDK_PATH_${ENV_SUFFIX}`]: PICO_SDK_PATH,
226 // eslint-disable-next-line @typescript-eslint/naming-convention
227 [`PICO_TOOLCHAIN_PATH_${ENV_SUFFIX}`]: TOOLCHAIN_PATH,
230 process.platform === "win32" ? "Path" : "PATH"
231 ] = `${TOOLCHAIN_PATH}:${
232 customEnv[process.platform === "win32" ? "Path" : "PATH"]
235 this._settings.getString(SettingsKey.python3Path) ||
236 process.platform === "win32"
240 const command: string = [
242 join(getScriptsRoot(), "pico_project.py"),
243 enumToParam(options.boardType),
244 ...options.consoleOptions.map(option => enumToParam(option)),
245 !options.consoleOptions.includes(ConsoleOptions.consoleOverUART)
248 ...options.libraries.map(option => enumToParam(option)),
249 // generate .vscode config
253 `"${options.projectRoot}"`,
257 installedSDKs[0].version,
261 this._logger.debug(`Executing project generator command: ${command}`);
264 // TODO: use exit codes to determine why the project generator failed (if it did)
265 // to be able to show the user a more detailed error message
266 const generatorExitCode = await this.runGenerator(command, {
268 cwd: getScriptsRoot(),
272 if (generatorExitCode === 0) {
273 void window.showInformationMessage(
274 `Successfully created project: ${options.name}`
277 void commands.executeCommand(
279 Uri.file(join(options.projectRoot, options.name)),
284 `Generator Process exited with code: ${generatorExitCode ?? "null"}`
287 void window.showErrorMessage(`Could not create project ${options.name}`);