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 b7b70bc99eb8 CAMEL-23831: Show files in folder browser with emojis and
source viewer in camel-tui
b7b70bc99eb8 is described below
commit b7b70bc99eb8966438e4445336ec87bbd6711965
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Jul 4 00:00:40 2026 +0200
CAMEL-23831: Show files in folder browser with emojis and source viewer in
camel-tui
CAMEL-23831: Detect Maven project runtime from pom.xml when running from
folder in camel-tui
Auto-detect Spring Boot, Quarkus, or Camel Main from pom.xml dependency
management entries. Lock the runtime selector in the run dialog and disable
dev mode for Maven projects. Use camel run pom.xml instead of --source-dir
so the export flow adds camel-cli-connector. Also fix camel run pom.xml to
auto-detect runtime before dispatch so --runtime flag is not required.
CAMEL-23831: Run existing Maven projects directly without export in camel
run pom.xml
When camel run targets an existing Maven project (pom.xml), skip the
export flow entirely and run Maven directly from the project directory.
This preserves the original application.properties, main class, and
project structure. A temporary pom (camel-jbang-run-pom.xml) is used
to inject camel-cli-connector, then cleaned up on exit.
Supports Spring Boot (spring-boot:run), Quarkus (quarkus:dev), and
Camel Main (camel:run) projects. Also reverts the hasSpringBootMainClass
workaround in ExportSpringBoot which is no longer needed.
CAMEL-23831: Fix logging config to reach Spring Boot app JVM via
jvmArguments
CAMEL-23831: Skip tests when running existing Maven projects to avoid
phantom processes
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../apache/camel/dsl/jbang/core/commands/Run.java | 281 +++++++++++++++++++++
.../camel/dsl/jbang/core/commands/RunHelper.java | 21 ++
.../dsl/jbang/core/commands/tui/ActionsPopup.java | 36 ++-
.../dsl/jbang/core/commands/tui/FilesBrowser.java | 110 +-------
.../dsl/jbang/core/commands/tui/FolderBrowser.java | 49 +++-
.../jbang/core/commands/tui/RunOptionsForm.java | 42 ++-
.../dsl/jbang/core/commands/tui/TuiHelper.java | 158 ++++++++++++
7 files changed, 573 insertions(+), 124 deletions(-)
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
index 230163bbb11e..383d99d58912 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
@@ -22,8 +22,10 @@ import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.io.Reader;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystems;
@@ -80,6 +82,10 @@ import org.apache.camel.util.json.JsonObject;
import org.apache.camel.util.json.Jsoner;
import org.apache.camel.xml.io.util.XmlStreamDetector;
import org.apache.camel.xml.io.util.XmlStreamInfo;
+import org.apache.maven.model.Dependency;
+import org.apache.maven.model.Model;
+import org.apache.maven.model.io.xpp3.MavenXpp3Reader;
+import org.apache.maven.model.io.xpp3.MavenXpp3Writer;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
@@ -630,11 +636,22 @@ public class Run extends CamelCommand {
}
}
+ // auto-detect runtime from pom.xml before dispatch
+ if (!exportRun && RuntimeType.main == runtime
+ && files != null && files.size() == 1 &&
files.get(0).endsWith("pom.xml")) {
+ RuntimeType detected =
RunHelper.detectRuntimeFromPom(Path.of(files.get(0)).toAbsolutePath());
+ if (detected != null) {
+ runtime = detected;
+ }
+ }
+
if (!exportRun) {
if (RuntimeType.quarkus == runtime) {
return runQuarkus();
} else if (RuntimeType.springBoot == runtime) {
return runSpringBoot();
+ } else if (files != null && files.size() == 1 &&
files.get(0).endsWith("pom.xml")) {
+ return runExistingCamelMainProject();
}
}
@@ -1296,6 +1313,12 @@ public class Run extends CamelCommand {
}
AtomicReference<Process> processRef = new AtomicReference<>();
+
+ // existing Maven project: run directly without export
+ if (files != null && files.size() == 1 &&
files.get(0).endsWith("pom.xml")) {
+ return runExistingQuarkusProject(processRef);
+ }
+
AtomicReference<String> appNameRef = new AtomicReference<>();
// create temp run dir
@@ -1423,6 +1446,169 @@ public class Run extends CamelCommand {
return p.waitFor();
}
+ private int runExistingCamelMainProject() throws Exception {
+ if (background) {
+ printer().printErr("Run Camel Main with --background is not
supported");
+ return 1;
+ }
+
+ AtomicReference<Process> processRef = new AtomicReference<>();
+ Path pomPath = Path.of(files.get(0)).toAbsolutePath();
+ Path projectDir = pomPath.getParent();
+
+ // read the pom.xml and add camel-cli-connector dependency
+ MavenXpp3Reader mavenReader = new MavenXpp3Reader();
+ Model model;
+ try (Reader reader = Files.newBufferedReader(pomPath)) {
+ model = mavenReader.read(reader);
+ }
+
+ boolean hasCliConnector = model.getDependencies().stream()
+ .anyMatch(d ->
"camel-cli-connector".equals(d.getArtifactId()));
+ if (!hasCliConnector) {
+ Dependency d = new Dependency();
+ d.setGroupId("org.apache.camel");
+ d.setArtifactId("camel-cli-connector");
+ model.getDependencies().add(d);
+ }
+
+ // write temporary pom with cli-connector injected
+ Path tempPom = projectDir.resolve("camel-jbang-run-pom.xml");
+ MavenXpp3Writer w = new MavenXpp3Writer();
+ try (FileOutputStream fos = new FileOutputStream(tempPom.toFile())) {
+ w.write(fos, model);
+ }
+
+ // shutdown hook to clean up temp files
+ Runtime.getRuntime().addShutdownHook(new Thread(() -> {
+ try {
+ Process process = processRef.get();
+ if (process != null) {
+ process.destroy();
+ for (int i = 0; i < 30; i++) {
+ if (!process.isAlive()) {
+ break;
+ }
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+ Files.deleteIfExists(tempPom);
+ } catch (Exception e) {
+ // ignore
+ }
+ }));
+
+ // find mvnw or fall back to mvn
+ String mvnw = FileUtil.isWindows() ? "mvnw.cmd" : "mvnw";
+ Path mvnwPath = projectDir.resolve(mvnw);
+ String mvnCmd = Files.isExecutable(mvnwPath) ? mvnwPath.toString() :
"mvn";
+
+ List<String> cmd = new ArrayList<>();
+ cmd.add(mvnCmd);
+ cmd.add("--quiet");
+ cmd.add("--file");
+ cmd.add(tempPom.toString());
+ if (jvmArgs != null && !jvmArgs.isBlank()) {
+ cmd.add("-Dcamel.jvmArgs=" + jvmArgs.trim());
+ }
+ cmd.add("-DskipTests");
+ cmd.add("camel:run");
+
+ printer().println("Running Camel Main project: " + projectDir);
+
+ ProcessBuilder pb = new ProcessBuilder();
+ pb.command(cmd);
+ pb.directory(projectDir.toFile());
+ pb.inheritIO();
+ Process p = pb.start();
+ processRef.set(p);
+ this.spawnPid = p.pid();
+ return p.waitFor();
+ }
+
+ private int runExistingQuarkusProject(AtomicReference<Process> processRef)
throws Exception {
+ Path pomPath = Path.of(files.get(0)).toAbsolutePath();
+ Path projectDir = pomPath.getParent();
+
+ // read the pom.xml and add camel-quarkus-cli-connector dependency
+ MavenXpp3Reader mavenReader = new MavenXpp3Reader();
+ Model model;
+ try (Reader reader = Files.newBufferedReader(pomPath)) {
+ model = mavenReader.read(reader);
+ }
+
+ boolean hasCliConnector = model.getDependencies().stream()
+ .anyMatch(d ->
"camel-quarkus-cli-connector".equals(d.getArtifactId()));
+ if (!hasCliConnector) {
+ Dependency d = new Dependency();
+ d.setGroupId("org.apache.camel.quarkus");
+ d.setArtifactId("camel-quarkus-cli-connector");
+ model.getDependencies().add(d);
+ }
+
+ // write temporary pom with cli-connector injected
+ Path tempPom = projectDir.resolve("camel-jbang-run-pom.xml");
+ MavenXpp3Writer w = new MavenXpp3Writer();
+ try (FileOutputStream fos = new FileOutputStream(tempPom.toFile())) {
+ w.write(fos, model);
+ }
+
+ // shutdown hook to clean up temp files
+ Runtime.getRuntime().addShutdownHook(new Thread(() -> {
+ try {
+ Process process = processRef.get();
+ if (process != null) {
+ process.destroy();
+ for (int i = 0; i < 30; i++) {
+ if (!process.isAlive()) {
+ break;
+ }
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+ Files.deleteIfExists(tempPom);
+ } catch (Exception e) {
+ // ignore
+ }
+ }));
+
+ // find mvnw or fall back to mvn
+ String mvnw = FileUtil.isWindows() ? "mvnw.cmd" : "mvnw";
+ Path mvnwPath = projectDir.resolve(mvnw);
+ String mvnCmd = Files.isExecutable(mvnwPath) ? mvnwPath.toString() :
"mvn";
+
+ List<String> cmd = new ArrayList<>();
+ cmd.add(mvnCmd);
+ cmd.add("--quiet");
+ cmd.add("--file");
+ cmd.add(tempPom.toString());
+ if (jvmArgs != null && !jvmArgs.isBlank()) {
+ cmd.add("-Djvm.args=" + jvmArgs.trim());
+ }
+ cmd.add("-DskipTests");
+ cmd.add("package");
+ cmd.add("quarkus:" + (dev ? "dev" : "run"));
+
+ printer().println("Running Quarkus project: " + projectDir);
+
+ ProcessBuilder pb = new ProcessBuilder();
+ pb.command(cmd);
+ pb.directory(projectDir.toFile());
+ pb.inheritIO();
+ Process p = pb.start();
+ processRef.set(p);
+ this.spawnPid = p.pid();
+ return p.waitFor();
+ }
+
protected int runSpringBoot() throws Exception {
if (background) {
printer().printErr("Run Camel Spring Boot with --background is not
supported");
@@ -1431,6 +1617,11 @@ public class Run extends CamelCommand {
AtomicReference<Process> processRef = new AtomicReference<>();
+ // existing Maven project: run directly without export
+ if (files != null && files.size() == 1 &&
files.get(0).endsWith("pom.xml")) {
+ return runExistingSpringBootProject(processRef);
+ }
+
// create temp run dir
Path runDirPath = Paths.get(RUN_PLATFORM_DIR,
Long.toString(System.currentTimeMillis()));
if (!this.background) {
@@ -1554,6 +1745,96 @@ public class Run extends CamelCommand {
return p.waitFor();
}
+ private int runExistingSpringBootProject(AtomicReference<Process>
processRef) throws Exception {
+ Path pomPath = Path.of(files.get(0)).toAbsolutePath();
+ Path projectDir = pomPath.getParent();
+
+ // read the pom.xml and add camel-cli-connector-starter dependency
+ MavenXpp3Reader mavenReader = new MavenXpp3Reader();
+ Model model;
+ try (Reader reader = Files.newBufferedReader(pomPath)) {
+ model = mavenReader.read(reader);
+ }
+
+ boolean hasCliConnector = model.getDependencies().stream()
+ .anyMatch(d ->
"camel-cli-connector-starter".equals(d.getArtifactId()));
+ if (!hasCliConnector) {
+ Dependency d = new Dependency();
+ d.setGroupId("org.apache.camel.springboot");
+ d.setArtifactId("camel-cli-connector-starter");
+ model.getDependencies().add(d);
+ }
+
+ // write temporary pom with cli-connector injected
+ Path tempPom = projectDir.resolve("camel-jbang-run-pom.xml");
+ MavenXpp3Writer w = new MavenXpp3Writer();
+ try (FileOutputStream fos = new FileOutputStream(tempPom.toFile())) {
+ w.write(fos, model);
+ }
+
+ // copy logback config for logging to file (so TUI can read logs)
+ Path logbackPath =
projectDir.resolve("src/main/resources/logback-camel-jbang.xml");
+ try (InputStream is =
Run.class.getClassLoader().getResourceAsStream("spring-boot-logback.xml")) {
+ if (is != null) {
+ Files.copy(is, logbackPath,
StandardCopyOption.REPLACE_EXISTING);
+ }
+ }
+
+ // shutdown hook to clean up temp files
+ Runtime.getRuntime().addShutdownHook(new Thread(() -> {
+ try {
+ Process process = processRef.get();
+ if (process != null) {
+ process.destroy();
+ for (int i = 0; i < 30; i++) {
+ if (!process.isAlive()) {
+ break;
+ }
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+ Files.deleteIfExists(tempPom);
+ Files.deleteIfExists(logbackPath);
+ } catch (Exception e) {
+ // ignore
+ }
+ }));
+
+ // find mvnw or fall back to mvn
+ String mvnw = FileUtil.isWindows() ? "mvnw.cmd" : "mvnw";
+ Path mvnwPath = projectDir.resolve(mvnw);
+ String mvnCmd = Files.isExecutable(mvnwPath) ? mvnwPath.toString() :
"mvn";
+
+ List<String> cmd = new ArrayList<>();
+ cmd.add(mvnCmd);
+ cmd.add("--quiet");
+ cmd.add("--file");
+ cmd.add(tempPom.toString());
+ String sbJvmArgs =
"-Dlogging.config=classpath:logback-camel-jbang.xml";
+ if (jvmArgs != null && !jvmArgs.isBlank()) {
+ sbJvmArgs += " " + jvmArgs.trim();
+ }
+ cmd.add("-Dspring-boot.run.jvmArguments=" + sbJvmArgs);
+ cmd.add("-DskipTests");
+ cmd.add("package");
+ cmd.add("spring-boot:run");
+
+ printer().println("Running Spring Boot project: " + projectDir);
+
+ ProcessBuilder pb = new ProcessBuilder();
+ pb.command(cmd);
+ pb.directory(projectDir.toFile());
+ pb.inheritIO();
+ Process p = pb.start();
+ processRef.set(p);
+ this.spawnPid = p.pid();
+ return p.waitFor();
+ }
+
private boolean acceptPropertiesFile(String file) {
String name = FileUtil.onlyName(file);
if (profile != null && name.startsWith("application-")) {
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/RunHelper.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/RunHelper.java
index 4198263a5d77..b0089a0a40e3 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/RunHelper.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/RunHelper.java
@@ -32,6 +32,7 @@ import java.util.stream.Stream;
import org.apache.camel.catalog.CamelCatalog;
import org.apache.camel.catalog.DefaultCamelCatalog;
import org.apache.camel.dsl.jbang.core.common.LauncherHelper;
+import org.apache.camel.dsl.jbang.core.common.RuntimeType;
import org.apache.camel.main.download.MavenDependencyDownloader;
import org.apache.camel.tooling.maven.MavenArtifact;
import org.apache.camel.util.FileUtil;
@@ -75,6 +76,26 @@ public final class RunHelper {
return answer;
}
+ public static RuntimeType detectRuntimeFromPom(Path pomPath) {
+ try {
+ Model model = loadMavenModel(pomPath);
+ if (model != null && model.getDependencyManagement() != null) {
+ for (Dependency d :
model.getDependencyManagement().getDependencies()) {
+ String a = d.getArtifactId();
+ if ("camel-spring-boot-bom".equals(a) ||
"spring-boot-dependencies".equals(a)) {
+ return RuntimeType.springBoot;
+ }
+ if ("quarkus-bom".equals(a) ||
"quarkus-camel-bom".equals(a)) {
+ return RuntimeType.quarkus;
+ }
+ }
+ }
+ } catch (Exception e) {
+ // ignore
+ }
+ return null;
+ }
+
public static List<String> scanMavenDependenciesFromPom(Path pomPath)
throws Exception {
Model model = loadMavenModel(pomPath);
if (model != null) {
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 e1f250978d17..aa3107e625a7 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
@@ -167,6 +167,7 @@ class ActionsPopup {
private final List<String> folderHistory = new ArrayList<>();
private int folderHistoryIndex = -1;
private String selectedFolder;
+ private String detectedPomPath;
private final FolderBrowser folderBrowser = new FolderBrowser();
private final McpLogPopup mcpLogPopup = new McpLogPopup();
@@ -1486,7 +1487,22 @@ class ActionsPopup {
showFolderInput = false;
persistLastFolder(folder);
String displayName = dirPath.getFileName().toString();
- runOptionsForm.open(displayName, displayName, false, true);
+
+ Path pomFile = dirPath.resolve("pom.xml");
+ String runtime = Files.isRegularFile(pomFile) ?
TuiHelper.detectPomRuntime(pomFile) : null;
+ if (runtime != null) {
+ detectedPomPath = pomFile.toString();
+ int lockedRuntime = 0;
+ if ("spring-boot".equals(runtime)) {
+ lockedRuntime = 1;
+ } else if ("quarkus".equals(runtime)) {
+ lockedRuntime = 2;
+ }
+ runOptionsForm.open(displayName, displayName, false, true,
lockedRuntime);
+ } else {
+ detectedPomPath = null;
+ runOptionsForm.open(displayName, displayName, false, true);
+ }
}
private static String loadLastFolder() {
@@ -1593,6 +1609,7 @@ class ActionsPopup {
return;
}
String folder = selectedFolder;
+ String pomPath = detectedPomPath;
String displayName = runOptionsForm.name();
if (displayName.isEmpty()) {
displayName = Path.of(folder).getFileName().toString();
@@ -1601,24 +1618,29 @@ class ActionsPopup {
boolean jaegerExport = runOptionsForm.isJaegerExport();
runOptionsForm.close();
selectedFolder = null;
+ detectedPomPath = null;
if (jaegerExport && !isJaegerRunning()) {
if (!isContainerRuntimeAvailable()) {
setNotification("Docker/Podman required for Jaeger. Run Doctor
for details", true);
return;
}
- startMissingInfraAndDeferFolder(folder, displayName, extraArgs);
+ startMissingInfraAndDeferFolder(folder, pomPath, displayName,
extraArgs);
return;
}
- doLaunchFolder(folder, displayName, extraArgs);
+ doLaunchFolder(folder, pomPath, displayName, extraArgs);
}
- private void doLaunchFolder(String folder, String displayName,
List<String> extraArgs) {
+ private void doLaunchFolder(String folder, String pomPath, String
displayName, List<String> extraArgs) {
try {
List<String> cmd = new
ArrayList<>(LauncherHelper.getCamelCommand());
cmd.add("run");
- cmd.add("--source-dir=" + folder);
+ if (pomPath != null) {
+ cmd.add(pomPath);
+ } else {
+ cmd.add("--source-dir=" + folder);
+ }
cmd.add("--logging-color=true");
cmd.addAll(extraArgs);
Path outputFile = Files.createTempFile("camel-folder-", ".log");
@@ -2214,9 +2236,9 @@ class ActionsPopup {
missingInfra, displayName, () -> doLaunchExample(exampleName,
displayName, extraArgs));
}
- private void startMissingInfraAndDeferFolder(String folder, String
displayName, List<String> extraArgs) {
+ private void startMissingInfraAndDeferFolder(String folder, String
pomPath, String displayName, List<String> extraArgs) {
startMissingInfraAndDefer(
- List.of("jaeger"), displayName, () -> doLaunchFolder(folder,
displayName, extraArgs));
+ List.of("jaeger"), displayName, () -> doLaunchFolder(folder,
pomPath, displayName, extraArgs));
}
private void startMissingInfraAndDefer(List<String> missingInfra, String
displayName, Runnable launchAction) {
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 47ecfa8d48d7..443bacf46fb0 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
@@ -17,14 +17,12 @@
package org.apache.camel.dsl.jbang.core.commands.tui;
import java.io.IOException;
-import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
-import java.util.Locale;
import dev.tamboui.layout.Rect;
import dev.tamboui.style.Color;
@@ -48,21 +46,6 @@ class FilesBrowser {
record FileEntry(String emoji, String name, long size, String path,
boolean directory) {
}
- private static final String[] CAMEL_YAML_MARKERS = {
- "- from:", "- route:",
- "- routeTemplate:", "- route-template:",
- "- templatedRoute:", "- templated-route:",
- "- routeConfiguration:", "- route-configuration:",
- "- rest:", "- beans:"
- };
-
- private static final String[] CAMEL_XML_MARKERS = {
- "<route", "<routes", "<routeTemplate", "<routeTemplates",
- "<templatedRoute", "<templatedRoutes",
- "<rest", "<rests", "<routeConfiguration",
- "<beans", "<blueprint", "<camel"
- };
-
private boolean visible;
private String title;
private Path rootDir;
@@ -116,7 +99,7 @@ class FilesBrowser {
if (Files.isDirectory(p) && !name.startsWith(".")) {
dirs.add(new FileEntry("📁", name, -1,
p.toString(), true));
} else if (Files.isRegularFile(p)) {
- String emoji = fileEmoji(p);
+ String emoji = TuiHelper.fileEmoji(p);
long size = 0;
try {
size = Files.size(p);
@@ -342,89 +325,14 @@ class FilesBrowser {
}
static String fileType(Path path) {
- String name = path.getFileName().toString();
- String lower = name.toLowerCase(Locale.ROOT);
- if (lower.endsWith(".kamelet.yaml") || lower.endsWith(".kamelet.yml"))
{
- return "camel";
- }
- if (lower.endsWith(".yaml") || lower.endsWith(".yml")) {
- return isCamelYaml(path) ? "camel" : "other";
- }
- if (lower.endsWith(".xml")) {
- return isCamelXml(path) ? "camel" : "other";
- }
- if (lower.endsWith(".java")) {
- return isCamelJava(path) ? "camel" : "java";
- }
- if (lower.endsWith(".properties") || lower.endsWith(".cfg")) {
- return "config";
- }
- if (lower.startsWith("readme")) {
- return "readme";
- }
- return "other";
- }
-
- private static String fileEmoji(Path path) {
- String name = path.getFileName().toString();
- String lower = name.toLowerCase(Locale.ROOT);
- if (lower.endsWith(".kamelet.yaml") || lower.endsWith(".kamelet.yml"))
{
- return "🐪";
- }
- if (lower.endsWith(".yaml") || lower.endsWith(".yml")) {
- return isCamelYaml(path) ? "🐪" : "📋";
- }
- if (lower.endsWith(".xml")) {
- return isCamelXml(path) ? "🐪" : "📋";
- }
- if (lower.endsWith(".java")) {
- return isCamelJava(path) ? "🐪" : "☕";
- }
- if (lower.endsWith(".properties") || lower.endsWith(".cfg")) {
- return "📄";
- }
- if (lower.startsWith("readme")) {
- return "📖";
- }
- return "📋";
+ String emoji = TuiHelper.fileEmoji(path);
+ return switch (emoji) {
+ case "🐪" -> "camel";
+ case "☕" -> "java";
+ case "📄" -> "config";
+ case "📖" -> "readme";
+ default -> "other";
+ };
}
- private static boolean isCamelYaml(Path path) {
- try {
- String content = Files.readString(path, StandardCharsets.UTF_8);
- for (String marker : CAMEL_YAML_MARKERS) {
- if (content.contains(marker)) {
- return true;
- }
- }
- } catch (IOException e) {
- // ignore
- }
- return false;
- }
-
- private static boolean isCamelXml(Path path) {
- try {
- String content = Files.readString(path, StandardCharsets.UTF_8);
- for (String marker : CAMEL_XML_MARKERS) {
- if (content.contains(marker)) {
- return true;
- }
- }
- } catch (IOException e) {
- // ignore
- }
- return false;
- }
-
- private static boolean isCamelJava(Path path) {
- try {
- String content = Files.readString(path, StandardCharsets.UTF_8);
- return content.contains("RouteBuilder")
- || content.contains("EndpointRouteBuilder");
- } catch (IOException e) {
- // ignore
- }
- return false;
- }
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FolderBrowser.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FolderBrowser.java
index 8322f48b5851..2442186839c0 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FolderBrowser.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FolderBrowser.java
@@ -47,7 +47,7 @@ import dev.tamboui.widgets.list.ScrollMode;
class FolderBrowser {
- record DirEntry(String name, String path) {
+ record DirEntry(String emoji, String name, String path, boolean directory)
{
}
private boolean visible;
@@ -58,6 +58,7 @@ class FolderBrowser {
private Consumer<String> onSelect;
private char lastJumpChar;
private int lastJumpIndex = -1;
+ private final SourceViewer sourceViewer = new SourceViewer();
boolean isVisible() {
return visible;
@@ -94,22 +95,31 @@ class FolderBrowser {
private boolean loadDirectory(Path dir, String selectName) {
List<DirEntry> dirs = new ArrayList<>();
+ List<DirEntry> files = new ArrayList<>();
try (var stream = Files.list(dir)) {
- stream.filter(Files::isDirectory)
- .filter(p -> !p.getFileName().toString().startsWith("."))
- .limit(200)
- .forEach(p -> dirs.add(new
DirEntry(p.getFileName().toString(), p.toString())));
+ stream.filter(p -> !p.getFileName().toString().startsWith("."))
+ .limit(500)
+ .forEach(p -> {
+ String name = p.getFileName().toString();
+ if (Files.isDirectory(p)) {
+ dirs.add(new DirEntry("📁", name, p.toString(),
true));
+ } else {
+ files.add(new DirEntry(TuiHelper.fileEmoji(p),
name, p.toString(), false));
+ }
+ });
} catch (IOException e) {
return false;
}
dirs.sort(Comparator.comparing(DirEntry::name,
String.CASE_INSENSITIVE_ORDER));
+ files.sort(Comparator.comparing(DirEntry::name,
String.CASE_INSENSITIVE_ORDER));
List<DirEntry> found = new ArrayList<>();
Path parent = dir.getParent();
if (parent != null) {
- found.add(new DirEntry("..", parent.toString()));
+ found.add(new DirEntry("📁", "..", parent.toString(), true));
}
found.addAll(dirs);
+ found.addAll(files);
if (found.isEmpty()) {
return false;
@@ -140,6 +150,14 @@ class FolderBrowser {
}
boolean handleKeyEvent(KeyEvent ke) {
+ if (sourceViewer.isVisible()) {
+ if (ke.isCancel()) {
+ sourceViewer.hide();
+ return true;
+ }
+ sourceViewer.handleKeyEvent(ke);
+ return true;
+ }
if (ke.isCancel()) {
visible = false;
return true;
@@ -194,7 +212,9 @@ class FolderBrowser {
Integer sel = listState.selected();
if (sel != null && sel < entries.size()) {
DirEntry entry = entries.get(sel);
- if ("..".equals(entry.name()) && currentDir != null) {
+ if (!entry.directory()) {
+ sourceViewer.loadFile(Path.of(entry.path()));
+ } else if ("..".equals(entry.name()) && currentDir != null) {
navigateBack();
} else {
offsetStack.push(listState.offset());
@@ -262,6 +282,11 @@ class FolderBrowser {
}
void render(Frame frame, Rect area) {
+ if (sourceViewer.isVisible()) {
+ frame.renderWidget(Clear.INSTANCE, area);
+ sourceViewer.render(frame, area);
+ return;
+ }
if (entries.isEmpty()) {
visible = false;
return;
@@ -285,8 +310,9 @@ class FolderBrowser {
ListItem[] items = new ListItem[entries.size()];
for (int i = 0; i < entries.size(); i++) {
DirEntry entry = entries.get(i);
- String label = " 📁 " + entry.name();
- items[i] = ListItem.from(Line.from(Span.styled(label,
Style.EMPTY.fg(Color.CYAN))));
+ String label = " " + entry.emoji() + " " + entry.name();
+ Style style = entry.directory() ? Style.EMPTY.fg(Color.CYAN) :
Style.EMPTY;
+ items[i] = ListItem.from(Line.from(Span.styled(label, style)));
}
ListWidget list = ListWidget.builder()
@@ -304,9 +330,14 @@ class FolderBrowser {
}
void renderFooter(List<Span> spans) {
+ if (sourceViewer.isVisible()) {
+ sourceViewer.renderFooter(spans);
+ return;
+ }
TuiHelper.hint(spans, "↑↓", "navigate");
TuiHelper.hint(spans, "Enter", "open");
TuiHelper.hint(spans, "Tab", "select");
TuiHelper.hintLast(spans, "Esc", "close");
}
+
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
index 44638e58de83..4c93d30b08c1 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
@@ -24,6 +24,7 @@ import dev.tamboui.style.Style;
import dev.tamboui.terminal.Frame;
import dev.tamboui.text.Line;
import dev.tamboui.text.Span;
+import dev.tamboui.text.Text;
import dev.tamboui.tui.event.KeyCode;
import dev.tamboui.tui.event.KeyEvent;
import dev.tamboui.widgets.Clear;
@@ -78,6 +79,7 @@ class RunOptionsForm {
private TextInputState maxInput;
private int maxMode;
private int runtimeMode;
+ private boolean runtimeLocked;
private int profileMode;
// Checkboxes
@@ -105,15 +107,25 @@ class RunOptionsForm {
}
void open(String defaultName, String exampleName, boolean bundled, boolean
dev) {
+ open(defaultName, exampleName, bundled, dev, -1);
+ }
+
+ void open(String defaultName, String exampleName, boolean bundled, boolean
dev, int lockedRuntime) {
nameInput = new TextInputState(defaultName != null ? defaultName : "");
portInput = new TextInputState("");
initHeapInput = new TextInputState("");
maxHeapInput = new TextInputState("");
maxInput = new TextInputState("");
maxMode = 0;
- runtimeMode = 0;
+ if (lockedRuntime >= 0 && lockedRuntime < RUNTIME_LABELS.length) {
+ runtimeMode = lockedRuntime;
+ runtimeLocked = true;
+ } else {
+ runtimeMode = 0;
+ runtimeLocked = false;
+ }
profileMode = 0;
- devMode = dev;
+ devMode = runtimeLocked ? false : dev;
observe = false;
backlogTrace = false;
stubMode = false;
@@ -320,8 +332,8 @@ class RunOptionsForm {
return true;
}
- // Runtime row: Space or Left/Right cycles
- if (selectedRow == ROW_RUNTIME) {
+ // Runtime row: Space or Left/Right cycles (unless locked)
+ if (selectedRow == ROW_RUNTIME && !runtimeLocked) {
if (ke.isChar(' ') || ke.isRight()) {
runtimeMode = (runtimeMode + 1) % RUNTIME_LABELS.length;
return true;
@@ -349,7 +361,10 @@ class RunOptionsForm {
// Checkbox rows: Space toggles
if (ke.isChar(' ') && selectedRow >= ROW_CONSOLE) {
switch (selectedRow) {
- case ROW_DEV -> devMode = !devMode;
+ case ROW_DEV -> {
+ if (!runtimeLocked)
+ devMode = !devMode;
+ }
case ROW_OBSERVE -> observe = !observe;
case ROW_TRACE -> backlogTrace = !backlogTrace;
case ROW_STUB -> stubMode = !stubMode;
@@ -506,7 +521,14 @@ class RunOptionsForm {
rowY++;
renderLabel(frame, innerX, rowY, labelW, "Runtime:", selectedRow ==
ROW_RUNTIME);
- renderCycler(frame, innerX + labelW, rowY, fieldW, RUNTIME_LABELS,
runtimeMode, selectedRow == ROW_RUNTIME);
+ if (runtimeLocked) {
+ String locked = RUNTIME_LABELS[runtimeMode] + " 🔒";
+ frame.renderWidget(
+
Paragraph.builder().text(Text.raw(locked)).style(Style.EMPTY.dim()).build(),
+ new Rect(innerX + labelW, rowY, fieldW, 1));
+ } else {
+ renderCycler(frame, innerX + labelW, rowY, fieldW, RUNTIME_LABELS,
runtimeMode, selectedRow == ROW_RUNTIME);
+ }
rowY++;
renderLabel(frame, innerX, rowY, labelW, "Profile:", selectedRow ==
ROW_PROFILE);
@@ -534,7 +556,13 @@ class RunOptionsForm {
renderCheckbox(frame, innerX, rowY, innerW, "Web console (/q/dev)",
webConsole, selectedRow == ROW_CONSOLE);
rowY++;
- renderCheckbox(frame, innerX, rowY, innerW, "Dev mode (live reload)",
devMode, selectedRow == ROW_DEV);
+ if (runtimeLocked) {
+ frame.renderWidget(Paragraph.from(Line.from(
+ Span.styled(" [ ] Dev mode (not available for Maven
projects)", Style.EMPTY.dim()))),
+ new Rect(innerX, rowY, innerW, 1));
+ } else {
+ renderCheckbox(frame, innerX, rowY, innerW, "Dev mode (live
reload)", devMode, selectedRow == ROW_DEV);
+ }
rowY++;
renderCheckbox(frame, innerX, rowY, innerW, "Observe (health +
metrics)", observe, selectedRow == ROW_OBSERVE);
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
index 914f5f958b7a..4756d1e06bd1 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
@@ -17,6 +17,7 @@
package org.apache.camel.dsl.jbang.core.commands.tui;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
@@ -614,4 +615,161 @@ final class TuiHelper {
}
return type;
}
+
+ // ---- File emoji helpers (shared by FilesBrowser, FolderBrowser) ----
+
+ private static final String[] CAMEL_YAML_MARKERS = {
+ "- from:", "- route:",
+ "- routeTemplate:", "- route-template:",
+ "- templatedRoute:", "- templated-route:",
+ "- routeConfiguration:", "- route-configuration:",
+ "- rest:", "- beans:"
+ };
+
+ private static final String[] CAMEL_XML_MARKERS = {
+ "<route", "<routes", "<routeTemplate", "<routeTemplates",
+ "<templatedRoute", "<templatedRoutes",
+ "<rest", "<rests", "<routeConfiguration",
+ "<beans", "<blueprint", "<camel"
+ };
+
+ static String fileEmoji(Path path) {
+ String name = path.getFileName().toString();
+ String lower = name.toLowerCase(Locale.ROOT);
+ if ("pom.xml".equals(lower)) {
+ return detectPomEmoji(path);
+ }
+ if (lower.endsWith(".kamelet.yaml") || lower.endsWith(".kamelet.yml"))
{
+ return "🐪";
+ }
+ if (lower.endsWith(".yaml") || lower.endsWith(".yml")) {
+ return isCamelYaml(path) ? "🐪" : "📋";
+ }
+ if (lower.endsWith(".xml")) {
+ return isCamelXml(path) ? "🐪" : "📋";
+ }
+ if (lower.endsWith(".java")) {
+ return isCamelJava(path) ? "🐪" : "☕";
+ }
+ if (lower.endsWith(".properties") || lower.endsWith(".cfg")) {
+ return "📄";
+ }
+ if (lower.endsWith(".json")) {
+ return "📋";
+ }
+ if (lower.endsWith(".md") || lower.endsWith(".adoc") ||
lower.endsWith(".txt")
+ || lower.startsWith("readme")) {
+ return "📖";
+ }
+ return "📄";
+ }
+
+ static String fileEmojiByName(String name) {
+ String lower = name.toLowerCase(Locale.ROOT);
+ if (lower.endsWith(".camel.yaml") || lower.endsWith(".camel.yml")
+ || lower.endsWith(".kamelet.yaml") ||
lower.endsWith(".kamelet.yml")) {
+ return "🐪";
+ }
+ if (lower.endsWith(".java")) {
+ return "☕";
+ }
+ if (lower.endsWith(".yaml") || lower.endsWith(".yml")) {
+ return "📋";
+ }
+ if (lower.endsWith(".xml")) {
+ return "📋";
+ }
+ if (lower.endsWith(".properties") || lower.endsWith(".cfg")) {
+ return "📄";
+ }
+ if (lower.endsWith(".json")) {
+ return "📋";
+ }
+ if (lower.endsWith(".md") || lower.endsWith(".adoc") ||
lower.endsWith(".txt")
+ || lower.startsWith("readme")) {
+ return "📖";
+ }
+ return "📄";
+ }
+
+ static String detectPomRuntime(Path pomFile) {
+ try {
+ String content = Files.readString(pomFile, StandardCharsets.UTF_8);
+ if (content.contains("quarkus-maven-plugin") ||
content.contains("quarkus-bom")
+ || content.contains("camel-quarkus")) {
+ return "quarkus";
+ }
+ if (content.contains("spring-boot-maven-plugin") ||
content.contains("spring-boot-starter")
+ || content.contains("camel-spring-boot")) {
+ return "spring-boot";
+ }
+ if (content.contains("camel-bom") ||
content.contains("camel-maven-plugin")
+ || content.contains("camel-main")) {
+ return "main";
+ }
+ } catch (IOException e) {
+ // ignore
+ }
+ return null;
+ }
+
+ private static String detectPomEmoji(Path path) {
+ try {
+ String content = Files.readString(path, StandardCharsets.UTF_8);
+ if (content.contains("quarkus-maven-plugin") ||
content.contains("quarkus-bom")
+ || content.contains("camel-quarkus")) {
+ return "🚀";
+ }
+ if (content.contains("spring-boot-maven-plugin") ||
content.contains("spring-boot-starter")
+ || content.contains("camel-spring-boot")) {
+ return "🍃";
+ }
+ if (content.contains("camel-core") || content.contains("camel-api")
+ || content.contains("org.apache.camel")) {
+ return "🐪";
+ }
+ } catch (IOException e) {
+ // ignore
+ }
+ return "📋";
+ }
+
+ private static boolean isCamelYaml(Path path) {
+ try {
+ String content = Files.readString(path, StandardCharsets.UTF_8);
+ for (String marker : CAMEL_YAML_MARKERS) {
+ if (content.contains(marker)) {
+ return true;
+ }
+ }
+ } catch (IOException e) {
+ // ignore
+ }
+ return false;
+ }
+
+ private static boolean isCamelXml(Path path) {
+ try {
+ String content = Files.readString(path, StandardCharsets.UTF_8);
+ for (String marker : CAMEL_XML_MARKERS) {
+ if (content.contains(marker)) {
+ return true;
+ }
+ }
+ } catch (IOException e) {
+ // ignore
+ }
+ return false;
+ }
+
+ private static boolean isCamelJava(Path path) {
+ try {
+ String content = Files.readString(path, StandardCharsets.UTF_8);
+ return content.contains("RouteBuilder")
+ || content.contains("EndpointRouteBuilder");
+ } catch (IOException e) {
+ // ignore
+ }
+ return false;
+ }
}