Skip to content

Commit 96c923f

Browse files
committed
feat: add EIM CLI wizard terminal launch support
Add GUI/CLI launch support so that when EIM is detected as CLI-only (no GUI capability), the wizard runs inside the Eclipse integrated terminal instead of being launched as a detached background process. - Add EimCliTerminalConnector, EimCliTerminalLauncherDelegate, and EimCliTerminalLaunchSupport for terminal-based EIM CLI launch - Add EimGuiOrCliLauncher to branch between GUI and CLI launch paths - Enrich shell PATH with package-manager directories (Homebrew, WinGet, Chocolatey, Scoop, Snap, etc.) per platform - Wire completion callback to reload eim_idf.json and refresh UI after EIM CLI wizard exits
1 parent 1b99852 commit 96c923f

14 files changed

Lines changed: 737 additions & 37 deletions

File tree

bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/EimConstants.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,7 @@ public interface EimConstants
2727

2828
String USER_EIM_DIR = Paths.get(System.getProperty("user.home"), ".espressif", "eim_gui").toString(); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
2929

30+
String USER_EIM_CLI_DIR = Paths.get(System.getProperty("user.home"), ".espressif", "eim").toString(); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
31+
3032
String EIM_JSON_VALID_VERSION = "2.0"; //$NON-NLS-1$
3133
}

bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/ToolInitializer.java

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import java.nio.file.Paths;
1212
import java.util.ArrayList;
1313
import java.util.List;
14+
import java.util.concurrent.TimeUnit;
1415

1516
import org.eclipse.core.resources.ResourcesPlugin;
1617
import org.eclipse.core.runtime.IPath;
@@ -65,9 +66,14 @@ private String findEimOnSystemPath()
6566
}
6667

6768
/**
68-
* Resolves the EIM executable path: <strong>system {@code PATH} first</strong>, then {@code eimPath} from
69-
* {@code eim_idf.json} when the path exists on disk, then {@code EIM_PATH} env variable, then
70-
* {@link #getDefaultEimPath()} (existence-checked).
69+
* Resolves the EIM executable path using priority-based resolution:
70+
* <ol>
71+
* <li>System {@code PATH}</li>
72+
* <li>{@code eimPath} from {@code eim_idf.json} (when the path exists on disk)</li>
73+
* <li>{@code EIM_PATH} env variable (existence-checked)</li>
74+
* <li>Default GUI install location (existence-checked)</li>
75+
* <li>Default CLI install location (existence-checked)</li>
76+
* </ol>
7177
*
7278
* @param eimJson parsed JSON or {@code null}
7379
* @return resolved absolute path string, or empty if nothing could be resolved
@@ -101,6 +107,12 @@ public String resolveEimExecutablePath(EimJson eimJson)
101107
return defaultEimPath.toString();
102108
}
103109

110+
Path cliEimPath = getDefaultCliEimPath();
111+
if (cliEimPath != null && Files.exists(cliEimPath))
112+
{
113+
return cliEimPath.toString();
114+
}
115+
104116
return StringUtil.EMPTY;
105117
}
106118

@@ -227,5 +239,55 @@ else if (os.equals(Platform.OS_MACOSX))
227239

228240
return defaultEimPath;
229241
}
230-
242+
243+
/**
244+
* Returns the default CLI EIM binary path per platform. Unlike the GUI path, this points to the CLI-only install
245+
* directory ({@code ~/.espressif/eim/}).
246+
*/
247+
public Path getDefaultCliEimPath()
248+
{
249+
String userHome = System.getProperty("user.home"); //$NON-NLS-1$
250+
String os = Platform.getOS();
251+
if (os.equals(Platform.OS_WIN32))
252+
{
253+
return Paths.get(userHome, ".espressif", "eim", "eim.exe"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
254+
}
255+
return Paths.get(userHome, ".espressif", "eim", "eim"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
256+
}
257+
258+
/**
259+
* Checks whether the EIM binary at the given path supports GUI mode by running {@code eim gui --help} and checking
260+
* for a successful exit code. This is used to determine whether to launch EIM as a GUI application or in CLI/wizard
261+
* mode.
262+
*
263+
* @param eimPath absolute path to the EIM executable
264+
* @return {@code true} if the binary supports the {@code gui} subcommand, {@code false} otherwise
265+
*/
266+
public boolean isEimGuiCapable(String eimPath)
267+
{
268+
if (StringUtil.isEmpty(eimPath))
269+
{
270+
return false;
271+
}
272+
273+
try
274+
{
275+
ProcessBuilder pb = new ProcessBuilder(eimPath, "gui", "--help"); //$NON-NLS-1$ //$NON-NLS-2$
276+
pb.redirectErrorStream(true);
277+
Process process = pb.start();
278+
boolean finished = process.waitFor(5, TimeUnit.SECONDS);
279+
if (!finished)
280+
{
281+
process.destroyForcibly();
282+
return false;
283+
}
284+
return process.exitValue() == 0;
285+
}
286+
catch (IOException | InterruptedException e)
287+
{
288+
Logger.log("EIM does not support the gui subcommand, falling back to CLI mode."); //$NON-NLS-1$
289+
return false;
290+
}
291+
}
292+
231293
}

bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/MacOsEimLauncherStrategy.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ public MacOsEimLauncherStrategy(Display display, MessageConsoleStream standardCo
6262
@Override
6363
public LaunchResult launch(String eimPath) throws IOException
6464
{
65+
if (!isAppBundle(eimPath))
66+
{
67+
return launchCliDirect(eimPath);
68+
}
69+
6570
String appBundlePath = deriveAppBundlePath(eimPath);
6671
String execPath = deriveExecPath(eimPath, appBundlePath);
6772
String bundleId = readBundleId(appBundlePath);
@@ -108,6 +113,41 @@ public LaunchResult launch(String eimPath) throws IOException
108113
"osascript exit=" + exit + "\n" + out); //$NON-NLS-1$ //$NON-NLS-2$
109114
}
110115

116+
private LaunchResult launchCliDirect(String eimPath) throws IOException
117+
{
118+
String quotedPath = ProcessUtils.bashSingleQuote(eimPath);
119+
String bashCmd = "nohup " + quotedPath + " > /dev/null 2>&1 & echo $!"; //$NON-NLS-1$ //$NON-NLS-2$
120+
121+
Process launcher = new ProcessBuilder("bash", "-lc", bashCmd) //$NON-NLS-1$ //$NON-NLS-2$
122+
.redirectErrorStream(true).start();
123+
124+
String out = ProcessUtils.readAll(launcher.getInputStream());
125+
Long pid = ProcessUtils.parseFirstLongLine(out);
126+
127+
if (pid == null)
128+
{
129+
Logger.log("macOS CLI launcher output was:\n" + out); //$NON-NLS-1$
130+
throw new IOException("No PID found in launcher output. Output was:\n" + out); //$NON-NLS-1$
131+
}
132+
133+
return LaunchResult.ofPid(pid.longValue(), eimPath, out);
134+
}
135+
136+
private boolean isAppBundle(String eimPath)
137+
{
138+
Path p = Paths.get(eimPath).toAbsolutePath().normalize();
139+
while (p != null)
140+
{
141+
String name = p.getFileName() != null ? p.getFileName().toString() : ""; //$NON-NLS-1$
142+
if (name.endsWith(".app")) //$NON-NLS-1$
143+
{
144+
return true;
145+
}
146+
p = p.getParent();
147+
}
148+
return false;
149+
}
150+
111151
@Override
112152
public IStatus waitForExit(LaunchResult launchResult, IProgressMonitor monitor)
113153
{

bundles/com.espressif.idf.ui/META-INF/MANIFEST.MF

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,12 @@ Require-Bundle: org.eclipse.core.runtime,
3030
org.eclipse.ltk.core.refactoring,
3131
org.eclipse.tools.templates.core,
3232
org.eclipse.tools.templates.ui,
33-
org.eclipse.ui.intro
33+
org.eclipse.ui.intro,
34+
org.eclipse.jface,
35+
org.eclipse.terminal.view.ui;bundle-version="[1.0.0,2.0.0)",
36+
org.eclipse.terminal.view.core;bundle-version="[1.0.0,2.0.0)",
37+
org.eclipse.terminal.connector.process;bundle-version="[1.0.0,2.0.0)",
38+
org.eclipse.terminal.control;bundle-version="[1.0.0,2.0.0)"
3439
Automatic-Module-Name: com.espressif.idf.ui
3540
Bundle-ActivationPolicy: lazy
3641
Bundle-RequiredExecutionEnvironment: JavaSE-21
@@ -45,6 +50,7 @@ Bundle-ClassPath: .,
4550
lib/gson-2.8.7.jar,
4651
lib/commonmark-0.24.0.jar,
4752
lib/jfreechart-1.5.5.jar
48-
Import-Package: org.eclipse.core.expressions,
53+
Import-Package: org.eclipse.cdt.utils.pty;mandatory:=native,
54+
org.eclipse.core.expressions,
4955
org.eclipse.tools.templates.freemarker,
5056
org.eclipse.ui.editors.text

bundles/com.espressif.idf.ui/plugin.xml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -904,4 +904,21 @@
904904
parentId="org.eclipse.ui.textEditorScope">
905905
</context>
906906
</extension>
907+
<extension
908+
point="org.eclipse.terminal.control.connectors">
909+
<connector
910+
class="com.espressif.idf.ui.tools.eim.terminal.EimCliTerminalConnector"
911+
hidden="true"
912+
id="com.espressif.idf.ui.eimCliTerminalConnector"
913+
name="EIM CLI">
914+
</connector>
915+
</extension>
916+
<extension
917+
point="org.eclipse.terminal.view.ui.launcherDelegates">
918+
<delegate
919+
class="com.espressif.idf.ui.tools.eim.terminal.EimCliTerminalLauncherDelegate"
920+
id="com.espressif.idf.ui.launcher.eimCliTerminal"
921+
label="EIM CLI Wizard">
922+
</delegate>
923+
</extension>
907924
</plugin>

bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/EimButtonLaunchListener.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
import com.espressif.idf.core.tools.EimLoader;
2929
import com.espressif.idf.core.tools.ToolInitializer;
3030
import com.espressif.idf.core.tools.exceptions.EimVersionMismatchException;
31-
import com.espressif.idf.core.tools.launch.LaunchResult;
3231
import com.espressif.idf.core.tools.vo.EimJson;
3332
import com.espressif.idf.ui.UIPlugin;
3433
import com.espressif.idf.ui.handlers.EclipseHandler;
@@ -88,8 +87,9 @@ protected IStatus run(IProgressMonitor monitor)
8887
{
8988
try
9089
{
91-
var launchResult = eimLoader.launchEimWithResult(idfEnvironmentVariables.getEnvValue(IDFEnvironmentVariables.EIM_PATH));
92-
eimLoader.waitForEimClosure(launchResult, EimButtonLaunchListener.this::refreshAfterEimClose);
90+
EimGuiOrCliLauncher.launch(toolInitializer, eimLoader,
91+
idfEnvironmentVariables.getEnvValue(IDFEnvironmentVariables.EIM_PATH), standardConsoleStream,
92+
display, EimButtonLaunchListener.this::refreshAfterEimClose);
9393
}
9494
catch (IOException e)
9595
{
@@ -199,8 +199,8 @@ public void onCompleted(String filePath)
199199
try
200200
{
201201
idfEnvironmentVariables.addEnvVariable(IDFEnvironmentVariables.EIM_PATH, appToLaunch);
202-
LaunchResult launchResult = eimLoader.launchEimWithResult(appToLaunch);
203-
eimLoader.waitForEimClosure(launchResult, EimButtonLaunchListener.this::refreshAfterEimClose);
202+
EimGuiOrCliLauncher.launch(toolInitializer, eimLoader, appToLaunch, standardConsoleStream, display,
203+
EimButtonLaunchListener.this::refreshAfterEimClose);
204204
}
205205
catch (IOException e)
206206
{
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*******************************************************************************
2+
* Copyright 2026 Espressif Systems (Shanghai) PTE LTD. All rights reserved.
3+
* Use is subject to license terms.
4+
*******************************************************************************/
5+
package com.espressif.idf.ui.tools;
6+
7+
import java.io.IOException;
8+
9+
import org.eclipse.swt.widgets.Display;
10+
import org.eclipse.ui.console.MessageConsoleStream;
11+
12+
import com.espressif.idf.core.logging.Logger;
13+
import com.espressif.idf.core.tools.EimLoader;
14+
import com.espressif.idf.core.tools.ToolInitializer;
15+
import com.espressif.idf.core.tools.launch.LaunchResult;
16+
import com.espressif.idf.core.tools.watcher.EimJsonWatchService;
17+
import com.espressif.idf.ui.tools.eim.terminal.EimCliTerminalLaunchSupport;
18+
19+
/**
20+
* Launches EIM as a GUI app when the binary supports it; otherwise runs the CLI wizard in the integrated terminal.
21+
*
22+
* @author Ali Azam Rana <ali.azamrana@espressif.com>
23+
*/
24+
public final class EimGuiOrCliLauncher
25+
{
26+
private EimGuiOrCliLauncher()
27+
{
28+
}
29+
30+
/**
31+
* @param afterEimClosed invoked after EIM exits (GUI or CLI terminal), on a thread appropriate for the launch mode;
32+
* for the CLI path the runnable is executed on the SWT UI thread after the terminal process ends
33+
*/
34+
public static void launch(ToolInitializer toolInitializer, EimLoader eimLoader, String eimPath,
35+
MessageConsoleStream standardConsoleStream, Display display, Runnable afterEimClosed) throws IOException
36+
{
37+
if (toolInitializer.isEimGuiCapable(eimPath))
38+
{
39+
LaunchResult launchResult = eimLoader.launchEimWithResult(eimPath);
40+
eimLoader.waitForEimClosure(launchResult, afterEimClosed);
41+
return;
42+
}
43+
44+
EimJsonWatchService.getInstance().pauseListeners();
45+
display.syncExec(() -> {
46+
try
47+
{
48+
standardConsoleStream.write(Messages.EimCliTerminalOpeningWizard + "\n"); //$NON-NLS-1$
49+
EimCliTerminalLaunchSupport.launch(eimPath, () -> display.asyncExec(() -> {
50+
try
51+
{
52+
standardConsoleStream.write(Messages.EimCliTerminalWizardCompleted + "\n"); //$NON-NLS-1$
53+
}
54+
catch (IOException e)
55+
{
56+
Logger.log(e);
57+
}
58+
try
59+
{
60+
afterEimClosed.run();
61+
}
62+
finally
63+
{
64+
EimJsonWatchService.getInstance().unpauseListeners();
65+
}
66+
}));
67+
}
68+
catch (IOException e)
69+
{
70+
Logger.log(e);
71+
EimJsonWatchService.getInstance().unpauseListeners();
72+
}
73+
catch (RuntimeException e)
74+
{
75+
Logger.log(e);
76+
EimJsonWatchService.getInstance().unpauseListeners();
77+
}
78+
});
79+
}
80+
}

bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/EspressifToolStartup.java

Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,11 @@
4545
import com.espressif.idf.ui.IDFConsole;
4646
import com.espressif.idf.ui.UIPlugin;
4747
import com.espressif.idf.ui.handlers.EclipseHandler;
48+
import com.espressif.idf.ui.tools.EimGuiOrCliLauncher;
4849
import com.espressif.idf.ui.tools.manager.ESPIDFManagerEditor;
4950
import com.espressif.idf.ui.tools.manager.EimEditorInput;
5051
import com.espressif.idf.ui.tools.watcher.EimJsonUiChangeHandler;
5152

52-
import com.espressif.idf.core.tools.launch.LaunchResult;
53-
5453
/**
5554
* Startup class to handle the tools
5655
*
@@ -367,7 +366,6 @@ public void onCompleted(String filePath)
367366
}
368367
});
369368

370-
LaunchResult launchResult = null;
371369
String appToLaunch = filePath;
372370
try
373371
{
@@ -377,33 +375,31 @@ public void onCompleted(String filePath)
377375
}
378376

379377
idfEnvironmentVariables.addEnvVariable(IDFEnvironmentVariables.EIM_PATH, appToLaunch);
380-
launchResult = eimLoader.launchEimWithResult(appToLaunch);
378+
EimGuiOrCliLauncher.launch(toolInitializer, eimLoader, appToLaunch, standardConsoleStream,
379+
Display.getDefault(), () -> {
380+
if (toolInitializer.isOldEspIdfConfigPresent() && !toolInitializer.isOldConfigExported())
381+
{
382+
Logger.log("Old configuration found and not converted");
383+
handleOldConfigExport();
384+
}
385+
try
386+
{
387+
eimJson = toolInitializer.loadEimJson();
388+
}
389+
catch (EimVersionMismatchException e)
390+
{
391+
Logger.log(e);
392+
MessageDialog.openError(Display.getDefault().getActiveShell(), e.msgTitle(),
393+
e.getMessage());
394+
return;
395+
}
396+
openEspIdfManager(eimJson);
397+
});
381398
}
382-
catch (
383-
IOException
384-
| InterruptedException e)
399+
catch (IOException | InterruptedException e)
385400
{
386401
Logger.log(e);
387402
}
388-
389-
eimLoader.waitForEimClosure(launchResult, () -> {
390-
if (toolInitializer.isOldEspIdfConfigPresent() && !toolInitializer.isOldConfigExported())
391-
{
392-
Logger.log("Old configuration found and not converted");
393-
handleOldConfigExport();
394-
}
395-
try
396-
{
397-
eimJson = toolInitializer.loadEimJson();
398-
}
399-
catch (EimVersionMismatchException e)
400-
{
401-
Logger.log(e);
402-
MessageDialog.openError(Display.getDefault().getActiveShell(), e.msgTitle(), e.getMessage());
403-
return;
404-
}
405-
openEspIdfManager(eimJson);
406-
});
407403
}
408404

409405
@Override

0 commit comments

Comments
 (0)