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;