diff --git a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/EimConstants.java b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/EimConstants.java index c60353014..413561415 100644 --- a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/EimConstants.java +++ b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/EimConstants.java @@ -27,5 +27,7 @@ public interface EimConstants String USER_EIM_DIR = Paths.get(System.getProperty("user.home"), ".espressif", "eim_gui").toString(); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + String USER_EIM_CLI_DIR = Paths.get(System.getProperty("user.home"), ".espressif", "eim").toString(); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + String EIM_JSON_VALID_VERSION = "2.0"; //$NON-NLS-1$ } diff --git a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/EimLoader.java b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/EimLoader.java index 9c973cb44..ea5218bb5 100644 --- a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/EimLoader.java +++ b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/EimLoader.java @@ -116,10 +116,18 @@ private void logError(String message) */ public LaunchResult launchEimWithResult(String eimPath) throws IOException { - LaunchResult result = launchService.launch(eimPath); + return launchEimWithResult(eimPath, new String[0]); + } + + /** + * Launches EIM with optional arguments (e.g. "gui") and returns the {@link com.espressif.idf.core.tools.launch.LaunchResult}. + */ + public LaunchResult launchEimWithResult(String eimPath, String... args) throws IOException + { + LaunchResult result = launchService.launch(eimPath, args); this.lastLaunchResult = result; - logMessage("Launched EIM application: " + eimPath + " (pid=" + result.pid().orElse(-1) + ")\n"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + logMessage("Launched EIM application: " + eimPath + " " + String.join(" ", args) + " (pid=" + result.pid().orElse(-1) + ")\n"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ return result; } diff --git a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/ToolInitializer.java b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/ToolInitializer.java index 276bd4b4f..81800ca5b 100644 --- a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/ToolInitializer.java +++ b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/ToolInitializer.java @@ -6,11 +6,13 @@ import java.io.File; import java.io.IOException; +import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.TimeUnit; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.IPath; @@ -65,9 +67,88 @@ private String findEimOnSystemPath() } /** - * Resolves the EIM executable path: system {@code PATH} first, then {@code eimPath} from - * {@code eim_idf.json} when the path exists on disk, then {@code EIM_PATH} env variable, then - * {@link #getDefaultEimPath()} (existence-checked). + * Probes well-known package manager bin directories for the {@code eim} executable. GUI-launched Eclipse processes + * on macOS/Linux often have a minimal PATH that excludes directories like {@code /opt/homebrew/bin}, so this method + * checks those locations directly regardless of the JVM's process PATH. + * + * @return absolute path to the executable, or empty if not found in any known location + */ + private String findEimInPackageManagerPaths() + { + String os = Platform.getOS(); + String home = System.getProperty("user.home"); //$NON-NLS-1$ + boolean isWindows = Platform.OS_WIN32.equals(os); + String execName = isWindows ? "eim.exe" : "eim"; //$NON-NLS-1$ //$NON-NLS-2$ + + List candidateDirs = new ArrayList<>(); + + if (Platform.OS_MACOSX.equals(os)) + { + candidateDirs.add("/opt/homebrew/bin"); //$NON-NLS-1$ + candidateDirs.add("/usr/local/bin"); //$NON-NLS-1$ + candidateDirs.add("/opt/local/bin"); //$NON-NLS-1$ + } + else if (Platform.OS_LINUX.equals(os)) + { + candidateDirs.add("/usr/local/bin"); //$NON-NLS-1$ + candidateDirs.add("/usr/bin"); //$NON-NLS-1$ + if (home != null) + { + candidateDirs.add(home + "/.local/bin"); //$NON-NLS-1$ + } + candidateDirs.add("/snap/bin"); //$NON-NLS-1$ + candidateDirs.add("/var/lib/flatpak/exports/bin"); //$NON-NLS-1$ + if (home != null) + { + candidateDirs.add(home + "/.local/share/flatpak/exports/bin"); //$NON-NLS-1$ + candidateDirs.add(home + "/.nix-profile/bin"); //$NON-NLS-1$ + } + candidateDirs.add("/nix/var/nix/profiles/default/bin"); //$NON-NLS-1$ + } + else if (isWindows) + { + String localAppData = System.getenv("LOCALAPPDATA"); //$NON-NLS-1$ + if (localAppData != null) + { + candidateDirs.add(localAppData + "\\Microsoft\\WinGet\\Links"); //$NON-NLS-1$ + } + String chocoInstall = System.getenv("ChocolateyInstall"); //$NON-NLS-1$ + if (chocoInstall != null && !chocoInstall.isBlank()) + { + candidateDirs.add(chocoInstall + "\\bin"); //$NON-NLS-1$ + } + else + { + candidateDirs.add("C:\\ProgramData\\chocolatey\\bin"); //$NON-NLS-1$ + } + if (home != null) + { + candidateDirs.add(home + "\\scoop\\shims"); //$NON-NLS-1$ + } + } + + for (String dir : candidateDirs) + { + Path candidate = Paths.get(dir, execName); + if (Files.isRegularFile(candidate) && Files.isExecutable(candidate)) + { + return candidate.toString(); + } + } + + return StringUtil.EMPTY; + } + + /** + * Resolves the EIM executable path using priority-based resolution: + *
    + *
  1. System {@code PATH}
  2. + *
  3. Well-known package manager directories (Homebrew, MacPorts, WinGet, Chocolatey, Scoop, etc.)
  4. + *
  5. {@code eimPath} from {@code eim_idf.json} (when the path exists on disk)
  6. + *
  7. {@code EIM_PATH} env variable (existence-checked)
  8. + *
  9. Default GUI install location (existence-checked)
  10. + *
  11. Default CLI install location (existence-checked)
  12. + *
* * @param eimJson parsed JSON or {@code null} * @return resolved absolute path string, or empty if nothing could be resolved @@ -80,6 +161,12 @@ public String resolveEimExecutablePath(EimJson eimJson) return fromPath; } + String fromPkgMgr = findEimInPackageManagerPaths(); + if (!StringUtil.isEmpty(fromPkgMgr)) + { + return fromPkgMgr; + } + if (eimJson != null && !StringUtil.isEmpty(eimJson.getEimPath())) { String jsonPath = eimJson.getEimPath(); @@ -101,6 +188,12 @@ public String resolveEimExecutablePath(EimJson eimJson) return defaultEimPath.toString(); } + Path cliEimPath = getDefaultCliEimPath(); + if (cliEimPath != null && Files.exists(cliEimPath)) + { + return cliEimPath.toString(); + } + return StringUtil.EMPTY; } @@ -226,5 +319,58 @@ else if (os.equals(Platform.OS_MACOSX)) return defaultEimPath; } - + + /** + * Returns the default CLI EIM binary path per platform. Unlike the GUI path, this points to the CLI-only install + * directory ({@code ~/.espressif/eim/}). + */ + public Path getDefaultCliEimPath() + { + String userHome = System.getProperty("user.home"); //$NON-NLS-1$ + String os = Platform.getOS(); + if (os.equals(Platform.OS_WIN32)) + { + return Paths.get(userHome, ".espressif", "eim", "eim.exe"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } + return Paths.get(userHome, ".espressif", "eim", "eim"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } + + /** + * Checks whether the EIM binary at the given path supports GUI mode by running {@code eim gui --help} and checking + * for a successful exit code. This is used to determine whether to launch EIM as a GUI application or in CLI/wizard + * mode. + * + * @param eimPath absolute path to the EIM executable + * @return {@code true} if the binary supports the {@code gui} subcommand, {@code false} otherwise + */ + public boolean isEimGuiCapable(String eimPath) + { + if (StringUtil.isEmpty(eimPath)) + { + return false; + } + + try + { + ProcessBuilder pb = new ProcessBuilder(eimPath, "gui", "--help"); //$NON-NLS-1$ //$NON-NLS-2$ + Logger.log("Checking if EIM supports GUI mode with command: " + String.join(" ", pb.command())); //$NON-NLS-1$ //$NON-NLS-2$ + pb.redirectErrorStream(true); + Process process = pb.start(); + // Drain stdout so the process doesn't block + process.getInputStream().transferTo(OutputStream.nullOutputStream()); + boolean finished = process.waitFor(5, TimeUnit.SECONDS); + if (!finished) + { + process.destroyForcibly(); + return false; + } + return process.exitValue() == 0; + } + catch (IOException | InterruptedException e) + { + Logger.log("EIM does not support the gui subcommand, falling back to CLI mode."); //$NON-NLS-1$ + return false; + } + } + } diff --git a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/EimLaunchService.java b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/EimLaunchService.java index 8d8bf114f..1dca64e77 100644 --- a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/EimLaunchService.java +++ b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/EimLaunchService.java @@ -31,6 +31,11 @@ public EimLaunchService(Display display, MessageConsoleStream standardConsoleStr } public LaunchResult launch(String eimPath) throws IOException + { + return launch(eimPath, new String[0]); + } + + public LaunchResult launch(String eimPath, String... args) throws IOException { if (eimPath == null || eimPath.isBlank()) throw new IOException("EIM path is null/blank"); //$NON-NLS-1$ @@ -38,7 +43,11 @@ public LaunchResult launch(String eimPath) throws IOException if (!Files.exists(Paths.get(eimPath))) throw new IOException("EIM path not found: " + eimPath); //$NON-NLS-1$ - return strategy.launch(eimPath); + if (args == null || args.length == 0) + { + return strategy.launch(eimPath); + } + return strategy.launch(eimPath, args); } public IStatus waitForExit(LaunchResult launchResult, IProgressMonitor monitor) diff --git a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/EimLauncherStrategy.java b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/EimLauncherStrategy.java index 402f03f69..efbe934a1 100644 --- a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/EimLauncherStrategy.java +++ b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/EimLauncherStrategy.java @@ -20,5 +20,7 @@ public interface EimLauncherStrategy { LaunchResult launch(String eimPath) throws IOException; + LaunchResult launch(String eimPath, String... args) throws IOException; + IStatus waitForExit(LaunchResult launchResult, IProgressMonitor monitor); } diff --git a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/LinuxEimLauncherStrategy.java b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/LinuxEimLauncherStrategy.java index 299aca87a..3967f9659 100644 --- a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/LinuxEimLauncherStrategy.java +++ b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/LinuxEimLauncherStrategy.java @@ -33,12 +33,19 @@ public LinuxEimLauncherStrategy(Display display, MessageConsoleStream standardCo @Override public LaunchResult launch(String eimPath) throws IOException + { + return launch(eimPath, new String[0]); + } + + @Override + public LaunchResult launch(String eimPath, String... args) throws IOException { if (!Files.exists(Paths.get(eimPath))) throw new IOException("EIM path not found: " + eimPath); //$NON-NLS-1$ String quotedPath = ProcessUtils.bashSingleQuote(eimPath); - String bashCmd = "nohup " + quotedPath + " > /dev/null 2>&1 & echo $!"; //$NON-NLS-1$ //$NON-NLS-2$ + String argsStr = (args != null && args.length > 0) ? " " + String.join(" ", args) : ""; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + String bashCmd = "nohup " + quotedPath + argsStr + " > /dev/null 2>&1 & echo $!"; //$NON-NLS-1$ //$NON-NLS-2$ List command = List.of("bash", "-lc", bashCmd); //$NON-NLS-1$ //$NON-NLS-2$ Process launcher = new ProcessBuilder(command).redirectErrorStream(true).start(); diff --git a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/MacOsEimLauncherStrategy.java b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/MacOsEimLauncherStrategy.java index 9632e34b0..9041dd731 100644 --- a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/MacOsEimLauncherStrategy.java +++ b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/MacOsEimLauncherStrategy.java @@ -37,8 +37,9 @@ public final class MacOsEimLauncherStrategy extends AbstractLoggingLauncherStrat private static final String MACOS_LAUNCH_AND_PID_APPLESCRIPT = """ set appPath to system attribute "APP_PATH" set bundleId to system attribute "BUNDLE_ID" + set openArgs to system attribute "OPEN_ARGS" - do shell script "open -a " & quoted form of appPath + do shell script "open -a " & quoted form of appPath & openArgs tell application "System Events" repeat 300 times @@ -62,14 +63,28 @@ public MacOsEimLauncherStrategy(Display display, MessageConsoleStream standardCo @Override public LaunchResult launch(String eimPath) throws IOException { + return launch(eimPath, new String[0]); + } + + @Override + public LaunchResult launch(String eimPath, String... args) throws IOException + { + if (!isAppBundle(eimPath)) + { + return launchCliDirect(eimPath, args); + } + String appBundlePath = deriveAppBundlePath(eimPath); String execPath = deriveExecPath(eimPath, appBundlePath); String bundleId = readBundleId(appBundlePath); + String argsForOpen = (args != null && args.length > 0) ? " --args " + String.join(" ", args) : ""; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + ProcessBuilder pb = new ProcessBuilder("osascript", "-"); //$NON-NLS-1$ //$NON-NLS-2$ pb.redirectErrorStream(true); pb.environment().put("APP_PATH", appBundlePath); //$NON-NLS-1$ pb.environment().put("BUNDLE_ID", bundleId); //$NON-NLS-1$ + pb.environment().put("OPEN_ARGS", argsForOpen); //$NON-NLS-1$ Process p = pb.start(); try (OutputStream stdin = p.getOutputStream()) @@ -94,20 +109,52 @@ public LaunchResult launch(String eimPath) throws IOException Logger.log("BUNDLE_ID=" + bundleId); //$NON-NLS-1$ Logger.log("Launcher output was:\n" + out); //$NON-NLS-1$ - // If osascript failed or returned no pid, we fall back to execPath polling. Long pid = ProcessUtils.parseFirstLongLine(out); if (exit == 0 && pid != null) { return LaunchResult.ofPid(pid.longValue(), execPath, out); } - // Fallback (still "successfully launched" from user perspective): - // - we already attempted "open -a" - // - we can wait for closure using pgrep -f execPath return LaunchResult.ofNoPid(execPath, "osascript exit=" + exit + "\n" + out); //$NON-NLS-1$ //$NON-NLS-2$ } + private LaunchResult launchCliDirect(String eimPath, String... args) throws IOException + { + String quotedPath = ProcessUtils.bashSingleQuote(eimPath); + String argsStr = (args != null && args.length > 0) ? " " + String.join(" ", args) : ""; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + String bashCmd = "nohup " + quotedPath + argsStr + " > /dev/null 2>&1 & echo $!"; //$NON-NLS-1$ //$NON-NLS-2$ + + Process launcher = new ProcessBuilder("bash", "-lc", bashCmd) //$NON-NLS-1$ //$NON-NLS-2$ + .redirectErrorStream(true).start(); + + String out = ProcessUtils.readAll(launcher.getInputStream()); + Long pid = ProcessUtils.parseFirstLongLine(out); + + if (pid == null) + { + Logger.log("macOS CLI launcher output was:\n" + out); //$NON-NLS-1$ + throw new IOException("No PID found in launcher output. Output was:\n" + out); //$NON-NLS-1$ + } + + return LaunchResult.ofPid(pid.longValue(), eimPath, out); + } + + private boolean isAppBundle(String eimPath) + { + Path p = Paths.get(eimPath).toAbsolutePath().normalize(); + while (p != null) + { + String name = p.getFileName() != null ? p.getFileName().toString() : ""; //$NON-NLS-1$ + if (name.endsWith(".app")) //$NON-NLS-1$ + { + return true; + } + p = p.getParent(); + } + return false; + } + @Override public IStatus waitForExit(LaunchResult launchResult, IProgressMonitor monitor) { diff --git a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/WindowsEimLauncherStrategy.java b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/WindowsEimLauncherStrategy.java index 8680aef7d..2da9f8907 100644 --- a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/WindowsEimLauncherStrategy.java +++ b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/WindowsEimLauncherStrategy.java @@ -33,14 +33,21 @@ public WindowsEimLauncherStrategy(Display display, MessageConsoleStream standard @Override public LaunchResult launch(String eimPath) throws IOException + { + return launch(eimPath, new String[0]); + } + + @Override + public LaunchResult launch(String eimPath, String... args) throws IOException { if (!Files.exists(Paths.get(eimPath))) throw new IOException("EIM path not found: " + eimPath); //$NON-NLS-1$ String escapedPathForPowershell = eimPath.replace("'", "''"); //$NON-NLS-1$ //$NON-NLS-2$ + String argsStr = (args != null && args.length > 0) ? " -ArgumentList '" + String.join(" ", args) + "'" : ""; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ String powershellCmd = String.format( - "Start-Process -FilePath '%s' -PassThru | Select-Object -ExpandProperty Id", //$NON-NLS-1$ - escapedPathForPowershell); + "Start-Process -FilePath '%s'%s -PassThru | Select-Object -ExpandProperty Id", //$NON-NLS-1$ + escapedPathForPowershell, argsStr); List command = List.of("powershell.exe", "-Command", powershellCmd); //$NON-NLS-1$ //$NON-NLS-2$ Process launcher = new ProcessBuilder(command).redirectErrorStream(true).start(); diff --git a/bundles/com.espressif.idf.ui/META-INF/MANIFEST.MF b/bundles/com.espressif.idf.ui/META-INF/MANIFEST.MF index 8905b1e52..0e212f1db 100644 --- a/bundles/com.espressif.idf.ui/META-INF/MANIFEST.MF +++ b/bundles/com.espressif.idf.ui/META-INF/MANIFEST.MF @@ -30,7 +30,12 @@ Require-Bundle: org.eclipse.core.runtime, org.eclipse.ltk.core.refactoring, org.eclipse.tools.templates.core, org.eclipse.tools.templates.ui, - org.eclipse.ui.intro + org.eclipse.ui.intro, + org.eclipse.jface, + org.eclipse.terminal.view.ui;bundle-version="[1.0.0,2.0.0)", + org.eclipse.terminal.view.core;bundle-version="[1.0.0,2.0.0)", + org.eclipse.terminal.connector.process;bundle-version="[1.0.0,2.0.0)", + org.eclipse.terminal.control;bundle-version="[1.0.0,2.0.0)" Automatic-Module-Name: com.espressif.idf.ui Bundle-ActivationPolicy: lazy Bundle-RequiredExecutionEnvironment: JavaSE-21 @@ -45,6 +50,7 @@ Bundle-ClassPath: ., lib/gson-2.8.7.jar, lib/commonmark-0.24.0.jar, lib/jfreechart-1.5.5.jar -Import-Package: org.eclipse.core.expressions, +Import-Package: org.eclipse.cdt.utils.pty;mandatory:=native, + org.eclipse.core.expressions, org.eclipse.tools.templates.freemarker, org.eclipse.ui.editors.text diff --git a/bundles/com.espressif.idf.ui/plugin.xml b/bundles/com.espressif.idf.ui/plugin.xml index a3207ea12..39ec1e98f 100644 --- a/bundles/com.espressif.idf.ui/plugin.xml +++ b/bundles/com.espressif.idf.ui/plugin.xml @@ -904,4 +904,21 @@ parentId="org.eclipse.ui.textEditorScope"> + + + + + + + diff --git a/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/EimButtonLaunchListener.java b/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/EimButtonLaunchListener.java index 3baa44356..a20337338 100644 --- a/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/EimButtonLaunchListener.java +++ b/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/EimButtonLaunchListener.java @@ -28,7 +28,6 @@ import com.espressif.idf.core.tools.EimLoader; import com.espressif.idf.core.tools.ToolInitializer; import com.espressif.idf.core.tools.exceptions.EimVersionMismatchException; -import com.espressif.idf.core.tools.launch.LaunchResult; import com.espressif.idf.core.tools.vo.EimJson; import com.espressif.idf.ui.UIPlugin; import com.espressif.idf.ui.handlers.EclipseHandler; @@ -88,8 +87,9 @@ protected IStatus run(IProgressMonitor monitor) { try { - var launchResult = eimLoader.launchEimWithResult(idfEnvironmentVariables.getEnvValue(IDFEnvironmentVariables.EIM_PATH)); - eimLoader.waitForEimClosure(launchResult, EimButtonLaunchListener.this::refreshAfterEimClose); + String resolvedEimPath = toolInitializer.resolveEimExecutablePath(null); + EimGuiOrCliLauncher.launch(toolInitializer, eimLoader, resolvedEimPath, standardConsoleStream, + display, EimButtonLaunchListener.this::refreshAfterEimClose); } catch (IOException e) { @@ -199,8 +199,8 @@ public void onCompleted(String filePath) try { idfEnvironmentVariables.addEnvVariable(IDFEnvironmentVariables.EIM_PATH, appToLaunch); - LaunchResult launchResult = eimLoader.launchEimWithResult(appToLaunch); - eimLoader.waitForEimClosure(launchResult, EimButtonLaunchListener.this::refreshAfterEimClose); + EimGuiOrCliLauncher.launch(toolInitializer, eimLoader, appToLaunch, standardConsoleStream, display, + EimButtonLaunchListener.this::refreshAfterEimClose); } catch (IOException e) { diff --git a/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/EimGuiOrCliLauncher.java b/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/EimGuiOrCliLauncher.java new file mode 100644 index 000000000..28af2f54d --- /dev/null +++ b/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/EimGuiOrCliLauncher.java @@ -0,0 +1,88 @@ +/******************************************************************************* + * Copyright 2026 Espressif Systems (Shanghai) PTE LTD. All rights reserved. + * Use is subject to license terms. + *******************************************************************************/ +package com.espressif.idf.ui.tools; + +import java.io.IOException; + +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.console.MessageConsoleStream; + +import com.espressif.idf.core.logging.Logger; +import com.espressif.idf.core.tools.EimLoader; +import com.espressif.idf.core.tools.ToolInitializer; +import com.espressif.idf.core.tools.launch.LaunchResult; +import com.espressif.idf.core.tools.watcher.EimJsonWatchService; +import com.espressif.idf.ui.tools.eim.terminal.EimCliTerminalLaunchSupport; + +/** + * Launches EIM as a GUI app when the binary supports it; otherwise runs the CLI wizard in the integrated terminal. + * + * @author Ali Azam Rana + */ +public final class EimGuiOrCliLauncher +{ + private EimGuiOrCliLauncher() + { + } + + /** + * @param afterEimClosed invoked after EIM exits (GUI or CLI terminal), on a thread appropriate for the launch mode; + * for the CLI path the runnable is executed on the SWT UI thread after the terminal process ends + */ + public static void launch(ToolInitializer toolInitializer, EimLoader eimLoader, String eimPath, + MessageConsoleStream standardConsoleStream, Display display, Runnable afterEimClosed) throws IOException + { + if (eimPath == null || eimPath.isBlank()) + { + Logger.log("EIM launch aborted: path is null or empty"); //$NON-NLS-1$ + return; + } + + if (toolInitializer.isEimGuiCapable(eimPath)) + { + Logger.log("EIM binary supports GUI mode, launching GUI"); //$NON-NLS-1$ + LaunchResult launchResult = eimLoader.launchEimWithResult(eimPath, "gui"); //$NON-NLS-1$ + eimLoader.waitForEimClosure(launchResult, afterEimClosed); + return; + } + + Logger.log("EIM binary does not support GUI mode, launching CLI terminal"); //$NON-NLS-1$ + EimJsonWatchService.getInstance().pauseListeners(); + display.syncExec(() -> { + try + { + standardConsoleStream.write(Messages.EimCliTerminalOpeningWizard + "\n"); //$NON-NLS-1$ + EimCliTerminalLaunchSupport.launch(eimPath, () -> display.asyncExec(() -> { + try + { + standardConsoleStream.write(Messages.EimCliTerminalWizardCompleted + "\n"); //$NON-NLS-1$ + } + catch (IOException e) + { + Logger.log(e); + } + try + { + afterEimClosed.run(); + } + finally + { + EimJsonWatchService.getInstance().unpauseListeners(); + } + })); + } + catch (IOException e) + { + Logger.log(e); + EimJsonWatchService.getInstance().unpauseListeners(); + } + catch (RuntimeException e) + { + Logger.log(e); + EimJsonWatchService.getInstance().unpauseListeners(); + } + }); + } +} diff --git a/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/EspressifToolStartup.java b/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/EspressifToolStartup.java index 7f893bc5b..7c87d9645 100644 --- a/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/EspressifToolStartup.java +++ b/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/EspressifToolStartup.java @@ -45,12 +45,11 @@ import com.espressif.idf.ui.IDFConsole; import com.espressif.idf.ui.UIPlugin; import com.espressif.idf.ui.handlers.EclipseHandler; +import com.espressif.idf.ui.tools.EimGuiOrCliLauncher; import com.espressif.idf.ui.tools.manager.ESPIDFManagerEditor; import com.espressif.idf.ui.tools.manager.EimEditorInput; import com.espressif.idf.ui.tools.watcher.EimJsonUiChangeHandler; -import com.espressif.idf.core.tools.launch.LaunchResult; - /** * Startup class to handle the tools * @@ -371,7 +370,6 @@ public void onCompleted(String filePath) } }); - LaunchResult launchResult = null; String appToLaunch = filePath; try { @@ -381,33 +379,31 @@ public void onCompleted(String filePath) } idfEnvironmentVariables.addEnvVariable(IDFEnvironmentVariables.EIM_PATH, appToLaunch); - launchResult = eimLoader.launchEimWithResult(appToLaunch); + EimGuiOrCliLauncher.launch(toolInitializer, eimLoader, appToLaunch, standardConsoleStream, + Display.getDefault(), () -> { + if (toolInitializer.isOldEspIdfConfigPresent() && !toolInitializer.isOldConfigExported()) + { + Logger.log("Old configuration found and not converted"); + handleOldConfigExport(); + } + try + { + eimJson = toolInitializer.loadEimJson(); + } + catch (EimVersionMismatchException e) + { + Logger.log(e); + MessageDialog.openError(Display.getDefault().getActiveShell(), e.msgTitle(), + e.getMessage()); + return; + } + openEspIdfManager(eimJson); + }); } - catch ( - IOException - | InterruptedException e) + catch (IOException | InterruptedException e) { Logger.log(e); } - - eimLoader.waitForEimClosure(launchResult, () -> { - if (toolInitializer.isOldEspIdfConfigPresent() && !toolInitializer.isOldConfigExported()) - { - Logger.log("Old configuration found and not converted"); - handleOldConfigExport(); - } - try - { - eimJson = toolInitializer.loadEimJson(); - } - catch (EimVersionMismatchException e) - { - Logger.log(e); - MessageDialog.openError(Display.getDefault().getActiveShell(), e.msgTitle(), e.getMessage()); - return; - } - openEspIdfManager(eimJson); - }); } @Override diff --git a/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/Messages.java b/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/Messages.java index 0bfda42de..a8e2b27c8 100644 --- a/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/Messages.java +++ b/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/Messages.java @@ -66,6 +66,10 @@ public class Messages extends NLS public static String MsgYes; public static String MsgNo; + + public static String EimCliTerminalWizardTitle; + public static String EimCliTerminalOpeningWizard; + public static String EimCliTerminalWizardCompleted; static { diff --git a/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/eim/terminal/EimCliTerminalConnector.java b/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/eim/terminal/EimCliTerminalConnector.java new file mode 100644 index 000000000..aa7af0522 --- /dev/null +++ b/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/eim/terminal/EimCliTerminalConnector.java @@ -0,0 +1,132 @@ +/******************************************************************************* + * Copyright 2026 Espressif Systems (Shanghai) PTE LTD. All rights reserved. + * Use is subject to license terms. + *******************************************************************************/ +package com.espressif.idf.ui.tools.eim.terminal; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; + +import org.eclipse.core.runtime.Platform; +import org.eclipse.terminal.connector.ISettingsStore; +import org.eclipse.terminal.connector.ITerminalControl; +import org.eclipse.terminal.connector.process.ProcessConnector; + +import com.espressif.idf.core.logging.Logger; + +/** + * Opens a login shell, sends the EIM CLI executable as the first command, and notifies + * {@link EimCliTerminalLaunchSupport} when the process exits. + * + * @author Ali Azam Rana + */ +public final class EimCliTerminalConnector extends ProcessConnector +{ + /** + * Key used to round-trip the EIM executable path through the settings store. + * Must match the key used in {@link EimCliTerminalLauncherDelegate#createTerminalConnector}. + */ + static final String SETTINGS_KEY_EIM_PATH = "eim.cli.executable"; //$NON-NLS-1$ + + /** + * Key for the extra PATH prefix (colon/semicolon-separated directories) that must be exported inside the shell + * session so that EIM subprocesses (git, python, cmake checks) can find tools installed via package managers. + */ + static final String SETTINGS_KEY_PATH_PREFIX = "eim.cli.path.prefix"; //$NON-NLS-1$ + + private String eimExecutablePath; + private String pathPrefix; + + @Override + public void load(ISettingsStore store) + { + super.load(store); + eimExecutablePath = store.get(SETTINGS_KEY_EIM_PATH, null); + pathPrefix = store.get(SETTINGS_KEY_PATH_PREFIX, null); + } + + @Override + public void connect(ITerminalControl control) + { + super.connect(control); + var process = getProcess(); + if (process == null) + { + Logger.log("EIM CLI terminal: no process after connect"); //$NON-NLS-1$ + EimCliTerminalLaunchSupport.invokeCompletionIfArmed(); + return; + } + + if (eimExecutablePath != null && !eimExecutablePath.isBlank()) + { + sendEimCommand(process.getOutputStream()); + } + + Thread waiter = new Thread(() -> { + try + { + int code = process.waitFor(); + Logger.log("EIM CLI terminal process exited with code " + code); //$NON-NLS-1$ + } + catch (InterruptedException e) + { + Thread.currentThread().interrupt(); + } + finally + { + EimCliTerminalLaunchSupport.invokeCompletionIfArmed(); + } + }, "EimCliTerminalWait"); //$NON-NLS-1$ + waiter.setDaemon(true); + waiter.start(); + } + + private void sendEimCommand(OutputStream out) + { + try + { + if (Platform.OS_WIN32.equals(Platform.getOS())) + { + sendWindowsCommands(out); + } + else + { + sendPosixCommands(out); + } + out.flush(); + } + catch (IOException e) + { + Logger.log(e); + } + } + + private void sendPosixCommands(OutputStream out) throws IOException + { + if (pathPrefix != null && !pathPrefix.isBlank()) + { + String exportCmd = "export PATH=\"" + pathPrefix + ":$PATH\"\r\n"; //$NON-NLS-1$ //$NON-NLS-2$ + out.write(exportCmd.getBytes(StandardCharsets.UTF_8)); + Logger.log("EIM CLI terminal PATH export: " + exportCmd.trim()); //$NON-NLS-1$ + } + String quoted = "'" + eimExecutablePath.replace("'", "'\\''") + "'"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + String command = quoted + " wizard; exit\r\n"; //$NON-NLS-1$ + out.write(command.getBytes(StandardCharsets.UTF_8)); + Logger.log("EIM CLI terminal sent command: " + command.trim()); //$NON-NLS-1$ + } + + private void sendWindowsCommands(OutputStream out) throws IOException + { + if (pathPrefix != null && !pathPrefix.isBlank()) + { + String envCmd = "$env:PATH = \"" + pathPrefix + ";\" + $env:PATH\r\n"; //$NON-NLS-1$ //$NON-NLS-2$ + out.write(envCmd.getBytes(StandardCharsets.UTF_8)); + Logger.log("EIM CLI terminal PATH update: " + envCmd.trim()); //$NON-NLS-1$ + } + String escaped = eimExecutablePath.replace("'", "''"); //$NON-NLS-1$ //$NON-NLS-2$ + String command = "& '" + escaped + "' wizard; exit\r\n"; //$NON-NLS-1$ //$NON-NLS-2$ + out.write(command.getBytes(StandardCharsets.UTF_8)); + Logger.log("EIM CLI terminal sent command: " + command.trim()); //$NON-NLS-1$ + } +} diff --git a/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/eim/terminal/EimCliTerminalLaunchSupport.java b/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/eim/terminal/EimCliTerminalLaunchSupport.java new file mode 100644 index 000000000..d3501ba1c --- /dev/null +++ b/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/eim/terminal/EimCliTerminalLaunchSupport.java @@ -0,0 +1,106 @@ +/******************************************************************************* + * Copyright 2026 Espressif Systems (Shanghai) PTE LTD. All rights reserved. + * Use is subject to license terms. + *******************************************************************************/ +package com.espressif.idf.ui.tools.eim.terminal; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.eclipse.swt.widgets.Display; +import org.eclipse.terminal.view.core.ITerminalsConnectorConstants; + +import com.espressif.idf.core.logging.Logger; +import com.espressif.idf.core.util.StringUtil; +import com.espressif.idf.ui.tools.Messages; + +/** + * Opens EIM CLI in an Eclipse Terminal tab and runs a completion callback once when the process exits + * (or if the terminal fails to open). + * + * @author Ali Azam Rana + */ +public final class EimCliTerminalLaunchSupport +{ + private static final AtomicBoolean completionFired = new AtomicBoolean(); + private static volatile Runnable pendingCompletion; + + private EimCliTerminalLaunchSupport() + { + } + + /** + * Arms the one-shot completion callback and opens a new terminal running EIM CLI. + * + * @param eimExecutablePath absolute path to the EIM executable + * @param onComplete runs on the SWT UI thread once after EIM exits or if the terminal cannot be opened + * @return future completing when the terminal tab has been opened (not when EIM exits) + */ + public static CompletableFuture launch(String eimExecutablePath, Runnable onComplete) + { + if (StringUtil.isEmpty(eimExecutablePath) || onComplete == null) + { + Logger.log("EIM CLI terminal launch skipped: missing path or callback"); //$NON-NLS-1$ + if (onComplete != null) + { + Display.getDefault().asyncExec(onComplete); + } + return CompletableFuture.completedFuture(null); + } + + completionFired.set(false); + pendingCompletion = onComplete; + + Map properties = new HashMap<>(); + properties.put(EimCliTerminalLauncherDelegate.PROP_EIM_EXECUTABLE, eimExecutablePath); + properties.put(ITerminalsConnectorConstants.PROP_DELEGATE_ID, EimCliTerminalLauncherDelegate.DELEGATE_ID); + properties.put(ITerminalsConnectorConstants.PROP_TERMINAL_CONNECTOR_ID, + EimCliTerminalLauncherDelegate.CONNECTOR_ID); + properties.put(ITerminalsConnectorConstants.PROP_TITLE, Messages.EimCliTerminalWizardTitle); + properties.put(ITerminalsConnectorConstants.PROP_FORCE_NEW, Boolean.TRUE); + properties.put(ITerminalsConnectorConstants.PROP_ENCODING, java.nio.charset.StandardCharsets.UTF_8.name()); + + CompletableFuture opened = new EimCliTerminalLauncherDelegate().execute(properties); + opened.whenComplete((v, ex) -> { + if (ex != null) + { + if (ex instanceof Exception) + { + Logger.log("EIM CLI terminal failed to open: " + ex.getMessage(), (Exception) ex); //$NON-NLS-1$ + } + else + { + Logger.log("EIM CLI terminal failed to open: " + ex); //$NON-NLS-1$ + } + invokeCompletionIfArmed(); + } + }); + return opened; + } + + /** + * Invokes the armed completion callback at most once (from the EIM process wait thread or open failure). + */ + public static void invokeCompletionIfArmed() + { + Runnable r = pendingCompletion; + if (r == null) + { + return; + } + if (!completionFired.compareAndSet(false, true)) + { + return; + } + pendingCompletion = null; + Display display = Display.getDefault(); + if (display == null) + { + r.run(); + return; + } + display.asyncExec(r); + } +} diff --git a/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/eim/terminal/EimCliTerminalLauncherDelegate.java b/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/eim/terminal/EimCliTerminalLauncherDelegate.java new file mode 100644 index 000000000..e7356f842 --- /dev/null +++ b/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/eim/terminal/EimCliTerminalLauncherDelegate.java @@ -0,0 +1,333 @@ +/******************************************************************************* + * Copyright 2026 Espressif Systems (Shanghai) PTE LTD. All rights reserved. + * Use is subject to license terms. + *******************************************************************************/ +package com.espressif.idf.ui.tools.eim.terminal; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.Platform; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.terminal.connector.ISettingsStore; +import org.eclipse.terminal.connector.ITerminalConnector; +import org.eclipse.terminal.connector.InMemorySettingsStore; +import org.eclipse.terminal.connector.TerminalConnectorExtension; +import org.eclipse.terminal.connector.process.ProcessSettings; +import org.eclipse.terminal.view.core.ITerminalsConnectorConstants; +import org.eclipse.terminal.view.ui.launcher.AbstractConfigurationPanel; +import org.eclipse.terminal.view.ui.launcher.AbstractLauncherDelegate; +import org.eclipse.terminal.view.ui.launcher.IConfigurationPanel; +import org.eclipse.terminal.view.ui.launcher.IConfigurationPanelContainer; + +import com.espressif.idf.core.logging.Logger; +import com.espressif.idf.ui.tools.Messages; + +/** + * Opens a local terminal tab running the EIM CLI wizard. + * + * @author Ali Azam Rana + */ +public final class EimCliTerminalLauncherDelegate extends AbstractLauncherDelegate +{ + /** Must match {@code delegate} id in {@code plugin.xml}. */ + public static final String DELEGATE_ID = "com.espressif.idf.ui.launcher.eimCliTerminal"; //$NON-NLS-1$ + + public static final String CONNECTOR_ID = "com.espressif.idf.ui.eimCliTerminalConnector"; //$NON-NLS-1$ + /** Map key for the absolute path to the EIM executable. */ + public static final String PROP_EIM_EXECUTABLE = "com.espressif.idf.ui.eimCli.executable"; //$NON-NLS-1$ + + @Override + public boolean needsUserConfiguration() + { + return false; + } + + @Override + public IConfigurationPanel getPanel(IConfigurationPanelContainer container) + { + return new EimCliTerminalEmptyConfigPanel(container); + } + + /** + * Minimal configuration panel (not shown when the launcher is invoked programmatically). + */ + private static final class EimCliTerminalEmptyConfigPanel extends AbstractConfigurationPanel + { + EimCliTerminalEmptyConfigPanel(IConfigurationPanelContainer container) + { + super(container); + } + + @Override + public void setupPanel(Composite parent) + { + setControl(new Composite(parent, SWT.NONE)); + } + } + + @Override + public CompletableFuture execute(Map properties) + { + if (!properties.containsKey(ITerminalsConnectorConstants.PROP_DELEGATE_ID)) + { + properties.put(ITerminalsConnectorConstants.PROP_DELEGATE_ID, DELEGATE_ID); + } + if (!properties.containsKey(ITerminalsConnectorConstants.PROP_TERMINAL_CONNECTOR_ID)) + { + properties.put(ITerminalsConnectorConstants.PROP_TERMINAL_CONNECTOR_ID, CONNECTOR_ID); + } + if (!properties.containsKey(ITerminalsConnectorConstants.PROP_TITLE)) + { + properties.put(ITerminalsConnectorConstants.PROP_TITLE, Messages.EimCliTerminalWizardTitle); + } + if (!properties.containsKey(ITerminalsConnectorConstants.PROP_FORCE_NEW)) + { + properties.put(ITerminalsConnectorConstants.PROP_FORCE_NEW, Boolean.TRUE); + } + return getTerminalService().openConsole(properties); + } + + @Override + public ITerminalConnector createTerminalConnector(Map properties) + { + String eimPath = (String) properties.get(PROP_EIM_EXECUTABLE); + if (eimPath == null || eimPath.isBlank()) + { + Logger.log("EIM CLI terminal: missing executable path property"); //$NON-NLS-1$ + return null; + } + Path exe = Path.of(eimPath); + if (!Files.exists(exe) || Files.isDirectory(exe)) + { + Logger.log("EIM CLI terminal: executable not found: " + eimPath); //$NON-NLS-1$ + return null; + } + + ProcessSettings processSettings = new ProcessSettings(); + processSettings.setLocalEcho(false); + + String userHome = System.getProperty("user.home"); //$NON-NLS-1$ + processSettings.setWorkingDir(userHome != null && !userHome.isBlank() ? userHome : "."); //$NON-NLS-1$ + + // Use ONLY the native system environment (no CDT/IDF toolchain variables) so EIM's + // prerequisite checks (Python SSL, git, cmake) use system-installed tools, not IDF-bundled ones + Map envMap = new java.util.HashMap<>(System.getenv()); + String pathPrefix = computePackageManagerPathPrefix(envMap); + enrichPathWithPackageManagers(envMap); + + // Remove IDF-specific env vars that could pollute EIM's subprocess checks + envMap.remove("IDF_PATH"); //$NON-NLS-1$ + envMap.remove("IDF_TOOLS_PATH"); //$NON-NLS-1$ + envMap.remove("IDF_PYTHON_ENV_PATH"); //$NON-NLS-1$ + envMap.remove("PYTHONPATH"); //$NON-NLS-1$ + envMap.remove("PYTHONHOME"); //$NON-NLS-1$ + + // Ensure TERM is set so interactive CLI tools (arrow-key menus, coloured output) work correctly + if (!envMap.containsKey("TERM")) //$NON-NLS-1$ + { + envMap.put("TERM", "xterm-256color"); //$NON-NLS-1$ //$NON-NLS-2$ + } + + String[] envArray = envMap.entrySet().stream() + .map(e -> e.getKey() + "=" + e.getValue()) //$NON-NLS-1$ + .toArray(String[]::new); + processSettings.setEnvironment(envArray); + processSettings.setMergeWithNativeEnvironment(false); + + // Launch an interactive login shell so user profiles are sourced and terminal capabilities are enabled + String os = Platform.getOS(); + if (Platform.OS_WIN32.equals(os)) + { + processSettings.setImage("powershell.exe"); //$NON-NLS-1$ + } + else + { + processSettings.setImage(resolvePosixShell()); + processSettings.setArguments("-li"); //$NON-NLS-1$ + } + + try + { + ITerminalConnector connector = TerminalConnectorExtension.makeTerminalConnector(CONNECTOR_ID); + if (connector != null) + { + ISettingsStore store = new InMemorySettingsStore(); + processSettings.save(store); + store.put(EimCliTerminalConnector.SETTINGS_KEY_EIM_PATH, eimPath); + store.put(EimCliTerminalConnector.SETTINGS_KEY_PATH_PREFIX, pathPrefix); + connector.setDefaultSettings(); + connector.load(store); + return connector; + } + } + catch (CoreException e) + { + Logger.log(e); + } + return null; + } + + /** + * Computes a path-separator-joined string of existing package manager bin directories that should be prepended to + * PATH inside the shell session. This prefix is sent as an {@code export PATH=...} command to guarantee it survives + * login shell profile sourcing (e.g. macOS {@code path_helper} in {@code /etc/zprofile}). + */ + private static String computePackageManagerPathPrefix(Map envMap) + { + String os = Platform.getOS(); + String home = System.getProperty("user.home"); //$NON-NLS-1$ + List extraDirs = getPackageManagerDirs(os, home); + + String pathKey = "PATH"; //$NON-NLS-1$ + for (String key : envMap.keySet()) + { + if (key.equalsIgnoreCase(pathKey)) + { + pathKey = key; + break; + } + } + String existing = envMap.getOrDefault(pathKey, ""); //$NON-NLS-1$ + String sep = java.io.File.pathSeparator; + Set currentEntries = new LinkedHashSet<>(List.of(existing.split(sep))); + + StringBuilder sb = new StringBuilder(); + for (String dir : extraDirs) + { + if (!currentEntries.contains(dir) && Files.isDirectory(Path.of(dir))) + { + if (sb.length() > 0) + { + sb.append(sep); + } + sb.append(dir); + } + } + return sb.toString(); + } + + /** + * Returns the list of well-known package manager bin directories for the current OS. + */ + private static List getPackageManagerDirs(String os, String home) + { + List extraDirs = new ArrayList<>(); + + if (Platform.OS_MACOSX.equals(os)) + { + extraDirs.add("/opt/homebrew/bin"); //$NON-NLS-1$ + extraDirs.add("/opt/homebrew/sbin"); //$NON-NLS-1$ + extraDirs.add("/usr/local/bin"); //$NON-NLS-1$ + extraDirs.add("/usr/local/sbin"); //$NON-NLS-1$ + extraDirs.add("/opt/local/bin"); //$NON-NLS-1$ + extraDirs.add("/opt/local/sbin"); //$NON-NLS-1$ + } + else if (Platform.OS_LINUX.equals(os)) + { + extraDirs.add("/usr/local/bin"); //$NON-NLS-1$ + extraDirs.add("/usr/bin"); //$NON-NLS-1$ + if (home != null) + { + extraDirs.add(home + "/.local/bin"); //$NON-NLS-1$ + } + extraDirs.add("/snap/bin"); //$NON-NLS-1$ + extraDirs.add("/var/lib/flatpak/exports/bin"); //$NON-NLS-1$ + if (home != null) + { + extraDirs.add(home + "/.local/share/flatpak/exports/bin"); //$NON-NLS-1$ + extraDirs.add(home + "/.nix-profile/bin"); //$NON-NLS-1$ + } + extraDirs.add("/nix/var/nix/profiles/default/bin"); //$NON-NLS-1$ + } + else if (Platform.OS_WIN32.equals(os)) + { + String localAppData = System.getenv("LOCALAPPDATA"); //$NON-NLS-1$ + if (localAppData != null) + { + extraDirs.add(localAppData + "\\Microsoft\\WinGet\\Links"); //$NON-NLS-1$ + } + String chocoInstall = System.getenv("ChocolateyInstall"); //$NON-NLS-1$ + if (chocoInstall != null && !chocoInstall.isBlank()) + { + extraDirs.add(chocoInstall + "\\bin"); //$NON-NLS-1$ + } + else + { + extraDirs.add("C:\\ProgramData\\chocolatey\\bin"); //$NON-NLS-1$ + } + if (home != null) + { + extraDirs.add(home + "\\scoop\\shims"); //$NON-NLS-1$ + } + } + + return extraDirs; + } + + /** + * Prepends well-known package-manager bin directories to PATH so tools like git, python, cmake are visible to EIM. + * Directories are only added if they exist on disk and are not already present in PATH. + */ + private static void enrichPathWithPackageManagers(Map envMap) + { + String os = Platform.getOS(); + String home = System.getProperty("user.home"); //$NON-NLS-1$ + List extraDirs = getPackageManagerDirs(os, home); + + if (extraDirs.isEmpty()) + { + return; + } + + String pathKey = "PATH"; //$NON-NLS-1$ + for (String key : envMap.keySet()) + { + if (key.equalsIgnoreCase(pathKey)) + { + pathKey = key; + break; + } + } + + String existing = envMap.getOrDefault(pathKey, ""); //$NON-NLS-1$ + String sep = File.pathSeparator; + + Set currentEntries = new LinkedHashSet<>(List.of(existing.split(sep))); + StringBuilder sb = new StringBuilder(); + for (String dir : extraDirs) + { + if (!currentEntries.contains(dir) && Files.isDirectory(Path.of(dir))) + { + sb.append(dir).append(sep); + currentEntries.add(dir); + } + } + sb.append(existing); + envMap.put(pathKey, sb.toString()); + } + + private static String resolvePosixShell() + { + Path bash = Path.of("/bin/bash"); //$NON-NLS-1$ + if (Files.isExecutable(bash)) + { + return bash.toString(); + } + String envShell = System.getenv("SHELL"); //$NON-NLS-1$ + if (envShell != null && !envShell.isBlank() && Files.isExecutable(Path.of(envShell))) + { + return envShell; + } + return "/bin/sh"; //$NON-NLS-1$ + } +} diff --git a/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/messages.properties b/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/messages.properties index eb13827c0..2f4d64bda 100644 --- a/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/messages.properties +++ b/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/messages.properties @@ -29,3 +29,6 @@ ESPIDFMainTablePage_VersionErrorToolTip=Version detection failed. Please re-inst ESPIDFMainTablePage_VersionToolTip=ESP-IDF Version: MsgYes=Yes MsgNo=No +EimCliTerminalWizardTitle=EIM Setup Wizard +EimCliTerminalOpeningWizard=Opening EIM in the integrated terminal (CLI wizard). +EimCliTerminalWizardCompleted=EIM wizard finished. Refreshing configuration... diff --git a/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/messages_zh.properties b/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/messages_zh.properties index 6b205ab6d..738787c30 100644 --- a/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/messages_zh.properties +++ b/bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/messages_zh.properties @@ -79,3 +79,6 @@ ForceDownloadToolTip=\u91cd\u65b0\u4e0b\u8f7d\u8fd9\u4e9b\u5de5\u5177\uff0c\u537 IDFDownloadHandler_DownloadPage_Title=\u4e0b\u8f7d\u6216\u4f7f\u7528 ESP-IDF IDFDownloadHandler_DownloadPageMsg=\u8bf7\u9009\u62e9\u9700\u8981\u4e0b\u8f7d\u7684 ESP-IDF \u7248\u672c\uff0c\u6216\u4f7f\u7528\u60a8\u7684\u672c\u5730 ESP-IDF \u9879\u76ee IDFDownloadHandler_ESPIDFConfiguration=ESP-IDF \u914d\u7f6e +EimCliTerminalWizardTitle=EIM \u8bbe\u7f6e\u5411\u5bfc +EimCliTerminalOpeningWizard=\u6b63\u5728\u96c6\u6210\u7ec8\u7aef\u4e2d\u6253\u5f00 EIM\uff08CLI \u5411\u5bfc\uff09\u3002 +EimCliTerminalWizardCompleted=EIM \u5411\u5bfc\u5df2\u7ed3\u675f\u3002\u6b63\u5728\u5237\u65b0\u914d\u7f6e...