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:
+ *
+ * - System {@code PATH}
+ * - Well-known package manager directories (Homebrew, MacPorts, WinGet, Chocolatey, Scoop, etc.)
+ * - {@code eimPath} from {@code eim_idf.json} (when the path exists on disk)
+ * - {@code EIM_PATH} env variable (existence-checked)
+ * - Default GUI install location (existence-checked)
+ * - Default CLI install location (existence-checked)
+ *
*
* @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...