From b93aa97ac634f6cd53d338d1644be0696305f5e7 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Fri, 22 May 2026 15:00:37 +0200 Subject: [PATCH 1/2] chore: Fix commands using System.console() to work inside camel shell Inside the JLine shell, System.console() returns null, causing subcommands that read interactive input to silently fail. This moves the active terminal reference to EnvironmentHelper so all commands can detect and use the shell's terminal for input/output. Commands fixed: - debug: user input (step, quit) now works inside shell - infra run: "press ENTER to stop" now works inside shell - init: interactive template picker now works inside shell - watch commands: "press enter" to stop now works inside shell - confirmOperation: interactive confirmation now works inside shell Co-Authored-By: Claude Opus 4.6 --- .../jbang/core/commands/CommandHelper.java | 12 +++-- .../camel/dsl/jbang/core/commands/Debug.java | 9 ++-- .../camel/dsl/jbang/core/commands/Init.java | 8 +++- .../camel/dsl/jbang/core/commands/Shell.java | 3 ++ .../jbang/core/commands/infra/InfraRun.java | 7 ++- .../jbang/core/common/EnvironmentHelper.java | 47 ++++++++++++++++++- 6 files changed, 69 insertions(+), 17 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CommandHelper.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CommandHelper.java index 460ae9520cd1e..1bf2b16ad5e60 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CommandHelper.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CommandHelper.java @@ -17,6 +17,7 @@ package org.apache.camel.dsl.jbang.core.commands; import java.io.IOException; +import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -26,6 +27,7 @@ import org.apache.camel.dsl.jbang.core.common.EnvironmentHelper; import org.apache.camel.dsl.jbang.core.common.PathUtils; import org.apache.camel.dsl.jbang.core.common.Printer; +import org.jline.terminal.Terminal; public final class CommandHelper { @@ -93,8 +95,10 @@ public static boolean confirmOperation(String message, boolean yes) { System.out.print(message + " [y/N] "); System.out.flush(); try { - // Do not use try-with-resources here: closing the Scanner would close System.in - Scanner scanner = new Scanner(System.in); + Terminal terminal = EnvironmentHelper.getActiveTerminal(); + InputStream input = terminal != null ? terminal.input() : System.in; + // Do not use try-with-resources here: closing the Scanner would close the input stream + Scanner scanner = new Scanner(input); String answer = scanner.nextLine().trim().toLowerCase(); return "y".equals(answer) || "yes".equals(answer); } catch (Exception e) { @@ -116,8 +120,8 @@ public ReadConsoleTask(Runnable listener) { @Override public void run() { - if (System.console() != null) { - System.console().readLine(); + String line = EnvironmentHelper.readLine(); + if (line != null) { listener.run(); } } diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java index c1b1b56bc8871..67812323163ab 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java @@ -17,7 +17,6 @@ package org.apache.camel.dsl.jbang.core.commands; import java.io.BufferedReader; -import java.io.Console; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -41,6 +40,7 @@ import org.apache.camel.dsl.jbang.core.commands.action.MessageTableHelper; import org.apache.camel.dsl.jbang.core.common.CamelCommandHelper; import org.apache.camel.dsl.jbang.core.common.CommandLineHelper; +import org.apache.camel.dsl.jbang.core.common.EnvironmentHelper; import org.apache.camel.dsl.jbang.core.common.PathUtils; import org.apache.camel.dsl.jbang.core.common.ProcessHelper; import org.apache.camel.dsl.jbang.core.common.VersionHelper; @@ -187,7 +187,6 @@ public Integer doCall() throws Exception { // read log input final AtomicBoolean quit = new AtomicBoolean(); - final Console c = System.console(); if (logLines > 0) { Thread t = new Thread(() -> { doReadLog(quit); @@ -196,7 +195,7 @@ public Integer doCall() throws Exception { } // read CLI input from user - Thread t2 = new Thread(() -> doRead(c, quit), "ReadCommand"); + Thread t2 = new Thread(() -> doRead(quit), "ReadCommand"); t2.start(); do { @@ -285,9 +284,9 @@ private void doReadLog(AtomicBoolean quit) { } while (!quit.get()); } - private void doRead(Console c, AtomicBoolean quit) { + private void doRead(AtomicBoolean quit) { do { - String line = c.readLine(); + String line = EnvironmentHelper.readLine(); if (line != null) { line = line.trim(); if ("q".equalsIgnoreCase(line) || "quit".equalsIgnoreCase(line) || "exit".equalsIgnoreCase(line)) { diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Init.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Init.java index 00772ae1cfa47..da2d911daf109 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Init.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Init.java @@ -34,6 +34,7 @@ import org.apache.camel.CamelContext; import org.apache.camel.dsl.jbang.core.commands.catalog.KameletCatalogHelper; import org.apache.camel.dsl.jbang.core.common.CommandLineHelper; +import org.apache.camel.dsl.jbang.core.common.EnvironmentHelper; import org.apache.camel.dsl.jbang.core.common.ResourceDoesNotExist; import org.apache.camel.dsl.jbang.core.common.VersionHelper; import org.apache.camel.github.GistResourceResolver; @@ -43,6 +44,7 @@ import org.apache.camel.util.FileUtil; import org.apache.camel.util.IOHelper; import org.apache.commons.io.IOUtils; +import org.jline.terminal.Terminal; import picocli.CommandLine.Command; import picocli.CommandLine.Option; import picocli.CommandLine.Parameters; @@ -106,7 +108,7 @@ public Integer doCall() throws Exception { } if (file == null) { // try interactive picker if running in a TTY and not in CI - if (System.console() != null && System.getenv("CI") == null) { + if (EnvironmentHelper.isInteractiveTerminal()) { return interactivePicker(); } printer().printErr("Missing required parameter: "); @@ -309,7 +311,9 @@ private int interactivePicker() throws Exception { pipeTemplates.add(new String[] { "init-pipe.yaml", "Pipe CR (source to sink)", ".yaml" }); categories.put("Pipes and CRs", pipeTemplates); - Scanner scanner = new Scanner(System.in); + Terminal activeTerminal = EnvironmentHelper.getActiveTerminal(); + InputStream scannerInput = activeTerminal != null ? activeTerminal.input() : System.in; + Scanner scanner = new Scanner(scannerInput); // Step 1: Pick a category printer().println("Select a template category:"); diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Shell.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Shell.java index da04270700771..7f406b61a6b9e 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Shell.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Shell.java @@ -99,8 +99,11 @@ public Integer doCall() throws Exception { } try (org.jline.shell.Shell shell = builder.build()) { + EnvironmentHelper.setActiveTerminal(shell.terminal()); printBanner(shell, camelVersion, colorEnabled); shell.run(); + } finally { + EnvironmentHelper.setActiveTerminal(null); } return 0; } diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraRun.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraRun.java index 4e1395de3fec2..7aed979d3784d 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraRun.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraRun.java @@ -16,7 +16,6 @@ */ package org.apache.camel.dsl.jbang.core.commands.infra; -import java.io.Console; import java.io.File; import java.io.IOException; import java.lang.reflect.Method; @@ -32,6 +31,7 @@ import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain; import org.apache.camel.dsl.jbang.core.common.CommandLineHelper; +import org.apache.camel.dsl.jbang.core.common.EnvironmentHelper; import org.apache.camel.dsl.jbang.core.common.Printer; import org.apache.camel.dsl.jbang.core.common.RuntimeUtil; import org.apache.camel.main.download.DependencyDownloaderClassLoader; @@ -226,15 +226,14 @@ protected Integer doRun(String testService, String testServiceImplementation, Te final CountDownLatch latch = new CountDownLatch(1); // running in foreground then wait for user to exit - final Console c = System.console(); - if (c != null) { + if (EnvironmentHelper.isInteractiveTerminal()) { if (!jsonOutput) { printer().println("Press ENTER to stop the execution"); } Thread t = new Thread(() -> { boolean quit = false; do { - String line = c.readLine(); + String line = EnvironmentHelper.readLine(); if (line != null) { quit = true; latch.countDown(); diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/EnvironmentHelper.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/EnvironmentHelper.java index e35385be27c21..034c59ba1914d 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/EnvironmentHelper.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/EnvironmentHelper.java @@ -16,6 +16,13 @@ */ package org.apache.camel.dsl.jbang.core.common; +import java.io.BufferedReader; +import java.io.Console; +import java.io.IOException; +import java.io.InputStreamReader; + +import org.jline.terminal.Terminal; + /** * Helper for detecting environment characteristics such as CI environments, color support, and interactive terminals. * @@ -32,9 +39,45 @@ */ public final class EnvironmentHelper { + private static volatile Terminal activeTerminal; + private EnvironmentHelper() { } + /** + * Sets the active JLine terminal. Called by the shell command to make the terminal available to subcommands. + */ + public static void setActiveTerminal(Terminal terminal) { + activeTerminal = terminal; + } + + /** + * Returns the active JLine terminal, or null if not running inside the shell. + */ + public static Terminal getActiveTerminal() { + return activeTerminal; + } + + /** + * Reads a single line from the best available input source: the active JLine terminal if inside the shell, + * otherwise {@link System#console()}. + * + * @return the line read, or null if no input source is available or an error occurs + */ + public static String readLine() { + Terminal terminal = activeTerminal; + if (terminal != null) { + try { + BufferedReader reader = new BufferedReader(new InputStreamReader(terminal.input())); + return reader.readLine(); + } catch (IOException e) { + return null; + } + } + Console c = System.console(); + return c != null ? c.readLine() : null; + } + /** * Determines whether colored output should be enabled based on environment variables and terminal capabilities. * @@ -59,7 +102,7 @@ public static boolean isColorEnabled() { if (getEnv("FORCE_COLOR") != null) { return true; } - return System.console() != null; + return activeTerminal != null || System.console() != null; } /** @@ -80,7 +123,7 @@ public static boolean isCIEnvironment() { * @return true if the terminal supports interactive prompts */ public static boolean isInteractiveTerminal() { - return System.console() != null && !isCIEnvironment(); + return (activeTerminal != null || System.console() != null) && !isCIEnvironment(); } // Visible for testing - allows overriding in tests From bc680c29cf6d160c288709d73bef4167084546a8 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Sat, 23 May 2026 10:34:41 +0200 Subject: [PATCH 2/2] chore: Reuse shell terminal for TUI subcommands When running TUI commands (tui, tui catalog) inside camel shell, the TUI crashes with "Stream Closed" because TamboUI tries to create a second system terminal while the shell already owns stdin. Add TuiBackendHelper that checks for an active shell terminal via EnvironmentHelper.getActiveTerminal() and passes it to JLineBackend via TuiConfig, so the TUI reuses the shell's terminal instead of creating a new one. Falls back to TuiRunner.create() when JLineBackend(Terminal) is not available (requires tamboui/tamboui#351). --- .../core/commands/tui/CamelCatalogTui.java | 2 +- .../jbang/core/commands/tui/CamelMonitor.java | 2 +- .../core/commands/tui/TuiBackendHelper.java | 55 +++++++++++++++++++ 3 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiBackendHelper.java diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelCatalogTui.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelCatalogTui.java index 7e8aef4c3b3b5..40e20580ae6c9 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelCatalogTui.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelCatalogTui.java @@ -96,7 +96,7 @@ public Integer doCall() throws Exception { loadCatalog(); - try (var tui = TuiRunner.create()) { + try (var tui = TuiBackendHelper.createTuiRunner()) { Signal.handle(new Signal("INT"), sig -> tui.quit()); tui.run(this::handleEvent, this::render); } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java index 72a1265c6d39d..32d3ebe9a9820 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java @@ -281,7 +281,7 @@ public Integer doCall() throws Exception { // Initial data load (synchronous before TUI starts) refreshDataSync(); - try (var tui = TuiRunner.create()) { + try (var tui = TuiBackendHelper.createTuiRunner()) { this.runner = tui; ctx.runner = tui; // Intercept Ctrl+C: quit the TUI cleanly instead of letting diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiBackendHelper.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiBackendHelper.java new file mode 100644 index 0000000000000..87c2c641561a7 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiBackendHelper.java @@ -0,0 +1,55 @@ +/* + * 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.lang.reflect.Constructor; + +import dev.tamboui.backend.jline3.JLineBackend; +import dev.tamboui.terminal.Backend; +import dev.tamboui.tui.TuiConfig; +import dev.tamboui.tui.TuiRunner; +import org.apache.camel.dsl.jbang.core.common.EnvironmentHelper; +import org.jline.terminal.Terminal; + +final class TuiBackendHelper { + + private TuiBackendHelper() { + } + + static TuiRunner createTuiRunner() throws Exception { + Terminal activeTerminal = EnvironmentHelper.getActiveTerminal(); + if (activeTerminal != null) { + Backend backend = createBackendForTerminal(activeTerminal); + if (backend != null) { + return TuiRunner.create(TuiConfig.builder().backend(backend).build()); + } + } + return TuiRunner.create(); + } + + private static Backend createBackendForTerminal(Terminal terminal) { + try { + Constructor ctor = JLineBackend.class.getDeclaredConstructor(Terminal.class); + return ctor.newInstance(terminal); + } catch (NoSuchMethodException e) { + // JLineBackend(Terminal) not available in this version, fall back + return null; + } catch (Exception e) { + return null; + } + } +}