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/ToolInitializer.java b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/ToolInitializer.java
index 3a9b90844..4ad906f51 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
@@ -11,6 +11,7 @@
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 +66,14 @@ 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).
+ * Resolves the EIM executable path using priority-based resolution:
+ *
+ * - System {@code PATH}
+ * - {@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
@@ -101,6 +107,12 @@ public String resolveEimExecutablePath(EimJson eimJson)
return defaultEimPath.toString();
}
+ Path cliEimPath = getDefaultCliEimPath();
+ if (cliEimPath != null && Files.exists(cliEimPath))
+ {
+ return cliEimPath.toString();
+ }
+
return StringUtil.EMPTY;
}
@@ -227,5 +239,55 @@ 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$
+ pb.redirectErrorStream(true);
+ Process process = pb.start();
+ 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/strategies/MacOsEimLauncherStrategy.java b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/MacOsEimLauncherStrategy.java
index 9632e34b0..72dc7f4b0 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
@@ -62,6 +62,11 @@ public MacOsEimLauncherStrategy(Display display, MessageConsoleStream standardCo
@Override
public LaunchResult launch(String eimPath) throws IOException
{
+ if (!isAppBundle(eimPath))
+ {
+ return launchCliDirect(eimPath);
+ }
+
String appBundlePath = deriveAppBundlePath(eimPath);
String execPath = deriveExecPath(eimPath, appBundlePath);
String bundleId = readBundleId(appBundlePath);
@@ -108,6 +113,41 @@ public LaunchResult launch(String eimPath) throws IOException
"osascript exit=" + exit + "\n" + out); //$NON-NLS-1$ //$NON-NLS-2$
}
+ private LaunchResult launchCliDirect(String eimPath) throws IOException
+ {
+ String quotedPath = ProcessUtils.bashSingleQuote(eimPath);
+ String bashCmd = "nohup " + quotedPath + " > /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)
{