This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new af3014825742 camel-jbang - TUI add F2 Open and F10 Process Control
popup
af3014825742 is described below
commit af3014825742ee7ae2f7ae24c5c268e09df14404
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Aug 4 22:17:33 2026 +0200
camel-jbang - TUI add F2 Open and F10 Process Control popup
F2 Open: Replace "Run from Folder..." with "Open..." that opens an
existing project as a phantom integration shown as Stopped in Overview.
Supports Maven projects (spring-boot, quarkus, camel-main detected via
pom.xml) and flat directories (camel run --source-dir with dev-mode).
F10 Process Control: Consolidate r/x/p/X keys into a single F10 popup
with context-aware options. Phantom shows Run. Running integration shows
Stop Routes/Start Routes/Restart/Stop/Kill. Infra shows Stop/Kill.
Popup displays integration name in title with emoji and color-coded
actions.
Also: mouse click opens files directly in Source tab file browser,
fix phantom accumulation in non-full-scan mode, fix restart blank
screen with time-limited forceFullScan.
camel-jbang - TUI add Remove option to F10 Process Control for phantoms
camel-jbang - TUI restrict phantom tabs to Overview and Source only
Phantom integrations now show only Overview and Source in the tab bar,
hiding all other tabs that would be empty without a running process.
Digit keys 3-9/0 are blocked, mouse clicks map to the two visible
tabs, and switching to a phantom from a disallowed tab auto-redirects
to Overview. Enter on a phantom in Overview goes to Source.
camel-jbang - TUI align F10 Control popup style with F2 Actions popup
Use ListWidget with ListState and Theme.selectionBg() highlight,
matching the F2 Actions popup style. Title changed to " Control ".
camel-jbang - TUI move Close to F10 Control and fix emoji rendering
Remove Close from F2 Actions menu since phantom removal is handled
by F10 Control popup via Remove option. Replace custom emojis in F10
with existing TuiIcons constants to fix character width rendering.
camel-jbang - TUI rename to Open/Close Project and add MCP tools
Rename F2 "Open..." to "Open Project..." and F10 "Remove" to
"Close Project" for clarity. Move Stop All from F2 to F10 popup.
Add tui_open_project MCP tool and stop-all/close actions to
tui_control tool.
camel-jbang - TUI fix F10 popup icons to use 2-column-wide emojis
Replace single-width icons (β, β) with proper 2-column emojis in
the F10 Control popup to fix alignment. Add SLEEP icon (π€) for
Stop Routes to distinguish from Stop/Stop All (π).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../dsl/jbang/core/commands/tui/ActionsPopup.java | 81 ++++++--
.../dsl/jbang/core/commands/tui/CamelMonitor.java | 158 +++++++++++----
.../core/commands/tui/DataRefreshService.java | 29 ++-
.../dsl/jbang/core/commands/tui/FilesBrowser.java | 6 +
.../jbang/core/commands/tui/FolderInputPopup.java | 46 +++--
.../jbang/core/commands/tui/IntegrationInfo.java | 3 +
.../dsl/jbang/core/commands/tui/LaunchManager.java | 53 +++++
.../dsl/jbang/core/commands/tui/McpFacade.java | 56 +++++-
.../jbang/core/commands/tui/MonitorContext.java | 24 +++
.../dsl/jbang/core/commands/tui/OverviewTab.java | 74 +++----
.../core/commands/tui/ProcessControlPopup.java | 213 +++++++++++++++++++++
.../dsl/jbang/core/commands/tui/SourceTab.java | 1 +
.../dsl/jbang/core/commands/tui/TuiIcons.java | 1 +
.../jbang/core/commands/tui/TuiToolRegistry.java | 24 ++-
14 files changed, 646 insertions(+), 123 deletions(-)
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
index 6570382b9299..a19d3b71ef24 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
@@ -53,12 +53,11 @@ class ActionsPopup {
SWITCH_INTEGRATION,
SEND_MESSAGE,
RUN_EXAMPLE,
- RUN_FOLDER,
+ OPEN_PROJECT,
RUN_INFRA,
BROWSE_FILES,
DOCTOR,
RESET_STATS,
- STOP_ALL,
SHELL,
SCREEN_SUBMENU,
SETTINGS,
@@ -131,6 +130,7 @@ class ActionsPopup {
private final CaptionOverlay captionOverlay;
private final LaunchManager launchManager;
+ private IntegrationInfo pendingPhantomRun;
private BiConsumer<String, Boolean> notificationCallback;
private String preSelectedRouteId;
@@ -160,7 +160,6 @@ class ActionsPopup {
infraBrowserPopup.setBurstCallback(burstCallback);
this.folderInputPopup = new FolderInputPopup(launchManager);
folderInputPopup.setBurstCallback(burstCallback);
- folderInputPopup.setOnFolderConfirmed(this::openFolderRunOptionsForm);
folderInputPopup.setInfraCatalogClearer(infraBrowserPopup::clearCatalog);
sendMessagePopup.setFileBrowser(sendFileBrowser);
}
@@ -168,6 +167,7 @@ class ActionsPopup {
void setContext(MonitorContext ctx) {
this.ctx = ctx;
docViewerPopup.setContext(ctx);
+ folderInputPopup.setContext(ctx);
}
LaunchManager getLaunchManager() {
@@ -280,8 +280,7 @@ class ActionsPopup {
flat.add(Action.SWITCH_INTEGRATION);
flat.add(null);
flat.addAll(List.of(
- Action.SEND_MESSAGE, Action.RUN_EXAMPLE, Action.RUN_FOLDER,
Action.RUN_INFRA, Action.BROWSE_FILES,
- Action.STOP_ALL));
+ Action.SEND_MESSAGE, Action.RUN_EXAMPLE, Action.OPEN_PROJECT,
Action.RUN_INFRA, Action.BROWSE_FILES));
flat.add(null);
flat.addAll(List.of(Action.DOCTOR, Action.RESET_STATS,
Action.SCREEN_SUBMENU, Action.SETTINGS));
flat.add(null);
@@ -425,10 +424,9 @@ class ActionsPopup {
labels.add("βββ");
labels.add("Send Message");
labels.add("Run an Example...");
- labels.add("Run from Folder...");
+ labels.add("Open Project...");
labels.add("Run Dev/Infra Service...");
labels.add("Browse Files... (Ctrl+F)");
- labels.add("Stop All");
labels.add("βββ");
labels.add("Run Doctor");
labels.add("Reset Stats");
@@ -528,15 +526,18 @@ class ActionsPopup {
if (runOptionsForm.isVisible()) {
if (ke.isCancel()) {
runOptionsForm.close();
+ pendingPhantomRun = null;
if (folderInputPopup.getSelectedFolder() != null) {
folderInputPopup.showInput();
- } else {
+ } else if (!showActionsMenu) {
exampleBrowserPopup.open();
}
} else if (ke.isConfirm()) {
String error = runOptionsForm.validate();
if (error != null) {
runOptionsForm.setError(error);
+ } else if (pendingPhantomRun != null) {
+ launchFromPhantom();
} else if (folderInputPopup.getSelectedFolder() != null) {
launchFromFolder();
} else {
@@ -657,7 +658,7 @@ class ActionsPopup {
} else if (action == Action.RUN_EXAMPLE) {
showActionsMenu = false;
exampleBrowserPopup.open();
- } else if (action == Action.RUN_FOLDER) {
+ } else if (action == Action.OPEN_PROJECT) {
showActionsMenu = false;
folderInputPopup.open();
} else if (action == Action.SCREENSHOT) {
@@ -721,11 +722,6 @@ class ActionsPopup {
Theme.toggle();
refreshTheme();
showActionsMenu = false;
- } else if (action == Action.STOP_ALL) {
- showActionsMenu = false;
- stopAllPopup.setConfirmActions(ctx != null &&
ctx.confirmActions);
- stopAllPopup.open();
- checkStopAllNotification();
} else if (action == Action.CAPTION) {
showActionsMenu = false;
captionOverlay.openInline();
@@ -972,10 +968,6 @@ class ActionsPopup {
frame.renderWidget(Clear.INSTANCE, popup);
String divider = " βββββββββββββββββββββββββββββββββ";
- String stopLabel = stopAllPopup.hasBothGroups()
- ? TuiIcons.menuItem(TuiIcons.STOP, "Stop All...")
- : TuiIcons.menuItem(TuiIcons.STOP, "Stop All");
-
boolean canSwitch = hasMultipleIntegrations();
List<ListItem> items = new ArrayList<>();
// Group 0: Navigation
@@ -990,12 +982,11 @@ class ActionsPopup {
? ListItem.from(TuiIcons.menuItem(TuiIcons.MESSAGE, "Send
Message"))
: ListItem.from(TuiIcons.menuItem(TuiIcons.MESSAGE, "Send
Message")).style(Style.EMPTY.dim()));
items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.CAMEL, "Run an
Example...")));
- items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.FOLDER_OPEN, "Run
from Folder...")));
+ items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.FOLDER_OPEN, "Open
Project...")));
items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.INFRA, "Run
Dev/Infra Service...")));
items.add(hasSelection
? ListItem.from(TuiIcons.menuItem(TuiIcons.FOLDER, "Browse
Files... (Ctrl+F)"))
: ListItem.from(TuiIcons.menuItem(TuiIcons.FOLDER, "Browse
Files... (Ctrl+F)")).style(Style.EMPTY.dim()));
- items.add(ListItem.from(stopLabel));
items.add(ListItem.from(divider).style(Style.EMPTY.dim()));
// Group 2: Diagnostics & Screen
items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.DOCTOR, "Run
Doctor")));
@@ -1129,6 +1120,18 @@ class ActionsPopup {
openDocForEntry(name, kind, catalog);
}
+ void openStopAll() {
+ stopAllPopup.setConfirmActions(ctx != null && ctx.confirmActions);
+ stopAllPopup.open();
+ checkStopAllNotification();
+ }
+
+ boolean hasRunningProcesses() {
+ return stopAllPopup.hasBothGroups()
+ || integrations.get().stream().anyMatch(i -> !i.vanishing &&
!i.phantom)
+ || infraServices.get().stream().anyMatch(i -> !i.vanishing);
+ }
+
private void openDocForEntry(String name, String kind,
org.apache.camel.catalog.CamelCatalog catalog) {
if (catalog == null) {
return;
@@ -1249,6 +1252,44 @@ class ActionsPopup {
folderInputPopup.launchFolder(displayName, extraArgs, jaegerExport);
}
+ void openRunOptionsForPhantom(IntegrationInfo phantom) {
+ pendingPhantomRun = phantom;
+ String displayName = phantom.name != null ? phantom.name : "project";
+ boolean isMaven = phantom.projectType != null;
+ if (isMaven) {
+ int lockedRuntime = switch (phantom.projectType) {
+ case "spring-boot" -> 1;
+ case "quarkus" -> 2;
+ default -> 0;
+ };
+ runOptionsForm.open(displayName, displayName, false, false,
lockedRuntime);
+ } else {
+ runOptionsForm.open(displayName, displayName, false, true);
+ }
+ }
+
+ private void launchFromPhantom() {
+ IntegrationInfo phantom = pendingPhantomRun;
+ pendingPhantomRun = null;
+ if (phantom == null) {
+ return;
+ }
+ String displayName = runOptionsForm.name();
+ if (displayName.isEmpty()) {
+ displayName = phantom.name != null ? phantom.name : "project";
+ }
+ List<String> extraArgs = runOptionsForm.buildArgs();
+ runOptionsForm.close();
+
+ ctx.removePhantom(phantom.pid);
+
+ if (phantom.projectType != null) {
+ launchManager.launchMavenProject(phantom.sourceDir,
phantom.projectType, displayName, extraArgs);
+ } else {
+ launchManager.launchCamelRun(phantom.sourceDir, displayName,
extraArgs);
+ }
+ }
+
// ---- Name Input ----
private void openExampleNameInput() {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
index b4bea74bd1fd..b60077ef396d 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
@@ -152,6 +152,7 @@ public class CamelMonitor extends CamelCommand {
private final PanelAnimation logPinAnim = new PanelAnimation();
private ActionsPopup actionsPopup;
+ private ProcessControlPopup processControlPopup;
private TuiRunner runner;
// Set by TuiWebServer for browser sessions; local terminal sessions leave
this null
// and let TuiBackendHelper auto-detect the active terminal instead.
@@ -282,6 +283,39 @@ public class CamelMonitor extends CamelCommand {
actionsPopup.setContext(ctx);
actionsPopup.setMonitorContext(ctx);
actionsPopup.setNotificationCallback((msg, error) ->
setNotification(msg, error));
+
+ processControlPopup = new ProcessControlPopup(ctx);
+ processControlPopup.setActions(new
ProcessControlPopup.ControlActions() {
+ @Override
+ public void sendRouteCommand(String pid, String routeId, String
command) {
+ CamelMonitor.this.sendRouteCommand(pid, routeId, command);
+ }
+
+ @Override
+ public void stopSelectedProcess(boolean forceKill) {
+ CamelMonitor.this.stopSelectedProcess(forceKill);
+ }
+
+ @Override
+ public void restartSelectedProcess() {
+ CamelMonitor.this.restartSelectedProcess();
+ }
+
+ @Override
+ public void onRunPhantom(IntegrationInfo phantom) {
+ actionsPopup.openRunOptionsForPhantom(phantom);
+ }
+
+ @Override
+ public void onStopAll() {
+ actionsPopup.openStopAll();
+ }
+
+ @Override
+ public boolean hasRunningProcesses() {
+ return actionsPopup.hasRunningProcesses();
+ }
+ });
ctx.notificationCallback = (msg, error) -> setNotification(msg, error);
ctx.openMarkdownCallback = actionsPopup::openMarkdown;
ctx.openOptionsCallback = actionsPopup::openOptions;
@@ -512,6 +546,11 @@ public class CamelMonitor extends CamelCommand {
stopSelectedProcess(forceKill);
}
+ @Override
+ public void stopAll() {
+ actionsPopup.openStopAll();
+ }
+
@Override
public void resetIntegrationTabState() {
CamelMonitor.this.resetIntegrationTabState();
@@ -700,6 +739,9 @@ public class CamelMonitor extends CamelCommand {
}
return result;
}
+ if (processControlPopup.isVisible()) {
+ return processControlPopup.handleKeyEvent(ke);
+ }
if (popupManager.handleKeyEvent(ke,
tabRegistry.selectedTabIndex(), TAB_LOG)) {
return true;
}
@@ -816,26 +858,28 @@ public class CamelMonitor extends CamelCommand {
if (ke.isChar('2')) {
return tabRegistry.handleTabKey(TAB_SOURCE, ctx,
dataService);
}
- if (ke.isChar('4')) {
- return tabRegistry.handleTabKey(TAB_ACTIVITY, ctx,
dataService);
- }
- if (ke.isChar('5')) {
- return tabRegistry.handleTabKey(TAB_DIAGRAM, ctx,
dataService);
- }
- if (ke.isChar('6')) {
- return tabRegistry.handleTabKey(TAB_ROUTES, ctx,
dataService);
- }
- if (ke.isChar('7')) {
- return tabRegistry.handleTabKey(TAB_ENDPOINTS, ctx,
dataService);
- }
- if (ke.isChar('8')) {
- return tabRegistry.handleTabKey(TAB_HISTORY, ctx,
dataService);
- }
- if (ke.isChar('9')) {
- return tabRegistry.handleTabKey(TAB_ERRORS, ctx,
dataService);
- }
- if (ke.isChar('0')) {
- return tabRegistry.handleTabKey(TAB_MORE, ctx,
dataService);
+ if (!isPhantomSelected()) {
+ if (ke.isChar('4')) {
+ return tabRegistry.handleTabKey(TAB_ACTIVITY, ctx,
dataService);
+ }
+ if (ke.isChar('5')) {
+ return tabRegistry.handleTabKey(TAB_DIAGRAM, ctx,
dataService);
+ }
+ if (ke.isChar('6')) {
+ return tabRegistry.handleTabKey(TAB_ROUTES, ctx,
dataService);
+ }
+ if (ke.isChar('7')) {
+ return tabRegistry.handleTabKey(TAB_ENDPOINTS, ctx,
dataService);
+ }
+ if (ke.isChar('8')) {
+ return tabRegistry.handleTabKey(TAB_HISTORY, ctx,
dataService);
+ }
+ if (ke.isChar('9')) {
+ return tabRegistry.handleTabKey(TAB_ERRORS, ctx,
dataService);
+ }
+ if (ke.isChar('0')) {
+ return tabRegistry.handleTabKey(TAB_MORE, ctx,
dataService);
+ }
}
}
}
@@ -906,6 +950,10 @@ public class CamelMonitor extends CamelCommand {
popupManager.openSwitchPopup(ctx.selectedPid,
getNonVanishingIntegrations());
return true;
}
+ if (ke.isKey(KeyCode.F10)) {
+ processControlPopup.open();
+ return true;
+ }
if (ke.hasCtrl() && ke.isChar('f')) {
openFilesPopup();
return true;
@@ -975,9 +1023,13 @@ public class CamelMonitor extends CamelCommand {
if (ke.isConfirm() && tab == TAB_OVERVIEW) {
tabRegistry.overviewTab().selectCurrentIntegration();
if (ctx.selectedPid != null) {
- int selectTab = resolveSelectTab();
- if (selectTab != TAB_OVERVIEW) {
- tabRegistry.handleTabKey(selectTab, ctx, dataService);
+ if (isPhantomSelected()) {
+ tabRegistry.handleTabKey(TAB_SOURCE, ctx, dataService);
+ } else {
+ int selectTab = resolveSelectTab();
+ if (selectTab != TAB_OVERVIEW) {
+ tabRegistry.handleTabKey(selectTab, ctx, dataService);
+ }
}
}
return true;
@@ -1027,9 +1079,11 @@ public class CamelMonitor extends CamelCommand {
int clickedTab = findClickedTab(me.x() - lastTabsArea.x());
if (clickedTab >= 0) {
if (isInfraSelected()) {
- // Infra mode: map 0βOverview, 1βLog
int realTab = clickedTab == 1 ? TAB_LOG : TAB_OVERVIEW;
tabsState.select(realTab);
+ } else if (isPhantomSelected()) {
+ int realTab = clickedTab == 1 ? TAB_SOURCE :
TAB_OVERVIEW;
+ tabRegistry.handleTabKey(realTab, ctx, dataService);
} else {
tabRegistry.handleTabKey(clickedTab, ctx, dataService);
}
@@ -1332,6 +1386,12 @@ public class CamelMonitor extends CamelCommand {
contentArea = new Rect(area.x(), area.y(), area.width(),
area.height() - 1);
lastContentArea = contentArea;
} else {
+ if (isPhantomSelected()) {
+ int t = tabRegistry.selectedTabIndex();
+ if (t != TAB_OVERVIEW && t != TAB_SOURCE) {
+ tabsState.select(TAB_OVERVIEW);
+ }
+ }
renderHeader(frame, mainChunks.get(0));
renderTabs(frame, mainChunks.get(1));
lastTabsArea = mainChunks.get(1);
@@ -1404,6 +1464,7 @@ public class CamelMonitor extends CamelCommand {
popupManager.renderConfirm(frame, contentArea);
}
actionsPopup.render(frame, contentArea);
+ processControlPopup.render(frame, contentArea);
if (captionOverlay.isCaptionVisible()) {
captionOverlay.render(frame, contentArea);
}
@@ -1547,7 +1608,6 @@ public class CamelMonitor extends CamelCommand {
Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_LOG, "3",
"Log"))
};
- // Map real tab index to infra tab index for highlight
int infraTabIdx = tabsState.selected() == TAB_LOG ? 1 : 0;
TabsState infraTabsState = new TabsState(infraTabIdx);
@@ -1566,6 +1626,30 @@ public class CamelMonitor extends CamelCommand {
return;
}
+ if (isPhantomSelected()) {
+ Line[] labels = {
+ Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_OVERVIEW,
"1", "Overview")),
+ Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_SOURCE,
"2", "Source"))
+ };
+
+ int phantomTabIdx = tabsState.selected() == TAB_SOURCE ? 1 : 0;
+ TabsState phantomTabsState = new TabsState(phantomTabIdx);
+
+ Tabs tabs = Tabs.builder()
+ .titles(labels)
+ .highlightStyle(Theme.accentBg())
+ .divider(divider)
+ .build();
+
+ Rect labelsArea = area.height() >= 2
+ ? new Rect(area.x(), area.y() + 1, area.width(), 1)
+ : area;
+ frame.renderStatefulWidget(tabs, labelsArea, phantomTabsState);
+ lastTabLabels = labels;
+ lastTabDivider = dividerStr;
+ return;
+ }
+
Line[] labels = {
Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_OVERVIEW,
"1", "Overview")),
Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_SOURCE, "2",
"Source")),
@@ -1831,6 +1915,7 @@ public class CamelMonitor extends CamelCommand {
// remember name so the restarted process gets auto-selected
ctx.lastSelectedName = name;
+ dataService.forceFullScan();
// stop gracefully
ph.destroy();
@@ -2152,22 +2237,12 @@ public class CamelMonitor extends CamelCommand {
actionsPopup.renderFooter(spans);
return 0;
}
+ if (processControlPopup.isVisible()) {
+ processControlPopup.renderFooter(spans);
+ return 0;
+ }
tabRegistry.overviewTab().renderFooter(spans);
int fKeyTotal = insertFKeyHints(spans);
- // Process action hints
- if (ctx.selectedPid != null && !isInfraSelected()) {
- IntegrationInfo selInfo = findSelectedIntegration();
- if (selInfo != null) {
- hint(spans, "p", selInfo.routeStarted > 0 ? "stop routes" :
"start routes");
- }
- }
- if (ctx.selectedPid != null) {
- if (!isInfraSelected()) {
- hint(spans, "r", "restart");
- }
- hint(spans, "x", "stop");
- hint(spans, "X", "kill");
- }
return fKeyTotal;
}
@@ -2254,6 +2329,11 @@ public class CamelMonitor extends CamelCommand {
return ctx.isInfraSelected();
}
+ private boolean isPhantomSelected() {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ return info != null && info.phantom;
+ }
+
private String selectedName() {
return ctx.selectedName();
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshService.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshService.java
index 24cf3b3a220b..e9f9d79d4474 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshService.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshService.java
@@ -82,6 +82,7 @@ class DataRefreshService {
private volatile List<Long> cachedPids = Collections.emptyList();
private volatile long lastFullScanTime;
private volatile long lastLivenessCheckTime;
+ private volatile long forceFullScanUntil;
private volatile long burstModeUntil;
final Set<String> stoppingPids = ConcurrentHashMap.newKeySet();
@@ -162,6 +163,10 @@ class DataRefreshService {
burstModeUntil = System.currentTimeMillis() + 20_000;
}
+ void forceFullScan() {
+ forceFullScanUntil = System.currentTimeMillis() + 20_000;
+ }
+
boolean isBurstMode() {
return System.currentTimeMillis() < burstModeUntil;
}
@@ -229,7 +234,8 @@ class DataRefreshService {
List<IntegrationInfo> infos = new ArrayList<>();
long now = System.currentTimeMillis();
boolean wantFullScan = refreshCtx.selectedTab() ==
TabRegistry.TAB_OVERVIEW || refreshCtx.isSwitchPopupVisible()
- || cachedPids.isEmpty();
+ || cachedPids.isEmpty()
+ || forceFullScanUntil > now;
long scanInterval = isBurstMode() ? 1000 : 2000;
boolean fullScan = wantFullScan && (now - lastFullScanTime >=
scanInterval);
List<Long> pids;
@@ -286,6 +292,9 @@ class DataRefreshService {
}
List<IntegrationInfo> previous = data.get();
for (IntegrationInfo prev : previous) {
+ if (prev.phantom) {
+ continue;
+ }
if (!prev.vanishing && !ctx.selectedPid.equals(prev.pid)) {
if (checkLiveness) {
try {
@@ -303,6 +312,7 @@ class DataRefreshService {
}
handleVanishing(infos, now);
+ mergePhantoms(infos);
data.set(infos);
return fullScan;
}
@@ -311,6 +321,9 @@ class DataRefreshService {
Set<String> livePids = infos.stream().map(i ->
i.pid).collect(Collectors.toSet());
List<IntegrationInfo> previous = data.get();
for (IntegrationInfo prev : previous) {
+ if (prev.phantom) {
+ continue;
+ }
if (!prev.vanishing && !livePids.contains(prev.pid) &&
!vanishing.containsKey(prev.pid)) {
boolean wasExplicitStop = stoppingPids.remove(prev.pid);
if (wasExplicitStop) {
@@ -338,6 +351,20 @@ class DataRefreshService {
}
}
+ private void mergePhantoms(List<IntegrationInfo> infos) {
+ Set<String> liveDirs = infos.stream()
+ .filter(i -> !i.vanishing && !i.phantom && i.directory != null)
+ .map(i -> i.directory)
+ .collect(Collectors.toSet());
+ for (IntegrationInfo phantom : ctx.phantomIntegrations) {
+ if (phantom.sourceDir != null &&
liveDirs.contains(phantom.sourceDir)) {
+ ctx.removePhantom(phantom.pid);
+ } else {
+ infos.add(phantom);
+ }
+ }
+ }
+
private void handleAutoSelect(List<IntegrationInfo> infos, boolean
fullScan) {
if (ctx.selectedPid != null && !refreshCtx.isInfraSelected()) {
boolean stillAlive = infos.stream()
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FilesBrowser.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FilesBrowser.java
index 365e344bf524..ddf3c62baa13 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FilesBrowser.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FilesBrowser.java
@@ -150,6 +150,12 @@ class FilesBrowser {
int clicked = listState.offset() + (me.y() - innerTop);
if (clicked >= 0 && clicked < entries.size()) {
listState.select(clicked);
+ FileEntry entry = entries.get(clicked);
+ if (entry.directory()) {
+ loadDirectory(Path.of(entry.path()));
+ } else {
+ sourceViewer.loadFile(Path.of(entry.path()));
+ }
}
return true;
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FolderInputPopup.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FolderInputPopup.java
index 28573981d15f..8fec5ccb3424 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FolderInputPopup.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FolderInputPopup.java
@@ -51,16 +51,22 @@ class FolderInputPopup {
private String detectedPomPath;
private final FolderBrowser folderBrowser = new FolderBrowser();
+ private MonitorContext ctx;
private final LaunchManager launchManager;
private Runnable burstCallback;
private BiConsumer<String, Boolean> notificationCallback;
private Runnable onFolderConfirmed;
private Runnable infraCatalogClearer;
+ private Runnable onPhantomCreated;
FolderInputPopup(LaunchManager launchManager) {
this.launchManager = launchManager;
}
+ void setContext(MonitorContext ctx) {
+ this.ctx = ctx;
+ }
+
void setBurstCallback(Runnable burstCallback) {
this.burstCallback = burstCallback;
}
@@ -77,6 +83,10 @@ class FolderInputPopup {
this.infraCatalogClearer = clearer;
}
+ void setOnPhantomCreated(Runnable callback) {
+ this.onPhantomCreated = callback;
+ }
+
boolean isVisible() {
return showInput || folderBrowser.isVisible();
}
@@ -194,7 +204,7 @@ class FolderInputPopup {
TuiHelper.hint(spans, "ββ", "history");
}
TuiHelper.hint(spans, "Tab", "browse");
- TuiHelper.hint(spans, "Enter", "run...");
+ TuiHelper.hint(spans, "Enter", "open");
TuiHelper.hintLast(spans, "Esc", "back");
}
}
@@ -231,8 +241,9 @@ class FolderInputPopup {
folder = System.getProperty("user.home") + folder.substring(1);
}
Path dirPath = Path.of(folder);
- if (!Files.isDirectory(dirPath)) {
- notify("Directory does not exist: " + folder, true);
+ boolean singleFile = Files.isRegularFile(dirPath);
+ if (!singleFile && !Files.isDirectory(dirPath)) {
+ notify("Path does not exist: " + folder, true);
return;
}
folderHistory.remove(folder);
@@ -240,20 +251,33 @@ class FolderInputPopup {
if (folderHistory.size() > 20) {
folderHistory.remove(folderHistory.size() - 1);
}
- selectedFolder = folder;
showInput = false;
persistLastFolder(folder);
- Path pomFile = dirPath.resolve("pom.xml");
+ String projectDir = singleFile ? dirPath.getParent().toString() :
folder;
+ Path pomFile = Path.of(projectDir).resolve("pom.xml");
String runtime = Files.isRegularFile(pomFile) ?
TuiHelper.detectPomRuntime(pomFile) : null;
- if (runtime != null) {
- detectedPomPath = pomFile.toString();
+
+ selectedFolder = projectDir;
+ detectedPomPath = runtime != null ? pomFile.toString() : null;
+
+ IntegrationInfo phantom = new IntegrationInfo();
+ phantom.name = dirPath.getFileName().toString();
+ phantom.directory = projectDir;
+ phantom.sourceDir = projectDir;
+ phantom.projectType = runtime;
+ if ("spring-boot".equals(runtime)) {
+ phantom.platform = "Spring Boot";
+ } else if ("quarkus".equals(runtime)) {
+ phantom.platform = "Quarkus";
} else {
- detectedPomPath = null;
+ phantom.platform = "Camel";
}
+ ctx.addPhantom(phantom);
+ ctx.selectedPid = phantom.pid;
- if (onFolderConfirmed != null) {
- onFolderConfirmed.run();
+ if (onPhantomCreated != null) {
+ onPhantomCreated.run();
}
}
@@ -314,7 +338,7 @@ class FolderInputPopup {
Block block = Block.builder()
.borderType(BorderType.ROUNDED).borders(Borders.ALL)
- .title(" Run from Folder ")
+ .title(" Open Project ")
.build();
frame.renderWidget(block, popup);
Rect inner = block.inner(popup);
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
index 9fcc784d5dad..19e445252f8f 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
@@ -32,6 +32,9 @@ class IntegrationInfo {
String directory;
String profile;
boolean devMode;
+ boolean phantom;
+ String projectType;
+ String sourceDir;
String ready;
int state;
long uptime;
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManager.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManager.java
index 5c4c8d556257..91a6770073e9 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManager.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManager.java
@@ -192,6 +192,59 @@ class LaunchManager {
return false;
}
+ void launchMavenProject(String dir, String projectType, String
displayName, List<String> extraArgs) {
+ try {
+ List<String> cmd = new ArrayList<>();
+ cmd.add(resolveMvnCommand(dir));
+ switch (projectType) {
+ case "spring-boot" -> cmd.add("spring-boot:run");
+ case "quarkus" -> cmd.add("quarkus:dev");
+ default -> cmd.add("camel:run");
+ }
+ cmd.addAll(extraArgs);
+ Path outputFile = createSecureTempFile("camel-maven-", ".log");
+ outputFile.toFile().deleteOnExit();
+ ProcessBuilder pb = new ProcessBuilder(cmd);
+ pb.directory(new java.io.File(dir));
+ pb.redirectErrorStream(true);
+ pb.redirectOutput(outputFile.toFile());
+ Process process = pb.start();
+ addPendingLaunch(displayName, process, outputFile);
+ notify("Starting: " + displayName + " (mvn " + cmd.get(1) + ")",
false);
+ } catch (Exception e) {
+ notify("Failed to start Maven project: " + e.getMessage(), true);
+ }
+ }
+
+ void launchCamelRun(String sourceDir, String displayName, List<String>
extraArgs) {
+ try {
+ List<String> cmd = new
ArrayList<>(LauncherHelper.getCamelCommand());
+ cmd.add("run");
+ cmd.add("--source-dir=" + sourceDir);
+ cmd.add("--logging-color=true");
+ cmd.addAll(extraArgs);
+ Path outputFile = createSecureTempFile("camel-folder-", ".log");
+ outputFile.toFile().deleteOnExit();
+ ProcessBuilder pb = new ProcessBuilder(cmd);
+ pb.redirectErrorStream(true);
+ pb.redirectOutput(outputFile.toFile());
+ Process process = pb.start();
+ addPendingLaunch(displayName, process, outputFile);
+ notify("Starting: " + displayName, false);
+ } catch (Exception e) {
+ notify("Failed to start: " + sourceDir + " - " + e.getMessage(),
true);
+ }
+ }
+
+ private static String resolveMvnCommand(String dir) {
+ String wrapper = System.getProperty("os.name",
"").toLowerCase().contains("win") ? "mvnw.cmd" : "./mvnw";
+ Path wrapperPath = Path.of(dir).resolve(wrapper.startsWith("./") ?
wrapper.substring(2) : wrapper);
+ if (Files.isRegularFile(wrapperPath)) {
+ return wrapperPath.toString();
+ }
+ return "mvn";
+ }
+
private void checkDeferredLaunch(long now) {
if (deferredLaunch != null) {
Set<String> runningAliases = infraServices.get().stream()
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
index 4b99916c4dc9..df238ba3e82d 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
@@ -80,6 +80,8 @@ class McpFacade {
void stopProcess(boolean forceKill);
+ void stopAll();
+
void resetIntegrationTabState();
}
@@ -835,6 +837,22 @@ class McpFacade {
if (action == null || action.isBlank()) {
return "Error: action is required";
}
+ if ("stop-all".equals(action)) {
+ bridge.stopAll();
+ return "Stopping all processes";
+ }
+ if ("close".equals(action)) {
+ if (ctx.selectedPid == null) {
+ return "Error: no integration selected";
+ }
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ if (info == null || !info.phantom) {
+ return "Error: selected integration is not a phantom (opened
but not running)";
+ }
+ ctx.removePhantom(info.pid);
+ ctx.selectedPid = null;
+ return "Closed project: " + info.name;
+ }
if (ctx.selectedPid == null) {
return "Error: no integration selected";
}
@@ -870,8 +888,44 @@ class McpFacade {
yield "Killed " + name;
}
default -> "Unknown action: " + action
- + ". Available: stop-routes, start-routes, restart,
stop, kill";
+ + ". Available: stop-routes, start-routes, restart,
stop, kill, stop-all, close";
};
}
+ String openProject(String directory) {
+ Path dirPath = Path.of(directory).toAbsolutePath().normalize();
+ if (!Files.isDirectory(dirPath)) {
+ if (Files.isRegularFile(dirPath)) {
+ dirPath = dirPath.getParent();
+ } else {
+ return "Error: directory does not exist: " + directory;
+ }
+ }
+ IntegrationInfo existing =
ctx.findPhantomByDirectory(dirPath.toString());
+ if (existing != null) {
+ ctx.selectedPid = existing.pid;
+ return "Project already open: " + existing.name;
+ }
+
+ Path pomFile = dirPath.resolve("pom.xml");
+ String runtime = Files.isRegularFile(pomFile) ?
TuiHelper.detectPomRuntime(pomFile) : null;
+
+ IntegrationInfo phantom = new IntegrationInfo();
+ phantom.name = dirPath.getFileName().toString();
+ phantom.directory = dirPath.toString();
+ phantom.sourceDir = dirPath.toString();
+ phantom.projectType = runtime;
+ if ("spring-boot".equals(runtime)) {
+ phantom.platform = "Spring Boot";
+ } else if ("quarkus".equals(runtime)) {
+ phantom.platform = "Quarkus";
+ } else {
+ phantom.platform = "Camel";
+ }
+ ctx.addPhantom(phantom);
+ ctx.selectedPid = phantom.pid;
+
+ return "Opened project: " + phantom.name + " (pid: " + phantom.pid +
")";
+ }
+
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
index 71977b4963d8..6b4e5cbb395c 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
@@ -19,8 +19,10 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
@@ -46,6 +48,8 @@ class MonitorContext {
volatile String selectedPid;
volatile String lastSelectedName;
+ final List<IntegrationInfo> phantomIntegrations = new
CopyOnWriteArrayList<>();
+ private final AtomicInteger phantomCounter = new AtomicInteger();
int shellPercent;
boolean logPinned;
int logPinPercent;
@@ -106,6 +110,26 @@ class MonitorContext {
return "?";
}
+ void addPhantom(IntegrationInfo info) {
+ info.phantom = true;
+ info.pid = "phantom-" + phantomCounter.incrementAndGet();
+ info.state = 9;
+ phantomIntegrations.add(info);
+ }
+
+ void removePhantom(String pid) {
+ phantomIntegrations.removeIf(i -> pid.equals(i.pid));
+ }
+
+ IntegrationInfo findPhantomByDirectory(String dir) {
+ if (dir == null) {
+ return null;
+ }
+ return phantomIntegrations.stream()
+ .filter(i -> dir.equals(i.sourceDir))
+ .findFirst().orElse(null);
+ }
+
private final ConcurrentHashMap<String, Object> actionLocks = new
ConcurrentHashMap<>();
void fireAction(String pid, JsonObject request) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
index 25cf2559dc42..4aa3a6e975d4 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
@@ -192,47 +192,8 @@ class OverviewTab extends AbstractTab {
}
return true;
}
- // Process control keys
+ // Process control keys (r/x/p/X moved to F10 ProcessControlPopup)
if (actions != null) {
- if (ke.isChar('p') && ctx.selectedPid != null &&
!ctx.isInfraSelected()) {
- IntegrationInfo selInfo = ctx.findSelectedIntegration();
- if (selInfo != null) {
- String cmd = selInfo.routeStarted > 0 ? "stop" : "start";
- if (ctx.confirmActions) {
- String label = selInfo.routeStarted > 0 ? "Stop" :
"Start";
- actions.showConfirm("Confirm " + label + " Routes",
- " " + label + " all routes for " +
ctx.selectedName() + "? ",
- () ->
actions.sendRouteCommand(ctx.selectedPid, "*", cmd));
- } else {
- actions.sendRouteCommand(ctx.selectedPid, "*", cmd);
- }
- }
- return true;
- }
- if (ke.isChar('x') && ctx.selectedPid != null) {
- if (ctx.confirmActions) {
- actions.showConfirm("Confirm Stop",
- " Stop " + ctx.selectedName() + " (PID: " +
ctx.selectedPid + ")? ",
- () -> actions.stopSelectedProcess(false));
- } else {
- actions.stopSelectedProcess(false);
- }
- return true;
- }
- if (ke.isChar('X') && ctx.selectedPid != null) {
- actions.showKillConfirm();
- return true;
- }
- if (ke.isChar('r') && ctx.selectedPid != null &&
!ctx.isInfraSelected()) {
- if (ctx.confirmActions) {
- actions.showConfirm("Confirm Restart",
- " Restart " + ctx.selectedName() + " (PID: " +
ctx.selectedPid + ")? ",
- () -> actions.restartSelectedProcess());
- } else {
- actions.restartSelectedProcess();
- }
- return true;
- }
if (ke.isChar('f') && ctx.selectedPid != null &&
!ctx.isInfraSelected()) {
actions.openFilesPopup();
return true;
@@ -467,7 +428,22 @@ class OverviewTab extends AbstractTab {
boolean isEven = (rowIndex++ % 2 == 0);
Style rowBg = isEven ? Style.EMPTY.bg(Theme.zebra()) :
Style.EMPTY;
- if (info.vanishing) {
+ if (info.phantom) {
+ String platformIcon = TuiIcons.runtimeIcon(info.platform
!= null ? info.platform : "");
+ String nameText = platformIcon + " " + (info.name != null
? info.name : "");
+ rows.add(Row.from(
+ Cell.from(Span.styled("-", Theme.muted())),
+ Cell.from(Span.styled(nameText, Theme.info())),
+ Cell.from(Span.styled("", Theme.muted())),
+ Cell.from(Span.styled("", Theme.muted())),
+ Cell.from(Span.styled(TuiIcons.STOPPED + "
Stopped", Theme.error())),
+ Cell.from(Span.styled("", Theme.muted())),
+ Cell.from(Span.styled("", Theme.muted())),
+ Cell.from(Span.styled("", Theme.muted())),
+ Cell.from(Span.styled("", Theme.muted())),
+ Cell.from(Span.styled("", Theme.muted())),
+ Cell.from(Span.styled("",
Theme.muted()))).style(rowBg));
+ } else if (info.vanishing) {
long elapsed = System.currentTimeMillis() -
info.vanishStart;
float fade = 1.0f - Math.min(1.0f, (float) elapsed /
VANISH_DURATION_MS);
int gray = (int) (100 * fade);
@@ -1074,6 +1050,9 @@ class OverviewTab extends AbstractTab {
default -> "[off]";
});
}
+ if (ctx.selectedPid != null) {
+ hint(spans, "F10", "control");
+ }
}
@Override
@@ -1398,15 +1377,11 @@ class OverviewTab extends AbstractTab {
## Process Control
- - `p` β stop or start all routes for the selected integration
- - `x` β stop the selected integration (graceful shutdown)
- - `X` β kill the selected integration (force terminate, always
confirms)
- - `r` β restart the selected integration
+ - `F10` β open process control popup (stop routes, start
routes, restart, stop, kill)
- `q` β quit the TUI
- By default, these actions show a confirmation dialog before
executing.
+ By default, stop/restart actions show a confirmation dialog
before executing.
You can turn this off in Settings (`F2` β `Settings...` β
`Confirm`).
- Kill (`X`) always confirms regardless of this setting.
""";
}
@@ -1473,12 +1448,13 @@ class OverviewTab extends AbstractTab {
Span.styled("Run an Example", Style.EMPTY.bold()),
Span.raw("."))));
lines.add(Line.from(Span.raw("")));
- lines.add(Line.from(Span.styled(TuiIcons.indent(TuiIcons.FOLDER) + "Or
run an existing project:", Style.EMPTY.bold())));
+ lines.add(
+ Line.from(Span.styled(TuiIcons.indent(TuiIcons.FOLDER) + "Or
open an existing project:", Style.EMPTY.bold())));
lines.add(Line.from(List.of(
Span.raw(" Press "),
Span.styled(" F2 ", Theme.hintKey()),
Span.raw(" to open Actions and select "),
- Span.styled("Run from Folder", Style.EMPTY.bold()),
+ Span.styled("Open", Style.EMPTY.bold()),
Span.raw("."))));
lines.add(Line.from(Span.raw("")));
lines.add(Line.from(Span.styled(TuiIcons.indent(TuiIcons.COMPUTER) +
"Or use the embedded JLine shell panel:",
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessControlPopup.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessControlPopup.java
new file mode 100644
index 000000000000..f89172746ddf
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessControlPopup.java
@@ -0,0 +1,213 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import dev.tamboui.layout.Rect;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Span;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.widgets.Clear;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Borders;
+import dev.tamboui.widgets.list.ListItem;
+import dev.tamboui.widgets.list.ListState;
+import dev.tamboui.widgets.list.ListWidget;
+import dev.tamboui.widgets.list.ScrollMode;
+
+import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.hint;
+import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.hintLast;
+
+class ProcessControlPopup {
+
+ enum ControlAction {
+ RUN(TuiIcons.CAMEL, "Run"),
+ CLOSE_PROJECT(TuiIcons.FAIL, "Close Project"),
+ STOP_ROUTES(TuiIcons.SLEEP, "Stop Routes"),
+ START_ROUTES(TuiIcons.CAMEL, "Start Routes"),
+ RESTART(TuiIcons.RESET, "Restart"),
+ STOP(TuiIcons.STOP, "Stop"),
+ KILL(TuiIcons.FAIL, "Kill"),
+ STOP_ALL(TuiIcons.STOP, "Stop All");
+
+ final String icon;
+ final String label;
+
+ ControlAction(String icon, String label) {
+ this.icon = icon;
+ this.label = label;
+ }
+ }
+
+ interface ControlActions {
+ void sendRouteCommand(String pid, String routeId, String command);
+
+ void stopSelectedProcess(boolean forceKill);
+
+ void restartSelectedProcess();
+
+ void onRunPhantom(IntegrationInfo phantom);
+
+ void onStopAll();
+
+ boolean hasRunningProcesses();
+ }
+
+ private final MonitorContext ctx;
+ private ControlActions actions;
+
+ private boolean visible;
+ private final List<ControlAction> currentOptions = new ArrayList<>();
+ private final ListState listState = new ListState();
+
+ ProcessControlPopup(MonitorContext ctx) {
+ this.ctx = ctx;
+ }
+
+ void setActions(ControlActions actions) {
+ this.actions = actions;
+ }
+
+ boolean isVisible() {
+ return visible;
+ }
+
+ void open() {
+ currentOptions.clear();
+ listState.select(0);
+
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ if (info != null) {
+ if (info.phantom) {
+ currentOptions.add(ControlAction.RUN);
+ currentOptions.add(ControlAction.CLOSE_PROJECT);
+ } else {
+ if (info.routeStarted > 0) {
+ currentOptions.add(ControlAction.STOP_ROUTES);
+ } else if (info.routeTotal > 0) {
+ currentOptions.add(ControlAction.START_ROUTES);
+ }
+ currentOptions.add(ControlAction.RESTART);
+ currentOptions.add(ControlAction.STOP);
+ currentOptions.add(ControlAction.KILL);
+ }
+ } else if (ctx.findSelectedInfra() != null) {
+ currentOptions.add(ControlAction.STOP);
+ currentOptions.add(ControlAction.KILL);
+ }
+
+ if (actions != null && actions.hasRunningProcesses()) {
+ currentOptions.add(ControlAction.STOP_ALL);
+ }
+
+ if (!currentOptions.isEmpty()) {
+ visible = true;
+ }
+ }
+
+ void close() {
+ visible = false;
+ }
+
+ boolean handleKeyEvent(KeyEvent ke) {
+ if (!visible) {
+ return false;
+ }
+ if (ke.isCancel()) {
+ visible = false;
+ } else if (ke.isUp()) {
+ listState.selectPrevious();
+ } else if (ke.isDown()) {
+ listState.selectNext(currentOptions.size());
+ } else if (ke.isConfirm()) {
+ Integer sel = listState.selected();
+ if (sel != null && sel < currentOptions.size()) {
+ visible = false;
+ executeAction(currentOptions.get(sel));
+ }
+ }
+ return true;
+ }
+
+ void render(Frame frame, Rect area) {
+ if (!visible || currentOptions.isEmpty()) {
+ return;
+ }
+
+ int popupW = 32;
+ int popupH = currentOptions.size() + 2;
+ int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
+ int y = area.top() + 2;
+ Rect popup = new Rect(x, y, Math.min(popupW, area.width()),
Math.min(popupH, area.height()));
+
+ frame.renderWidget(Clear.INSTANCE, popup);
+
+ ListItem[] items = new ListItem[currentOptions.size()];
+ for (int i = 0; i < currentOptions.size(); i++) {
+ ControlAction action = currentOptions.get(i);
+ items[i] = ListItem.from(TuiIcons.menuItem(action.icon,
action.label));
+ }
+
+ ListWidget list = ListWidget.builder()
+ .items(items)
+ .highlightStyle(Theme.selectionBg())
+ .highlightSymbol("")
+ .scrollMode(ScrollMode.NONE)
+ .block(Block.builder()
+ .borderType(BorderType.ROUNDED)
+ .borders(Borders.ALL)
+ .title(" Control ")
+ .build())
+ .build();
+ frame.renderStatefulWidget(list, popup, listState);
+ }
+
+ void renderFooter(List<Span> spans) {
+ hint(spans, "Enter", "select");
+ hintLast(spans, "Esc", "cancel");
+ }
+
+ private void executeAction(ControlAction action) {
+ if (actions == null) {
+ return;
+ }
+ switch (action) {
+ case RUN -> {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ if (info != null && info.phantom) {
+ actions.onRunPhantom(info);
+ }
+ }
+ case CLOSE_PROJECT -> {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ if (info != null && info.phantom) {
+ ctx.removePhantom(info.pid);
+ ctx.selectedPid = null;
+ }
+ }
+ case STOP_ROUTES -> actions.sendRouteCommand(ctx.selectedPid, "*",
"stop");
+ case START_ROUTES -> actions.sendRouteCommand(ctx.selectedPid,
"*", "start");
+ case RESTART -> actions.restartSelectedProcess();
+ case STOP -> actions.stopSelectedProcess(false);
+ case KILL -> actions.stopSelectedProcess(true);
+ case STOP_ALL -> actions.onStopAll();
+ }
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
index 0cf36c3a9756..d72748be9217 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
@@ -210,6 +210,7 @@ class SourceTab extends AbstractTab {
int clicked = listState.offset() + (me.y() - innerTop);
if (clicked >= 0 && clicked < entries.size()) {
listState.select(clicked);
+ openSelectedEntry();
}
return true;
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
index 74595c96b69a..e8716ba2d65f 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
@@ -55,6 +55,7 @@ final class TuiIcons {
static final String GO_TO = "π";
static final String MESSAGE = "π©";
static final String KEYSTROKES = "π€";
+ static final String SLEEP = "π€";
static final String STOP = "π";
static final String RECORD = "π΄";
static final String DOCTOR = "π©Ί";
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
index 043e21ef9de6..2391bdd28391 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
@@ -127,6 +127,7 @@ class TuiToolRegistry {
case "tui_toggle_trace_display" -> callToggleTraceDisplay(args);
case "tui_get_readme" -> callGetReadme(args);
case "tui_control" -> callControl(args);
+ case "tui_open_project" -> callOpenProject(args);
case "tui_get_files" -> callGetFiles(args);
case "tui_get_spans" -> callGetSpans(args);
case "tui_locate" -> callLocate(args);
@@ -518,10 +519,21 @@ class TuiToolRegistry {
+ "start-routes (or resume) β resume all
routes; "
+ "restart β gracefully restart the
integration; "
+ "stop β gracefully stop the process; "
- + "kill β forcefully terminate the process.",
+ + "kill β forcefully terminate the process; "
+ + "stop-all β stop all running processes; "
+ + "close β close a phantom (opened but not
running) project.",
Map.of("action", propDef("string",
- "Control action: stop-routes, start-routes, pause,
resume, restart, stop, or kill")),
+ "Control action: stop-routes, start-routes, pause,
resume, restart, stop, kill, stop-all, or close")),
List.of("action"))));
+ tools.add(toToolDef(toolDef(
+ "tui_open_project",
+ "Opens a project directory as a phantom integration (shown as
Stopped in Overview). "
+ + "The project can then be browsed in the
Source tab and run via tui_control. "
+ + "Supports Maven projects (Spring Boot,
Quarkus, Camel Main detected via pom.xml) "
+ + "and flat directories with Camel route
files.",
+ Map.of("directory", propDef("string",
+ "Absolute path to the project directory to open")),
+ List.of("directory"))));
tools.add(toToolDef(toolDef(
"tui_get_files",
"Returns source files from the selected integration's
directory. "
@@ -1541,6 +1553,14 @@ class TuiToolRegistry {
return facade.controlIntegration(action);
}
+ private String callOpenProject(Map<String, Object> args) {
+ String directory = (String) args.get("directory");
+ if (directory == null || directory.isBlank()) {
+ return "Error: directory is required";
+ }
+ return facade.openProject(directory);
+ }
+
private String callGetFiles(Map<String, Object> args) {
String name = args.get("name") instanceof String s ? s : null;
String file = args.get("file") instanceof String s ? s : null;