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

commit 92f52bb3d6c56c3cdf42f8d6ba907cb0ea353aa3
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Jul 3 17:28:59 2026 +0200

    CAMEL-23831: Add --jvm-args option to camel run and TUI run dialog
    
    - Add --jvm-args CLI option for passing JVM arguments (e.g. -Xms128m 
-Xmx256m)
    - Wire into all three runtimes: JBang (--java-options), Spring Boot
      (-Dspring-boot.run.jvmArguments), Quarkus (-Djvm.args)
    - Spawn separate JVM via JBang when --jvm-args is set for Camel Main
    - Add Init heap and Max heap fields to TUI run dialog with placeholder hints
    - Add keystroke-level input validation (digits + single k/m/g suffix)
    - Add launch-time validation (port range, max >= init heap)
    - Show validation errors inline in the run dialog
    - Color heap chart bars individually based on heap load at each point in 
time
    - Document --jvm-args with examples in run.adoc
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../pages/jbang-commands/camel-jbang-debug.adoc    |   1 +
 .../ROOT/pages/jbang-commands/camel-jbang-dev.adoc |   1 +
 .../ROOT/pages/jbang-commands/camel-jbang-run.adoc |   1 +
 .../ROOT/partials/jbang-commands/examples/run.adoc |  14 ++
 .../META-INF/camel-jbang-commands-metadata.json    |   6 +-
 .../apache/camel/dsl/jbang/core/commands/Run.java  |  40 ++++-
 .../dsl/jbang/core/commands/tui/ActionsPopup.java  |   5 +-
 .../dsl/jbang/core/commands/tui/MemoryTab.java     |  12 +-
 .../jbang/core/commands/tui/RunOptionsForm.java    | 163 +++++++++++++++++++--
 9 files changed, 212 insertions(+), 31 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-debug.adoc 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-debug.adoc
index 56291aa0ad2e..8302578f9113 100644
--- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-debug.adoc
+++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-debug.adoc
@@ -40,6 +40,7 @@ camel debug [options]
 | `--java-version,--java` | Java version (21, 25) | 21 | String
 | `--jfr` | Enables Java Flight Recorder saving recording to disk on exit | 
false | boolean
 | `--jfr-profile` | Java Flight Recorder profile to use (such as default or 
profile) |  | String
+| `--jvm-args` | Additional JVM arguments (e.g. -Xmx256m -Xms128m) |  | String
 | `--jvm-debug` | To enable JVM remote debugging on port 4004 by default. The 
supported values are true to enable the remote debugging, false to disable the 
remote debugging or a number to use a custom port |  | int
 | `--kamelets-version` | Apache Camel Kamelets version (auto-detected from 
classpath if not set) |  | String
 | `--lazy-bean` | Whether to use lazy bean initialization (can help with 
complex classloading issues) | false | boolean
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-dev.adoc 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-dev.adoc
index 271834e0c8c0..7e2fa362fbb3 100644
--- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-dev.adoc
+++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-dev.adoc
@@ -38,6 +38,7 @@ camel dev [options]
 | `--java-version,--java` | Java version (21, 25) | 21 | String
 | `--jfr` | Enables Java Flight Recorder saving recording to disk on exit | 
false | boolean
 | `--jfr-profile` | Java Flight Recorder profile to use (such as default or 
profile) |  | String
+| `--jvm-args` | Additional JVM arguments (e.g. -Xmx256m -Xms128m) |  | String
 | `--jvm-debug` | To enable JVM remote debugging on port 4004 by default. The 
supported values are true to enable the remote debugging, false to disable the 
remote debugging or a number to use a custom port |  | int
 | `--kamelets-version` | Apache Camel Kamelets version (auto-detected from 
classpath if not set) |  | String
 | `--lazy-bean` | Whether to use lazy bean initialization (can help with 
complex classloading issues) | false | boolean
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-run.adoc 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-run.adoc
index 79eba60ebcc2..554c5aab82b5 100644
--- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-run.adoc
+++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-run.adoc
@@ -38,6 +38,7 @@ camel run [options]
 | `--java-version,--java` | Java version (21, 25) | 21 | String
 | `--jfr` | Enables Java Flight Recorder saving recording to disk on exit | 
false | boolean
 | `--jfr-profile` | Java Flight Recorder profile to use (such as default or 
profile) |  | String
+| `--jvm-args` | Additional JVM arguments (e.g. -Xmx256m -Xms128m) |  | String
 | `--jvm-debug` | To enable JVM remote debugging on port 4004 by default. The 
supported values are true to enable the remote debugging, false to disable the 
remote debugging or a number to use a custom port |  | int
 | `--kamelets-version` | Apache Camel Kamelets version (auto-detected from 
classpath if not set) |  | String
 | `--lazy-bean` | Whether to use lazy bean initialization (can help with 
complex classloading issues) | false | boolean
diff --git 
a/docs/user-manual/modules/ROOT/partials/jbang-commands/examples/run.adoc 
b/docs/user-manual/modules/ROOT/partials/jbang-commands/examples/run.adoc
index 9610a6b09f77..e6eb95721105 100644
--- a/docs/user-manual/modules/ROOT/partials/jbang-commands/examples/run.adoc
+++ b/docs/user-manual/modules/ROOT/partials/jbang-commands/examples/run.adoc
@@ -27,3 +27,17 @@ Run with additional dependencies:
 ----
 camel run hello.java --dep=camel-jackson
 ----
+
+Run with custom JVM memory settings (init and max heap):
+
+[source,bash]
+----
+camel run hello.java --jvm-args="-Xms128m -Xmx256m"
+----
+
+Run with max heap only:
+
+[source,bash]
+----
+camel run hello.java --jvm-args="-Xmx512m"
+----
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
 
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
index 48228ac50fbe..d30315094616 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
@@ -6,9 +6,9 @@
     { "name": "cmd", "fullName": "cmd", "description": "Performs commands in 
the running Camel integrations, such as start\/stop route, or change logging 
levels.", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"browse", "fullName": "cmd browse", "description": "Browse pending messages on 
endpoints [...]
     { "name": "completion", "fullName": "completion", "description": "Generate 
completion script for bash\/zsh", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.Complete", "options": [ { "names": 
"-h,--help", "description": "Display the help and sub-commands", "javaType": 
"boolean", "type": "boolean" } ] },
     { "name": "config", "fullName": "config", "description": "Get and set user 
configuration values", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.config.ConfigCommand", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "get", 
"fullName": "config get", "description": "Display user configuration value", 
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.config. [...]
-    { "name": "debug", "fullName": "debug", "description": "Debug local Camel 
integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Debug", 
"options": [ { "names": "--ago", "description": "Use ago instead of yyyy-MM-dd 
HH:mm:ss in timestamp.", "javaType": "boolean", "type": "boolean" }, { "names": 
"--background", "description": "Run in the background", "defaultValue": 
"false", "javaType": "boolean", "type": "boolean" }, { "names": 
"--background-wait", "description": "To  [...]
+    { "name": "debug", "fullName": "debug", "description": "Debug local Camel 
integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Debug", 
"options": [ { "names": "--ago", "description": "Use ago instead of yyyy-MM-dd 
HH:mm:ss in timestamp.", "javaType": "boolean", "type": "boolean" }, { "names": 
"--background", "description": "Run in the background", "defaultValue": 
"false", "javaType": "boolean", "type": "boolean" }, { "names": 
"--background-wait", "description": "To  [...]
     { "name": "dependency", "fullName": "dependency", "description": "Displays 
all Camel dependencies required to run", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.DependencyCommand", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"copy", "fullName": "dependency copy", "description": "Copies all Camel 
dependencies required to run to a specific directory", "sourc [...]
-    { "name": "dev", "fullName": "dev", "description": "Run in dev mode with 
live reload", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Dev", 
"options": [ { "names": "--background", "description": "Run in the background", 
"defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": 
"--background-wait", "description": "To wait for run in background to startup 
successfully, before returning", "defaultValue": "true", "javaType": "boolean", 
"type": "boolean" }, [...]
+    { "name": "dev", "fullName": "dev", "description": "Run in dev mode with 
live reload", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Dev", 
"options": [ { "names": "--background", "description": "Run in the background", 
"defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": 
"--background-wait", "description": "To wait for run in background to startup 
successfully, before returning", "defaultValue": "true", "javaType": "boolean", 
"type": "boolean" }, [...]
     { "name": "dirty", "fullName": "dirty", "description": "Check if there are 
dirty files from previous Camel runs that did not terminate gracefully", 
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.Dirty", 
"options": [ { "names": "--clean", "description": "Clean dirty files which are 
no longer in use", "defaultValue": "false", "javaType": "boolean", "type": 
"boolean" }, { "names": "-h,--help", "description": "Display the help and 
sub-commands", "javaType": "boolean", " [...]
     { "name": "doc", "fullName": "doc", "description": "Shows documentation 
for kamelet, component, and other Camel resources", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.catalog.CatalogDoc", "options": [ { 
"names": "--camel-version", "description": "To use a different Camel version 
than the default version", "javaType": "java.lang.String", "type": "string" }, 
{ "names": "--download", "description": "Whether to allow automatic downloading 
JAR dependencies (over the internet [...]
     { "name": "doctor", "fullName": "doctor", "description": "Checks the 
environment and reports potential issues", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.Doctor", "options": [ { "names": 
"-h,--help", "description": "Display the help and sub-commands", "javaType": 
"boolean", "type": "boolean" } ] },
@@ -26,7 +26,7 @@
     { "name": "plugin", "fullName": "plugin", "description": "Manage plugins 
that add sub-commands to this CLI", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.plugin.PluginCommand", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "add", 
"fullName": "plugin add", "description": "Add new plugin", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.plugin.PluginA [...]
     { "name": "ps", "fullName": "ps", "description": "List running Camel 
integrations", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.process.ListProcess", "options": [ { 
"names": "--json", "description": "Output in JSON Format", "javaType": 
"boolean", "type": "boolean" }, { "names": "--pid", "description": "List only 
pid in the output", "javaType": "boolean", "type": "boolean" }, { "names": 
"--remote", "description": "Break down counters into remote\/total pairs", 
"javaType": [...]
     { "name": "restart", "fullName": "restart", "description": "Restarts 
running Camel integrations (stop + re-launch)", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.process.RestartProcess", "options": [ 
{ "names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ] },
-    { "name": "run", "fullName": "run", "description": "Run as local Camel 
integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Run", 
"options": [ { "names": "--background", "description": "Run in the background", 
"defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": 
"--background-wait", "description": "To wait for run in background to startup 
successfully, before returning", "defaultValue": "true", "javaType": "boolean", 
"type": "boolean" }, { [...]
+    { "name": "run", "fullName": "run", "description": "Run as local Camel 
integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Run", 
"options": [ { "names": "--background", "description": "Run in the background", 
"defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": 
"--background-wait", "description": "To wait for run in background to startup 
successfully, before returning", "defaultValue": "true", "javaType": "boolean", 
"type": "boolean" }, { [...]
     { "name": "sbom", "fullName": "sbom", "description": "Generate a CycloneDX 
or SPDX SBOM for a specific project", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.SBOMGenerator", "options": [ { 
"names": "--build-property", "description": "Maven build properties, ex. 
--build-property=prop1=foo", "javaType": "java.util.List", "type": "array" }, { 
"names": "--camel-spring-boot-version", "description": "Camel version to use 
with Spring Boot", "javaType": "java.lang.String", "type" [...]
     { "name": "script", "fullName": "script", "description": "Run Camel 
integration as shell script for terminal scripting", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.Script", "options": [ { "names": 
"--logging", "description": "Can be used to turn on logging (logs to file in 
<user home>\/.camel directory)", "defaultValue": "false", "javaType": 
"boolean", "type": "boolean" }, { "names": "--logging-level", "description": 
"Logging level (ERROR, WARN, INFO, DEBUG, TRACE)", "d [...]
     { "name": "shell", "fullName": "shell", "description": "Interactive Camel 
CLI shell.", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Shell", 
"options": [ { "names": "-h,--help", "description": "Display the help and 
sub-commands", "javaType": "boolean", "type": "boolean" } ] },
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 b3336b687eb9..230163bbb11e 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
@@ -261,6 +261,10 @@ public class Run extends CamelCommand {
                           "enable the remote debugging, false to disable the 
remote debugging or a number to use a custom port")
     int jvmDebugPort;
 
+    @Option(names = { "--jvm-args" },
+            description = "Additional JVM arguments (e.g. -Xmx256m -Xms128m)")
+    String jvmArgs;
+
     @Option(names = { "--name" }, defaultValue = "CamelJBang", description = 
"The name of the Camel application")
     String name;
 
@@ -558,7 +562,8 @@ public class Run extends CamelCommand {
     }
 
     private boolean isDebugMode() {
-        return jvmDebugPort > 0 || debugOptions.openTelemetryAgent;
+        return jvmDebugPort > 0 || debugOptions.openTelemetryAgent
+                || (jvmArgs != null && !jvmArgs.isBlank());
     }
 
     private void writeSetting(KameletMain main, Properties existing, String 
key, String value) {
@@ -1398,9 +1403,17 @@ public class Run extends CamelCommand {
             mvnw = "/mvnw.cmd";
         }
         ProcessBuilder pb = new ProcessBuilder();
-        pb.command(runDirPath + mvnw, "--quiet", "--file",
-                runDirPath.toRealPath().resolve("pom.xml").toString(), 
"package",
-                "quarkus:" + (dev ? "dev" : "run"));
+        List<String> mvnCmd = new ArrayList<>();
+        mvnCmd.add(runDirPath + mvnw);
+        mvnCmd.add("--quiet");
+        mvnCmd.add("--file");
+        mvnCmd.add(runDirPath.toRealPath().resolve("pom.xml").toString());
+        if (jvmArgs != null && !jvmArgs.isBlank()) {
+            mvnCmd.add("-Djvm.args=" + jvmArgs.trim());
+        }
+        mvnCmd.add("package");
+        mvnCmd.add("quarkus:" + (dev ? "dev" : "run"));
+        pb.command(mvnCmd);
 
         pb.inheritIO(); // run in foreground (with IO so logs are visible)
         Process p = pb.start();
@@ -1522,9 +1535,16 @@ public class Run extends CamelCommand {
         if (FileUtil.isWindows()) {
             mvnw = "/mvnw.cmd";
         }
-        pb.command(runDirPath + mvnw, "--quiet", "--file",
-                runDirPath.toRealPath().resolve("pom.xml").toString(),
-                "spring-boot:run");
+        List<String> mvnCmd = new ArrayList<>();
+        mvnCmd.add(runDirPath + mvnw);
+        mvnCmd.add("--quiet");
+        mvnCmd.add("--file");
+        mvnCmd.add(runDirPath.toRealPath().resolve("pom.xml").toString());
+        if (jvmArgs != null && !jvmArgs.isBlank()) {
+            mvnCmd.add("-Dspring-boot.run.jvmArguments=" + jvmArgs.trim());
+        }
+        mvnCmd.add("spring-boot:run");
+        pb.command(mvnCmd);
 
         pb.inheritIO(); // run in foreground (with IO so logs are visible)
         Process p = pb.start();
@@ -1728,6 +1748,12 @@ public class Run extends CamelCommand {
             jbangArgs.add("--debug=" + jvmDebugPort); // jbang --debug=port
             cmds.removeIf(arg -> arg.startsWith("--jvm-debug"));
         }
+        if (jvmArgs != null && !jvmArgs.isBlank()) {
+            for (String arg : jvmArgs.trim().split("\\s+")) {
+                jbangArgs.add("--java-options=" + arg);
+            }
+            cmds.removeIf(a -> a.startsWith("--jvm-args"));
+        }
         if (debugOptions.openTelemetryAgent) {
             boolean jaegerExport = 
"jaeger".equals(debugOptions.openTelemetryAgentExport);
             
jbangArgs.add("--javaagent=io.opentelemetry.javaagent:opentelemetry-javaagent:RELEASE");
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 6a85b2f3d91d..5f5373b39c45 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
@@ -540,7 +540,10 @@ class ActionsPopup {
                     showExampleBrowser = true;
                 }
             } else if (ke.isConfirm()) {
-                if (selectedFolder != null) {
+                String error = runOptionsForm.validate();
+                if (error != null) {
+                    runOptionsForm.setError(error);
+                } else if (selectedFolder != null) {
                     launchFolder();
                 } else {
                     launchWithName();
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryTab.java
index a3a9acaabfc3..529cfaf9adc3 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryTab.java
@@ -266,9 +266,6 @@ class MemoryTab extends AbstractTab {
             ceiling = observedMax;
         }
 
-        long pct = ceiling > 0 ? info.heapMemUsed * 100 / ceiling : 0;
-        Color barColor = pct >= 80 ? Color.LIGHT_RED : pct >= 60 ? 
Color.YELLOW : Color.GREEN;
-
         String title = String.format(" Heap Usage (%s / %s committed) ", 
formatBytes(info.heapMemUsed), formatBytes(ceiling));
 
         // Render the block border first
@@ -291,11 +288,14 @@ class MemoryTab extends AbstractTab {
             data[dataOffset + (i - startIdx)] = hist.get(i);
         }
 
-        // Render multi-row bar chart into the buffer
+        // Render multi-row bar chart with per-column color based on heap load 
at that point
         Buffer buf = frame.buffer();
-        Style barStyle = Style.EMPTY.fg(barColor);
 
         for (int col = 0; col < chartW; col++) {
+            long colPct = ceiling > 0 ? data[col] * 100 / ceiling : 0;
+            Color colColor = colPct >= 80 ? Color.LIGHT_RED : colPct >= 60 ? 
Color.YELLOW : Color.GREEN;
+            Style colStyle = Style.EMPTY.fg(colColor);
+
             double ratio = (double) data[col] / ceiling;
             // Total eighths this column fills (chartH rows * 8 eighths per 
row)
             double fillEighths = ratio * chartH * 8.0;
@@ -307,7 +307,7 @@ class MemoryTab extends AbstractTab {
                 int x = inner.x() + col;
                 int rowEighths = Math.min(8, Math.max(0, totalEighths - row * 
8));
                 if (rowEighths > 0) {
-                    buf.setString(x, y, BAR_EIGHTHS[rowEighths], barStyle);
+                    buf.setString(x, y, BAR_EIGHTHS[rowEighths], colStyle);
                 }
             }
         }
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 967947065946..77e395313c09 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
@@ -47,18 +47,21 @@ class RunOptionsForm {
     private static final int ROW_RUNTIME = 1;
     private static final int ROW_PROFILE = 2;
     private static final int ROW_PORT = 3;
-    private static final int ROW_MAX = 4;
-    private static final int ROW_CONSOLE = 5;
-    private static final int ROW_DEV = 6;
-    private static final int ROW_OBSERVE = 7;
-    private static final int ROW_TRACE = 8;
-    private static final int ROW_STUB = 9;
-    private static final int ROW_OTEL_AGENT = 10;
-    private static final int ROW_COUNT = 11;
+    private static final int ROW_INIT_HEAP = 4;
+    private static final int ROW_MAX_HEAP = 5;
+    private static final int ROW_MAX = 6;
+    private static final int ROW_CONSOLE = 7;
+    private static final int ROW_DEV = 8;
+    private static final int ROW_OBSERVE = 9;
+    private static final int ROW_TRACE = 10;
+    private static final int ROW_STUB = 11;
+    private static final int ROW_OTEL_AGENT = 12;
+    private static final int ROW_COUNT = 13;
 
     private boolean visible;
     private int page;
     private int selectedRow;
+    private String errorMessage;
 
     private static final String[] MAX_MODES = { "Max seconds:", "Max 
messages:", "Max idle secs:" };
     private static final String[] MAX_FLAGS = { "--max-seconds=", 
"--max-messages=", "--max-idle-seconds=" };
@@ -70,6 +73,8 @@ class RunOptionsForm {
     // Text fields
     private TextInputState nameInput;
     private TextInputState portInput;
+    private TextInputState initHeapInput;
+    private TextInputState maxHeapInput;
     private TextInputState maxInput;
     private int maxMode;
     private int runtimeMode;
@@ -102,6 +107,8 @@ class RunOptionsForm {
     void open(String defaultName, String exampleName, boolean bundled, boolean 
dev) {
         nameInput = new TextInputState(defaultName != null ? defaultName : "");
         portInput = new TextInputState("");
+        initHeapInput = new TextInputState("");
+        maxHeapInput = new TextInputState("");
         maxInput = new TextInputState("");
         maxMode = 0;
         runtimeMode = 0;
@@ -133,6 +140,10 @@ class RunOptionsForm {
         return stubMode;
     }
 
+    void setError(String error) {
+        this.errorMessage = error;
+    }
+
     boolean isJaegerExport() {
         return otelAgent && otelExportTarget == 1;
     }
@@ -204,6 +215,21 @@ class RunOptionsForm {
         if (!port.isEmpty()) {
             args.add("--port=" + port);
         }
+        StringBuilder jvmArgs = new StringBuilder();
+        String initHeap = initHeapInput.text().trim();
+        if (!initHeap.isEmpty()) {
+            jvmArgs.append("-Xms").append(initHeap);
+        }
+        String maxHeap = maxHeapInput.text().trim();
+        if (!maxHeap.isEmpty()) {
+            if (!jvmArgs.isEmpty()) {
+                jvmArgs.append(" ");
+            }
+            jvmArgs.append("-Xmx").append(maxHeap);
+        }
+        if (!jvmArgs.isEmpty()) {
+            args.add("--jvm-args=" + jvmArgs);
+        }
         String maxVal = maxInput.text().trim();
         if (!maxVal.isEmpty() && !"0".equals(maxVal)) {
             args.add(MAX_FLAGS[maxMode] + maxVal);
@@ -247,6 +273,7 @@ class RunOptionsForm {
     // ---- Options page (page 0) ----
 
     private boolean handleOptionsPage(KeyEvent ke) {
+        errorMessage = null;
         if (ke.isUp()) {
             selectedRow = (selectedRow - 1 + ROW_COUNT) % ROW_COUNT;
             return true;
@@ -336,7 +363,11 @@ class RunOptionsForm {
         if (selectedRow <= ROW_MAX && selectedRow != ROW_RUNTIME && 
selectedRow != ROW_PROFILE) {
             TextInputState active = activeInput();
             if (active != null) {
-                handleTextInput(ke, active, selectedRow == ROW_PORT || 
selectedRow == ROW_MAX);
+                if (selectedRow == ROW_INIT_HEAP || selectedRow == 
ROW_MAX_HEAP) {
+                    handleHeapInput(ke, active);
+                } else {
+                    handleTextInput(ke, active, selectedRow == ROW_PORT || 
selectedRow == ROW_MAX);
+                }
             }
             return true;
         }
@@ -444,7 +475,7 @@ class RunOptionsForm {
 
     private void renderOptionsPage(Frame frame, Rect area) {
         int popupW = Math.min(64, area.width() - 4);
-        int popupH = 15;
+        int popupH = errorMessage != null ? 18 : 17;
         int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
         int y = area.top() + Math.max(0, (area.height() - popupH) / 4);
         Rect popup = new Rect(x, y, Math.min(popupW, area.width()), 
Math.min(popupH, area.height()));
@@ -466,7 +497,7 @@ class RunOptionsForm {
 
         int innerX = popup.left() + 2;
         int innerW = popup.width() - 4;
-        int labelW = 16;
+        int labelW = 18;
         int fieldW = innerW - labelW;
         int rowY = popup.top() + 1;
 
@@ -486,6 +517,16 @@ class RunOptionsForm {
         renderTextInput(frame, innerX + labelW, rowY, fieldW, portInput, 
selectedRow == ROW_PORT);
         rowY++;
 
+        renderLabel(frame, innerX, rowY, labelW, "Init heap (-Xms):", 
selectedRow == ROW_INIT_HEAP);
+        renderTextInputWithHint(frame, innerX + labelW, rowY, fieldW, 
initHeapInput, selectedRow == ROW_INIT_HEAP,
+                "e.g. 128m, 1g");
+        rowY++;
+
+        renderLabel(frame, innerX, rowY, labelW, "Max heap (-Xmx):", 
selectedRow == ROW_MAX_HEAP);
+        renderTextInputWithHint(frame, innerX + labelW, rowY, fieldW, 
maxHeapInput, selectedRow == ROW_MAX_HEAP,
+                "e.g. 256m, 2g");
+        rowY++;
+
         renderLabel(frame, innerX, rowY, labelW, MAX_MODES[maxMode], 
selectedRow == ROW_MAX);
         renderTextInput(frame, innerX + labelW, rowY, fieldW, maxInput, 
selectedRow == ROW_MAX);
         rowY++;
@@ -519,6 +560,12 @@ class RunOptionsForm {
                     Span.styled(" ", Style.EMPTY),
                     Span.styled(jaegerLabel, jaegerStyle))), exportArea);
         }
+        if (errorMessage != null) {
+            rowY++;
+            Rect errorArea = new Rect(innerX, rowY, innerW, 1);
+            frame.renderWidget(Paragraph.from(Line.from(
+                    Span.styled("⚠ " + errorMessage, Style.EMPTY.bold()))), 
errorArea);
+        }
     }
 
     private void renderPropertiesPage(Frame frame, Rect area) {
@@ -638,11 +685,89 @@ class RunOptionsForm {
         return switch (selectedRow) {
             case ROW_NAME -> nameInput;
             case ROW_PORT -> portInput;
+            case ROW_INIT_HEAP -> initHeapInput;
+            case ROW_MAX_HEAP -> maxHeapInput;
             case ROW_MAX -> maxInput;
             default -> null;
         };
     }
 
+    private void handleHeapInput(KeyEvent ke, TextInputState active) {
+        if (ke.isDeleteBackward()) {
+            active.deleteBackward();
+        } else if (ke.isDeleteForward()) {
+            active.deleteForward();
+        } else if (ke.isLeft()) {
+            active.moveCursorLeft();
+        } else if (ke.isRight()) {
+            active.moveCursorRight();
+        } else if (ke.isHome()) {
+            active.moveCursorToStart();
+        } else if (ke.isEnd()) {
+            active.moveCursorToEnd();
+        } else if (ke.code() == KeyCode.CHAR) {
+            char c = ke.string().charAt(0);
+            String text = active.text();
+            if (Character.isDigit(c)) {
+                // only allow digits before any suffix
+                if (text.isEmpty() || 
Character.isDigit(text.charAt(text.length() - 1))) {
+                    active.insert(c);
+                }
+            } else if ((c == 'k' || c == 'm' || c == 'g') && !text.isEmpty()
+                    && Character.isDigit(text.charAt(text.length() - 1))) {
+                active.insert(c);
+            }
+        }
+    }
+
+    String validate() {
+        String port = portInput.text().trim();
+        if (!port.isEmpty()) {
+            try {
+                int p = Integer.parseInt(port);
+                if (p < 0 || p > 65535) {
+                    return "Port must be 0-65535";
+                }
+            } catch (NumberFormatException e) {
+                return "Invalid port: " + port;
+            }
+        }
+        String initHeap = initHeapInput.text().trim();
+        String maxHeap = maxHeapInput.text().trim();
+        if (!initHeap.isEmpty() && !isValidHeap(initHeap)) {
+            return "Invalid init heap: " + initHeap;
+        }
+        if (!maxHeap.isEmpty() && !isValidHeap(maxHeap)) {
+            return "Invalid max heap: " + maxHeap;
+        }
+        if (!initHeap.isEmpty() && !maxHeap.isEmpty()) {
+            long initBytes = parseHeapBytes(initHeap);
+            long maxBytes = parseHeapBytes(maxHeap);
+            if (initBytes > maxBytes) {
+                return "Init heap cannot exceed max heap";
+            }
+        }
+        return null;
+    }
+
+    private static boolean isValidHeap(String value) {
+        return value.matches("\\d+[kmg]?");
+    }
+
+    private static long parseHeapBytes(String value) {
+        char last = value.charAt(value.length() - 1);
+        if (Character.isDigit(last)) {
+            return Long.parseLong(value);
+        }
+        long num = Long.parseLong(value.substring(0, value.length() - 1));
+        return switch (last) {
+            case 'k' -> num * 1024;
+            case 'm' -> num * 1024 * 1024;
+            case 'g' -> num * 1024 * 1024 * 1024;
+            default -> num;
+        };
+    }
+
     private void handleTextInput(KeyEvent ke, TextInputState active, boolean 
digitsOnly) {
         if (ke.isDeleteBackward()) {
             active.deleteBackward();
@@ -674,6 +799,11 @@ class RunOptionsForm {
     }
 
     private void renderTextInput(Frame frame, int x, int y, int w, 
TextInputState state, boolean active) {
+        renderTextInputWithHint(frame, x, y, w, state, active, null);
+    }
+
+    private void renderTextInputWithHint(
+            Frame frame, int x, int y, int w, TextInputState state, boolean 
active, String hint) {
         Rect inputArea = new Rect(x, y, w, 1);
         if (active) {
             TextInput textInput = TextInput.builder()
@@ -682,9 +812,14 @@ class RunOptionsForm {
             frame.renderStatefulWidget(textInput, inputArea, state);
         } else {
             String text = state.text();
-            Style style = text.isEmpty() ? Style.EMPTY.dim() : Style.EMPTY;
-            frame.renderWidget(Paragraph.from(Line.from(
-                    Span.styled(text.isEmpty() ? "—" : text, style))), 
inputArea);
+            if (text.isEmpty() && hint != null) {
+                frame.renderWidget(Paragraph.from(Line.from(
+                        Span.styled(hint, Style.EMPTY.dim()))), inputArea);
+            } else {
+                Style style = text.isEmpty() ? Style.EMPTY.dim() : Style.EMPTY;
+                frame.renderWidget(Paragraph.from(Line.from(
+                        Span.styled(text.isEmpty() ? "—" : text, style))), 
inputArea);
+            }
         }
     }
 

Reply via email to