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 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; + } + } +}