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 9f3f1afd0677 CAMEL-23226: Enhance Shell with rich prompt, banner, and
auto-suggestions (#22170)
9f3f1afd0677 is described below
commit 9f3f1afd067736341276e96b7e59b616d2bdc847
Author: Guillaume Nodet <[email protected]>
AuthorDate: Thu Apr 9 11:15:09 2026 +0200
CAMEL-23226: Enhance Shell with rich prompt, banner, and auto-suggestions
(#22170)
* CAMEL-23226: Migrate Shell to JLine 4 Shell API
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* CAMEL-23226: Add POSIX, interactive, and script commands to JBang shell
Add PosixCommandGroup (cd, ls, grep, cat, head, tail, etc.),
InteractiveCommandGroup (nano, less, ttop), and enable
script/variable commands for a richer shell experience.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* CAMEL-23226: Enhance Shell with alias persistence and init script support
- Add DefaultAliasManager with persistence at ~/.camel-jbang-aliases
for persistent alias/unalias commands
- Add init script support via ~/.camel-jbang-init file
- Remove scriptCommands(true) which requires a scriptRunner to function
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* CAMEL-23226: Enhance Shell with rich prompt, banner, and auto-suggestions
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* CAMEL-23226: Bump JLine to 4.0.8 for picocli argument completion
JLine 4.0.8 includes jline/jline3#1708 which fixes PicocliCommandRegistry
to use picocli's AutoComplete for proper tab completion of options, option
values, positional parameters, and nested subcommands.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* CAMEL-23226: Restore help colors and completion styling in Shell
- Restore HELP_COLORS variable for colored help output
- Restore OTHERS_GROUP_NAME and COMPLETION_STYLE_GROUP variables
- Use EnvironmentHelper.isColorEnabled() instead of duplicated logic
- Use ShellBuilder import instead of FQCN where possible
- Add TODO for replacing AutosuggestionWidgets with CommandTailTipWidgets
(needs ShellBuilder to expose CommandDispatcher in onReaderReady callback)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* CAMEL-23226: Use CommandTailTipWidgets for unified autosuggestion and
tooltips
Replace AutosuggestionWidgets with CommandTailTipWidgets which provides
both fish-style autosuggestion AND command description tooltips below
the cursor line. This requires the JLine ShellBuilder.onReaderReady
BiConsumer<LineReader, CommandDispatcher> API enhancement.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* CAMEL-23226: Bump JLine to 4.0.9
Includes:
- BiConsumer<LineReader, CommandDispatcher> overload for
ShellBuilder.onReaderReady
- Deprecated no-op jansi()/jna() on TerminalBuilder for JLine 3
compatibility
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
---
dsl/camel-jbang/camel-jbang-core/pom.xml | 8 +-
.../camel/dsl/jbang/core/commands/Shell.java | 204 +++++++++++----------
2 files changed, 112 insertions(+), 100 deletions(-)
diff --git a/dsl/camel-jbang/camel-jbang-core/pom.xml
b/dsl/camel-jbang/camel-jbang-core/pom.xml
index 31dccd48d50d..f236054ebe75 100644
--- a/dsl/camel-jbang/camel-jbang-core/pom.xml
+++ b/dsl/camel-jbang/camel-jbang-core/pom.xml
@@ -89,13 +89,13 @@
<version>${picocli-version}</version>
</dependency>
<dependency>
- <groupId>info.picocli</groupId>
- <artifactId>picocli-shell-jline3</artifactId>
- <version>${picocli-version}</version>
+ <groupId>org.jline</groupId>
+ <artifactId>jline</artifactId>
+ <version>${jline-version}</version>
</dependency>
<dependency>
<groupId>org.jline</groupId>
- <artifactId>jline</artifactId>
+ <artifactId>jline-picocli</artifactId>
<version>${jline-version}</version>
</dependency>
<dependency>
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 eeffd993ed50..8cb436ccbde7 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
@@ -16,35 +16,23 @@
*/
package org.apache.camel.dsl.jbang.core.commands;
+import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
-import java.util.function.Supplier;
+import org.apache.camel.dsl.jbang.core.common.EnvironmentHelper;
+import org.apache.camel.dsl.jbang.core.common.VersionHelper;
import org.apache.camel.util.HomeHelper;
-import org.jline.builtins.ClasspathResourceUtil;
-import org.jline.builtins.ConfigurationPath;
-import org.jline.console.SystemRegistry;
-import org.jline.console.impl.Builtins;
-import org.jline.console.impl.SystemRegistryImpl;
-import org.jline.keymap.KeyMap;
-import org.jline.reader.Binding;
-import org.jline.reader.EndOfFileException;
+import org.jline.builtins.InteractiveCommandGroup;
+import org.jline.builtins.PosixCommandGroup;
+import org.jline.picocli.PicocliCommandRegistry;
import org.jline.reader.LineReader;
-import org.jline.reader.LineReaderBuilder;
-import org.jline.reader.MaskingCallback;
-import org.jline.reader.Parser;
-import org.jline.reader.Reference;
-import org.jline.reader.UserInterruptException;
-import org.jline.reader.impl.DefaultHighlighter;
-import org.jline.reader.impl.DefaultParser;
-import org.jline.terminal.Terminal;
-import org.jline.terminal.TerminalBuilder;
+import org.jline.shell.ShellBuilder;
+import org.jline.shell.impl.DefaultAliasManager;
+import org.jline.shell.widget.CommandTailTipWidgets;
import org.jline.utils.AttributedStringBuilder;
import org.jline.utils.AttributedStyle;
-import org.jline.utils.InfoCmp;
-import org.jline.widget.TailTipWidgets;
import picocli.CommandLine;
-import picocli.shell.jline3.PicocliCommands;
@CommandLine.Command(name = "shell",
description = "Interactive Camel JBang shell.",
@@ -55,92 +43,116 @@ import picocli.shell.jline3.PicocliCommands;
"Press Ctrl-C to exit." })
public class Shell extends CamelCommand {
+ // Camel orange color (packed RGB)
+ private static final int CAMEL_ORANGE = 0xF69123;
+
+ // Help colors: title=bold blue, command=38, args=italic, options=yellow,
description=dark gray
+ private static final String HELP_COLORS = "ti=1;34:co=38:ar=3:op=33:de=90";
+
public Shell(CamelJBangMain main) {
super(main);
}
@Override
public Integer doCall() throws Exception {
- Supplier<Path> workDir = () ->
Paths.get(System.getProperty("user.dir"));
- // set up JLine built-in commands
- Path appConfig =
ClasspathResourceUtil.getResourcePath("/nano/jnanorc", getClass()).getParent();
- Builtins builtins = new Builtins(workDir, new
ConfigurationPath(appConfig, workDir.get()), null) {
- @Override
- public String name() {
- return "built-in";
- }
- };
-
- PicocliCommands.PicocliCommandsFactory factory = new
PicocliCommands.PicocliCommandsFactory();
- PicocliCommands commands = new
PicocliCommands(CamelJBangMain.getCommandLine());
- commands.name("Camel");
-
- try (Terminal terminal = TerminalBuilder.builder().build()) {
- Parser parser = new DefaultParser();
- SystemRegistry systemRegistry = new SystemRegistryImpl(parser,
terminal, workDir, null);
- systemRegistry.setCommandRegistries(builtins, commands);
- systemRegistry.register("help", commands);
-
- String history = Paths.get(HomeHelper.resolveHomeDir(),
".camel-jbang-history").toString();
- LineReader reader = LineReaderBuilder.builder()
- .terminal(terminal)
- .completer(systemRegistry.completer())
- .parser(parser)
- .highlighter(new ReplHighlighter())
- .variable(LineReader.LIST_MAX, 50) // max tab completion
candidates
- .variable(LineReader.HISTORY_FILE, history)
- .variable(LineReader.OTHERS_GROUP_NAME, "Others")
- .variable(LineReader.COMPLETION_STYLE_GROUP,
"fg:blue,bold")
- .variable("HELP_COLORS", "ti=1;34:co=38:ar=3:op=33:de=90")
- .option(LineReader.Option.GROUP_PERSIST, true)
- .build();
- builtins.setLineReader(reader);
- factory.setTerminal(terminal);
- TailTipWidgets widgets
- = new TailTipWidgets(reader,
systemRegistry::commandDescription, 5, TailTipWidgets.TipType.COMPLETER);
- widgets.enable();
- KeyMap<Binding> keyMap = reader.getKeyMaps().get("main");
- keyMap.bind(new Reference("tailtip-toggle"), KeyMap.alt("s"));
- String prompt = "camel> ";
- String rightPrompt = null;
-
- // start the shell and process input until the user quits with
Ctrl-C or Ctrl-D
- String line;
- boolean run = true;
- TerminalBuilder.setTerminalOverride(terminal);
- while (run) {
- try {
- systemRegistry.cleanUp();
- line = reader.readLine(prompt, rightPrompt,
(MaskingCallback) null, null);
- systemRegistry.execute(line);
- } catch (UserInterruptException e) {
- // ctrl + c is pressed so exit
- run = false;
- } catch (EndOfFileException e) {
- // ctrl + d is pressed so exit
- run = false;
- } catch (Exception e) {
- systemRegistry.trace(e);
- }
- }
- } finally {
- TerminalBuilder.setTerminalOverride(null);
+ PicocliCommandRegistry registry = new
PicocliCommandRegistry(CamelJBangMain.getCommandLine());
+
+ String homeDir = HomeHelper.resolveHomeDir();
+ Path history = Paths.get(homeDir, ".camel-jbang-history");
+
+ // Alias persistence: aliases are stored in ~/.camel-jbang-aliases
+ Path aliasFile = Paths.get(homeDir, ".camel-jbang-aliases");
+ DefaultAliasManager aliasManager = new DefaultAliasManager(aliasFile);
+
+ // Init script: if ~/.camel-jbang-init exists, it will be executed on
shell startup
+ Path initScript = Paths.get(homeDir, ".camel-jbang-init");
+
+ String camelVersion = VersionHelper.extractCamelVersion();
+ boolean colorEnabled = EnvironmentHelper.isColorEnabled();
+
+ // org.jline.shell.Shell is used via FQCN to avoid clash with this
class name
+ ShellBuilder builder = org.jline.shell.Shell.builder()
+ .prompt(() -> buildPrompt(camelVersion, colorEnabled))
+ .rightPrompt(() -> buildRightPrompt(colorEnabled))
+ .groups(registry, new PosixCommandGroup(), new
InteractiveCommandGroup())
+ .historyFile(history)
+ .historyCommands(true)
+ .helpCommands(true)
+ .variableCommands(true)
+ .commandHighlighter(true)
+ .aliasManager(aliasManager)
+ .onReaderReady((reader, dispatcher) -> {
+ // CommandTailTipWidgets provides both fish-style
autosuggestion
+ // and command description tooltips below the cursor line
+ new CommandTailTipWidgets(reader, dispatcher, 5).enable();
+ })
+ .variable(LineReader.LIST_MAX, 50)
+ .variable(LineReader.OTHERS_GROUP_NAME, "Others")
+ .variable(LineReader.COMPLETION_STYLE_GROUP, "fg:blue,bold")
+ .variable("HELP_COLORS", HELP_COLORS)
+ .option(LineReader.Option.GROUP_PERSIST, true);
+
+ if (Files.exists(initScript)) {
+ builder.initScript(initScript.toFile());
+ }
+
+ try (org.jline.shell.Shell shell = builder.build()) {
+ printBanner(shell, camelVersion, colorEnabled);
+ shell.run();
}
return 0;
}
- private static class ReplHighlighter extends DefaultHighlighter {
- @Override
- protected void commandStyle(LineReader reader, AttributedStringBuilder
sb, boolean enable) {
- if (enable) {
- if
(reader.getTerminal().getNumericCapability(InfoCmp.Capability.max_colors) >=
256) {
- sb.style(AttributedStyle.DEFAULT.bold().foreground(69));
- } else {
-
sb.style(AttributedStyle.DEFAULT.foreground(AttributedStyle.CYAN));
- }
- } else {
- sb.style(AttributedStyle.DEFAULT.boldOff().foregroundOff());
+ private static String buildPrompt(String camelVersion, boolean
colorEnabled) {
+ if (!colorEnabled) {
+ return camelVersion != null ? "camel " + camelVersion + "> " :
"camel> ";
+ }
+ AttributedStringBuilder sb = new AttributedStringBuilder();
+ sb.append("camel",
AttributedStyle.DEFAULT.bold().foregroundRgb(CAMEL_ORANGE));
+ if (camelVersion != null) {
+ sb.append(" ");
+ sb.append(camelVersion,
AttributedStyle.DEFAULT.foreground(AttributedStyle.GREEN));
+ }
+ sb.append("> ", AttributedStyle.DEFAULT);
+ return sb.toAnsi();
+ }
+
+ private static String buildRightPrompt(boolean colorEnabled) {
+ String cwd = System.getProperty("user.dir");
+ String home = System.getProperty("user.home");
+ if (cwd != null && home != null && cwd.startsWith(home)) {
+ cwd = "~" + cwd.substring(home.length());
+ }
+ if (cwd == null) {
+ return null;
+ }
+ if (!colorEnabled) {
+ return cwd;
+ }
+ return new AttributedStringBuilder()
+ .append(cwd, AttributedStyle.DEFAULT.faint())
+ .toAnsi();
+ }
+
+ private static void printBanner(org.jline.shell.Shell shell, String
camelVersion, boolean colorEnabled) {
+ var writer = shell.terminal().writer();
+ if (colorEnabled) {
+ AttributedStringBuilder sb = new AttributedStringBuilder();
+ sb.append("Apache Camel",
AttributedStyle.DEFAULT.bold().foregroundRgb(CAMEL_ORANGE));
+ sb.append(" JBang Shell", AttributedStyle.DEFAULT.bold());
+ if (camelVersion != null) {
+ sb.append(" v" + camelVersion,
AttributedStyle.DEFAULT.foreground(AttributedStyle.GREEN));
+ }
+ writer.println(sb.toAnsi(shell.terminal()));
+ } else {
+ String banner = "Apache Camel JBang Shell";
+ if (camelVersion != null) {
+ banner += " v" + camelVersion;
}
+ writer.println(banner);
}
+ writer.println("Type 'help' for available commands, 'exit' to quit.");
+ writer.println();
+ writer.flush();
}
}