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 64f458f82ac0 chore: camel-jbang - examples from the TUI run in their 
own directory; the Overview keeps its integrations visible (#27202)
64f458f82ac0 is described below

commit 64f458f82ac081b34f812bd74d5f03183e45637b
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Oct 1 15:12:11 2026 +0200

    chore: camel-jbang - examples from the TUI run in their own directory; the 
Overview keeps its integrations visible (#27202)
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../core/commands/tui/ExampleBrowserPopup.java     | 23 +++----
 .../dsl/jbang/core/commands/tui/LaunchManager.java | 75 +++++++++++++++++++++-
 .../dsl/jbang/core/commands/tui/OverviewTab.java   | 10 ++-
 .../commands/tui/LaunchManagerExampleTest.java     | 63 ++++++++++++++++++
 .../core/commands/tui/OverviewTabRenderTest.java   | 31 +++++++++
 5 files changed, 183 insertions(+), 19 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ExampleBrowserPopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ExampleBrowserPopup.java
index f8f75a1f28ef..0ef2bf139cfe 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ExampleBrowserPopup.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ExampleBrowserPopup.java
@@ -16,7 +16,6 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.tui;
 
-import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -41,7 +40,6 @@ import dev.tamboui.widgets.list.ListState;
 import dev.tamboui.widgets.list.ListWidget;
 import dev.tamboui.widgets.list.ScrollMode;
 import org.apache.camel.dsl.jbang.core.common.ExampleHelper;
-import org.apache.camel.dsl.jbang.core.common.LauncherHelper;
 import org.apache.camel.util.json.JsonObject;
 
 class ExampleBrowserPopup {
@@ -286,21 +284,16 @@ class ExampleBrowserPopup {
 
     void doLaunch(String exampleName, String displayName, List<String> 
extraArgs) {
         try {
-            List<String> cmd = new 
ArrayList<>(LauncherHelper.getCamelCommand());
-            cmd.add("run");
-            cmd.add("--example=" + exampleName);
-            cmd.add("--logging-color=true");
-            cmd.addAll(extraArgs);
+            List<String> args = new ArrayList<>();
+            args.add("run");
+            args.add("--example=" + exampleName);
+            args.add("--logging-color=true");
+            args.addAll(extraArgs);
             if (exampleName.contains("/") && extraArgs.stream().noneMatch(a -> 
a.startsWith("--name"))) {
-                cmd.add("--name=" + TuiHelper.stripCategory(exampleName));
+                args.add("--name=" + TuiHelper.stripCategory(exampleName));
             }
-            Path outputFile = 
LaunchManager.createSecureTempFile("camel-example-", ".log");
-            outputFile.toFile().deleteOnExit();
-            ProcessBuilder pb = new ProcessBuilder(cmd);
-            pb.redirectErrorStream(true);
-            pb.redirectOutput(outputFile.toFile());
-            Process process = pb.start();
-            launchManager.addPendingLaunch(displayName, process, outputFile);
+            // the launch manager runs it in a directory of its own, with the 
example's files
+            launchManager.launchDetached(displayName, args);
             if (burstCallback != null) {
                 burstCallback.run();
             }
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 b3566400313a..c169ad862787 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
@@ -24,7 +24,9 @@ import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Queue;
 import java.util.Set;
+import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.TimeUnit;
 import java.util.function.BiConsumer;
 import java.util.function.Supplier;
@@ -39,7 +41,8 @@ class LaunchManager {
     private static volatile Path secureTempDir;
 
     private final Supplier<List<InfraInfo>> infraServices;
-    private final List<PendingLaunch> pendingLaunches = new ArrayList<>();
+    // added from the UI thread and from tool threads (MCP, the example 
launcher), read on the UI thread
+    private final Queue<PendingLaunch> pendingLaunches = new 
ConcurrentLinkedQueue<>();
     private DeferredLaunch deferredLaunch;
     private volatile String pendingAutoSelect;
     private BiConsumer<String, Boolean> notificationCallback;
@@ -76,17 +79,85 @@ class LaunchManager {
      * file. Used by the AI panel's {@code /run} and {@code /infra run} slash 
commands.
      */
     void launchDetached(String displayName, List<String> extraArgs) throws 
IOException {
+        JsonObject example = exampleOf(extraArgs);
+        if (example == null) {
+            start(displayName, extraArgs, null);
+            return;
+        }
+        // the example runs in a directory of its own, with its files: its 
routes read relative to it (orders, inbox),
+        // where camel run --example would run in the directory of the TUI; a 
GitHub example is downloaded first
+        Thread t = new Thread(() -> {
+            try {
+                Path dir = ExampleHelper.isBundled(example)
+                        ? ExampleHelper.extractBundledExample(example) : 
ExampleHelper.downloadGithubExample(example);
+                start(displayName, exampleArgs(extraArgs, example), dir);
+            } catch (Exception e) {
+                notify("Failed to start: " + displayName + " - " + 
e.getMessage(), true);
+            }
+        }, "CamelTuiExampleLaunch");
+        t.setDaemon(true);
+        t.start();
+    }
+
+    private void start(String displayName, List<String> args, Path dir) throws 
IOException {
         List<String> cmd = new ArrayList<>(LauncherHelper.getCamelCommand());
-        cmd.addAll(extraArgs);
+        cmd.addAll(args);
         Path outputFile = createSecureTempFile("camel-launch-", ".log");
         outputFile.toFile().deleteOnExit();
         ProcessBuilder pb = new ProcessBuilder(cmd);
+        if (dir != null) {
+            pb.directory(dir.toFile());
+        }
         pb.redirectErrorStream(true);
         pb.redirectOutput(outputFile.toFile());
         Process process = pb.start();
         addPendingLaunch(displayName, process, outputFile);
     }
 
+    /** The catalog entry of the example a camel run with --example=name runs, 
when it lists its files; else null. */
+    static JsonObject exampleOf(List<String> args) {
+        if (args.isEmpty() || !"run".equals(args.get(0))) {
+            return null;
+        }
+        String name = null;
+        for (String a : args) {
+            if (a.startsWith("--example=")) {
+                name = a.substring("--example=".length());
+            }
+        }
+        if (name == null || name.isBlank()) {
+            return null;
+        }
+        try {
+            List<JsonObject> catalog = ExampleHelper.loadCatalog();
+            JsonObject entry = ExampleHelper.findExample(catalog, name);
+            if (entry == null && !name.contains("/")) {
+                List<JsonObject> same = 
ExampleHelper.findExamplesByShortName(catalog, name);
+                entry = same.size() == 1 ? same.get(0) : null;
+            }
+            return entry != null && !ExampleHelper.getFiles(entry).isEmpty() ? 
entry : null;
+        } catch (RuntimeException e) {
+            return null;
+        }
+    }
+
+    /** The arguments with --example=name replaced by the files of the 
example, and its name unless one is given. */
+    static List<String> exampleArgs(List<String> args, JsonObject example) {
+        List<String> answer = new ArrayList<>();
+        boolean named = args.stream().anyMatch(a -> a.startsWith("--name"));
+        for (String a : args) {
+            if (a.startsWith("--example=")) {
+                answer.addAll(ExampleHelper.getFiles(example));
+                if (!named) {
+                    answer.add("--name=" + 
TuiHelper.stripCategory(example.getString("name")));
+                }
+            } else {
+                answer.add(a);
+            }
+        }
+        return answer;
+    }
+
     void addPendingLaunch(String name, Process process, Path outputFile) {
         pendingLaunches.add(new PendingLaunch(name, process, outputFile, 
System.currentTimeMillis()));
         pendingAutoSelect = name;
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 f5592f99e7b4..da3a8ce5d197 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
@@ -293,13 +293,19 @@ class OverviewTab extends AbstractTab {
                 && !throughputHistory.isEmpty() && ctx.shellPercent < 50;
         List<Constraint> constraints = new ArrayList<>();
         constraints.add(Constraint.fill());
+        int infraPanelHeight = 0;
         if (infraCount > 0) {
-            int infraPanelHeight = Math.max(6, Math.min(infraCount + 3, 
area.height() / 3));
+            infraPanelHeight = Math.max(6, Math.min(infraCount + 3, 
area.height() / 3));
             constraints.add(Constraint.length(infraPanelHeight));
         }
+        // the integrations come first: the borders, the header and a few 
rows, before the chart takes its height
+        int tableRoom = Math.max(1, area.height() - infraPanelHeight - 
Math.min(integrationCount + 3, 7));
+        if (hasSparkline && tableRoom < 5) {
+            hasSparkline = false;
+        }
         if (hasSparkline || showInfraDetail) {
             bottomPanelHeight = Math.max(5, Math.min(bottomPanelHeight, 
area.height() - 5));
-            constraints.add(Constraint.length(bottomPanelHeight));
+            constraints.add(Constraint.length(Math.max(Math.min(5, tableRoom), 
Math.min(bottomPanelHeight, tableRoom))));
         }
         List<Rect> chunks = Layout.vertical()
                 .constraints(constraints)
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManagerExampleTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManagerExampleTest.java
new file mode 100644
index 000000000000..d21f992d06e8
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManagerExampleTest.java
@@ -0,0 +1,63 @@
+/*
+ * 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.List;
+
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * An example launched from the TUI runs in a directory of its own, with its 
files, so its routes find what they read
+ * relative to it.
+ */
+class LaunchManagerExampleTest {
+
+    @Test
+    void theExampleOfARunIsFoundByItsName() {
+        JsonObject example = LaunchManager.exampleOf(List.of("run", 
"--example=route/content-based-router"));
+        assertThat(example).isNotNull();
+        
assertThat(example.getString("name")).isEqualTo("route/content-based-router");
+        // by its short name too, when that is unique
+        assertThat(LaunchManager.exampleOf(List.of("run", 
"--example=content-based-router"))).isNotNull();
+    }
+
+    @Test
+    void otherCommandsAreNoExample() {
+        assertThat(LaunchManager.exampleOf(List.of("run", 
"MyRoute.java"))).isNull();
+        assertThat(LaunchManager.exampleOf(List.of("infra", "run", 
"kafka"))).isNull();
+        assertThat(LaunchManager.exampleOf(List.of("run", 
"--example=does-not-exist"))).isNull();
+    }
+
+    @Test
+    void theExampleArgumentBecomesItsFilesAndName() {
+        List<String> args = List.of("run", 
"--example=route/content-based-router", "--logging-color=true");
+        JsonObject example = LaunchManager.exampleOf(args);
+
+        List<String> answer = LaunchManager.exampleArgs(args, example);
+
+        assertThat(answer).startsWith("run").contains("--logging-color=true", 
"--name=content-based-router")
+                .doesNotContain("--example=route/content-based-router")
+                .anyMatch(a -> a.endsWith(".yaml") || a.endsWith(".java") || 
a.endsWith(".xml"))
+                .anyMatch(a -> a.startsWith("orders/"));
+        // a name given stays the only one
+        assertThat(LaunchManager.exampleArgs(List.of("run", 
"--example=route/content-based-router", "--name=cbr"), example))
+                
.contains("--name=cbr").doesNotContain("--name=content-based-router");
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTabRenderTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTabRenderTest.java
index 894ec99f0f32..57cc57dadf15 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTabRenderTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTabRenderTest.java
@@ -19,6 +19,7 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.HashSet;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicReference;
 
@@ -304,6 +305,36 @@ class OverviewTabRenderTest {
         assertTrue(rendered.contains("SERVICE"), "Infra panel should have 
SERVICE header");
     }
 
+    @Test
+    void theIntegrationsKeepTheirRowsWhenTheInfraAndChartPanelsDoNotFit() {
+        List<IntegrationInfo> infos = new ArrayList<>();
+        for (String name : List.of("orders", "billing", "shipping")) {
+            IntegrationInfo i = new IntegrationInfo();
+            i.pid = String.valueOf(1000 + infos.size());
+            i.name = name;
+            i.state = 5;
+            infos.add(i);
+        }
+        InfraInfo infra = new InfraInfo();
+        infra.pid = "9999";
+        infra.alias = "kafka";
+        infra.alive = true;
+        MonitorContext ctx2 = new MonitorContext(new AtomicReference<>(infos), 
new AtomicReference<>(List.of(infra)));
+        ctx2.selectedPid = "1000";
+        MetricsCollector metrics = new MetricsCollector();
+        metrics.getThroughputHistory().put("1000", new 
LinkedList<>(List.of(1L, 2L, 3L)));
+
+        OverviewTab tab = new OverviewTab(ctx2, metrics, new HashSet<>(), () 
-> {
+        });
+        // a short content area: what is left of a 34-row terminal with the 
log pinned
+        String rendered = TuiTestHelper.renderToString(tab, 150, 22);
+
+        assertTrue(rendered.contains("orders"), rendered);
+        assertTrue(rendered.contains("billing"), rendered);
+        assertTrue(rendered.contains("shipping"), rendered);
+        assertTrue(rendered.contains("kafka"), rendered);
+    }
+
     @Test
     void toggleInfraFocusWithKeyI() {
         info.state = 5;

Reply via email to