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 7dd843696baf CAMEL-25426: camel-jbang - small fixes to the monitor,
the source editor and the shell (#27520)
7dd843696baf is described below
commit 7dd843696baf14aebbfc05cdb7f5daa93edf39d8
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Oct 8 01:55:08 2026 +0200
CAMEL-25426: camel-jbang - small fixes to the monitor, the source editor
and the shell (#27520)
* CAMEL-25426: camel-jbang - the shell accepts a leading camel, as the docs
type it
The shell's prompt is camel>, so commands are typed without the prefix, but
the docs and the monitor's settings page say camel ps, camel run, and the
shell answered "Unknown command: camel". A leading camel now runs the rest
of the line, in camel shell and in the monitor's shell panel.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
* CAMEL-25426: camel-base - failure handled events say the error handler
handled it, not processor null
ExchangeFailureHandledEvent and ExchangeFailureHandlingEvent read "exchange
failed and sent to processor: null" when the error handler had no failure
processor. They now say "and handled by the error handler" (or "being
handled") in that case.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
* CAMEL-25426: camel-jbang - the monitor's example browser is as tall as
its list
The popup was sized by the number of examples (40), not the lines it shows,
so the 11 groups left a big empty block under the hint line. It is now as
tall as its list. The footer says 0-9 jump, as 0 jumps to the tenth group.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
* CAMEL-25426: camel-jbang - the monitor's info panel names the inflight
load
The load figures under "Load (1m/5m/15m):" read as an unlabelled "Infl"
row. It now says Inflight.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
* CAMEL-25426: camel-jbang - the monitor's Process tab has a help page
F1 showed nothing on the Process tab, the only tab without a help page, and
its description promised CPU, memory and file descriptors, which it does not
show. It now has a help page (fields, command line, keys) and says what it
shows.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
* CAMEL-25426: camel-jbang - the Ollama context panel wraps, and Doctor's
models line fits its box
The Context panel of the Ollama tab cut its sentence ("once the runner is
foun"): it now wraps. Doctor's models line ran into the right border; it is
now as wide as the rows above it.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
* CAMEL-25426: camel-jbang - the Architecture view says where the AI
project overview is
Routes not grouped yet said "/overview groups it", without saying that
/overview is a command of the AI panel. It now says "the AI project overview
groups the routes (F8, then /overview)".
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
* CAMEL-25426: camel-jbang - the Doctor test expects the shorter models line
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
* CAMEL-25426: camel-jbang - the Architecture view's Flow panel does not
hide a box
The Flow panel of the selected group was always drawn at the bottom right,
on top of a capability box when one was there (half of "Customer
Notification" was hidden). It now goes to the first corner the diagram
leaves blank, and to the bottom right only when none is.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
* CAMEL-25426: camel-jbang - converting a route file in the source editor
warns that both files define the routes
Convert to XML (or YAML, Java) writes the new file next to the original and
keeps it, so the folder has the same routes twice, and the next run failed
with "Duplicate route ids detected" with nothing saying why before. The
notice now says so, as a warning: delete or rename one of them.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
* CAMEL-25426: camel-jbang - the source editor is not modified after
undoing every change
Undo always marked the file as modified, so after undoing back to the saved
text the title still showed "*" and Esc asked to discard changes. Undo and
redo now compare with the saved text. One save path did not update that
saved text, which the change gutter and the F7 diff also use.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
* CAMEL-25426: camel-jbang - the AI log's Detail panel wraps long lines
The Detail panel cut long lines at its right edge ("threw an exception
(`java.l"). The content is now wrapped to the panel.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
* CAMEL-25426: camel-jbang - the monitor's toggle-theme action is called
next-theme
The action never toggled between two themes: it steps to the next theme of
the list, and saves it to the user config. It is now called next-theme;
toggle-theme still works.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
* CAMEL-25426: camel-jbang - converting a route file in the source editor
can replace the original
Convert to XML (YAML, Java) now asks whether to replace the original: y
writes the converted file and removes the original, so the folder does not
define the same routes twice; k keeps both, with the warning as before.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
* CAMEL-25426: camel-jbang - the Memory Leak tab says when no reference
chain was recorded
A flagged object showed "Reference Chain (Object -> GC Root):" with only the
object below it: JFR records the path to a GC root only with the cutoff
setting of jdk.OldObjectSample, which the recording does not set. The tab
now says the chain was not recorded, instead of a heading that promises one.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
* CAMEL-25426: camel-jbang - the monitor's Activity window reads forward,
and Endpoints show a count of 0
The Activity summary said "Window: 2m20s ... 2m18s", the oldest age first,
which reads as a range going backwards; it now says "2m18s - 2m20s ago". The
Endpoints tab left TOTAL blank for an endpoint with no messages, next to
BODY and HDR of 0 B; it now shows 0.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
* CAMEL-25426: camel-jbang - the source editor offers the common options
before the advanced ones
Completion of endpoint (and EIP) options listed them alphabetically, so the
advanced ones (bridgeErrorHandler, exceptionHandler, exchangePattern) came
before the common ones (delay, period). The options of the catalog's
advanced groups now come last, below an "advanced" divider, as the options
of the kamelet component do below a Kamelet's own.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
* CAMEL-25426: camel-jbang - the popup of a save that validation blocked
offers the quick fix
The "Validation Error" popup of a blocked save offered only Esc close; the
fix the editor knew for the problem (Shift+F9: periiod -> period) showed
only in the panel at the bottom after closing it. The popup now offers it,
and Shift+F9 there goes to the line and applies it.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
* CAMEL-25426: camel-jbang - undo in the source editor takes back one
change, with the cursor where it was
Undo restored the snapshot under the one taken before the latest change, so
it took back two changes at once (typing "xy" and Ctrl+Z removed both), and
the last undo only moved the cursor to line 1. It now restores the snapshot
taken just before the latest change, with its cursor.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
* CAMEL-25426: camel-jbang - the AI log shows an answer as Markdown, as the
AI panel does
The Detail panel of the AI log showed a prompt or an answer as raw Markdown
(**declined**, backticks). It now renders it as the AI panel does, wrapped
to the panel. Tool arguments and results stay as JSON.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
* CAMEL-25426: camel-jbang - the Inspect tab keeps room for the processor
on a narrow screen
At 100 columns the ROUTE and ID columns kept 25 characters each and the
processor got 4 ("from", "un", "to"). They now give way below 130 columns,
and a processor that does not fit ends with an ellipsis.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
* CAMEL-25426: camel-base - failure handled events also name no processor
for an onException without an id
The steps of an onException are a Pipeline, which prints as its id; without
an id it printed null, so the events still read "sent to processor: null".
Such a handler is now the error handler too, as for no failure processor.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
---------
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
.../impl/event/ExchangeFailureHandledEvent.java | 5 ++
.../impl/event/ExchangeFailureHandlingEvent.java | 5 ++
.../ExchangeFailureHandledEventTextTest.java | 77 ++++++++++++++++++
.../core/commands/CamelShellCommandRegistry.java | 90 ++++++++++++++++++++++
.../camel/dsl/jbang/core/commands/Shell.java | 8 +-
.../commands/CamelShellCommandRegistryTest.java | 78 +++++++++++++++++++
.../dsl/jbang/core/commands/tui/ActionsPopup.java | 11 ++-
.../dsl/jbang/core/commands/tui/ActivityTab.java | 5 +-
.../dsl/jbang/core/commands/tui/AiLogPopup.java | 35 +++++----
.../jbang/core/commands/tui/ArchitectureView.java | 36 ++++++++-
.../jbang/core/commands/tui/AutocompletePopup.java | 21 ++++-
.../dsl/jbang/core/commands/tui/DialogHelper.java | 11 ++-
.../dsl/jbang/core/commands/tui/DoctorPopup.java | 3 +-
.../dsl/jbang/core/commands/tui/EndpointsTab.java | 2 +-
.../core/commands/tui/ExampleBrowserPopup.java | 9 ++-
.../jbang/core/commands/tui/FileActionsPopup.java | 37 ++++++++-
.../dsl/jbang/core/commands/tui/HistoryTab.java | 42 +++++++---
.../dsl/jbang/core/commands/tui/MemoryLeakTab.java | 30 +++++++-
.../dsl/jbang/core/commands/tui/OllamaTab.java | 5 +-
.../dsl/jbang/core/commands/tui/OverviewTab.java | 4 +-
.../dsl/jbang/core/commands/tui/ProcessTab.java | 7 +-
.../dsl/jbang/core/commands/tui/ShellPanel.java | 9 +--
.../jbang/core/commands/tui/SourceEditHistory.java | 6 +-
.../dsl/jbang/core/commands/tui/SourceTab.java | 21 +++--
.../dsl/jbang/core/commands/tui/SourceViewer.java | 43 ++++++++++-
.../core/commands/tui/TuiToolDefinitions.java | 2 +-
.../jbang/core/commands/tui/TuiToolRegistry.java | 2 +-
.../src/main/resources/tui/help/process.md | 28 +++++++
.../commands/tui/ArchitectureFlowPanelTest.java | 50 ++++++++++++
.../core/commands/tui/AutocompletePopupTest.java | 23 ++++++
.../core/commands/tui/DoctorPopupOllamaTest.java | 2 +-
.../jbang/core/commands/tui/SourceConvertTest.java | 21 ++++-
.../core/commands/tui/SourceEditHistoryTest.java | 20 +++++
.../core/commands/tui/SourceViewerEditTest.java | 15 ++++
34 files changed, 680 insertions(+), 83 deletions(-)
diff --git
a/core/camel-base/src/main/java/org/apache/camel/impl/event/ExchangeFailureHandledEvent.java
b/core/camel-base/src/main/java/org/apache/camel/impl/event/ExchangeFailureHandledEvent.java
index 40a62863fcdc..672397cb7036 100644
---
a/core/camel-base/src/main/java/org/apache/camel/impl/event/ExchangeFailureHandledEvent.java
+++
b/core/camel-base/src/main/java/org/apache/camel/impl/event/ExchangeFailureHandledEvent.java
@@ -70,6 +70,11 @@ public class ExchangeFailureHandledEvent extends
AbstractExchangeEvent implement
String uri = URISupport.sanitizeUri(deadLetterUri);
return getExchange().getExchangeId() + " exchange failed"
+ " and sent to dead letter channel: " + uri;
+ } else if (failureHandler == null || failureHandler.toString() == null
+ || "null".equals(failureHandler.toString())) {
+ // no failure processor, or one without a name (the steps of an
onException print as their id, which
+ // they may not have): the error handler handles it
+ return getExchange().getExchangeId() + " exchange failed and
handled by the error handler";
} else {
return getExchange().getExchangeId() + " exchange failed"
+ " and sent to processor: " + failureHandler;
diff --git
a/core/camel-base/src/main/java/org/apache/camel/impl/event/ExchangeFailureHandlingEvent.java
b/core/camel-base/src/main/java/org/apache/camel/impl/event/ExchangeFailureHandlingEvent.java
index b846c9264208..7b955c958bfe 100644
---
a/core/camel-base/src/main/java/org/apache/camel/impl/event/ExchangeFailureHandlingEvent.java
+++
b/core/camel-base/src/main/java/org/apache/camel/impl/event/ExchangeFailureHandlingEvent.java
@@ -59,6 +59,11 @@ public class ExchangeFailureHandlingEvent extends
AbstractExchangeEvent implemen
String uri = URISupport.sanitizeUri(deadLetterUri);
return getExchange().getExchangeId() + " exchange failed"
+ " and sending to dead letter channel: " + uri;
+ } else if (failureHandler == null || failureHandler.toString() == null
+ || "null".equals(failureHandler.toString())) {
+ // no failure processor, or one without a name (the steps of an
onException print as their id, which
+ // they may not have): the error handler handles it
+ return getExchange().getExchangeId() + " exchange failed and being
handled by the error handler";
} else {
return getExchange().getExchangeId() + " exchange failed"
+ " and sending to processor: " + failureHandler;
diff --git
a/core/camel-core/src/test/java/org/apache/camel/processor/ExchangeFailureHandledEventTextTest.java
b/core/camel-core/src/test/java/org/apache/camel/processor/ExchangeFailureHandledEventTextTest.java
new file mode 100644
index 000000000000..11cd069506d5
--- /dev/null
+++
b/core/camel-core/src/test/java/org/apache/camel/processor/ExchangeFailureHandledEventTextTest.java
@@ -0,0 +1,77 @@
+/*
+ * 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.processor;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.spi.CamelEvent;
+import org.apache.camel.support.EventNotifierSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The text of the failure handling events says what handled the failure, not
"processor: null" for the steps of an
+ * onException, which print as their id (none).
+ */
+class ExchangeFailureHandledEventTextTest extends ContextTestSupport {
+
+ private final List<String> texts = new CopyOnWriteArrayList<>();
+
+ @Test
+ void anOnExceptionWithoutIdIsTheErrorHandler() {
+ template.sendBody("direct:start", "Hello");
+
+ assertThat(texts).isNotEmpty();
+ assertThat(texts).noneMatch(t -> t.contains("processor: null"));
+ assertThat(texts).anyMatch(t -> t.endsWith("exchange failed and
handled by the error handler"));
+ }
+
+ @Override
+ protected CamelContext createCamelContext() throws Exception {
+ CamelContext context = super.createCamelContext();
+ context.getManagementStrategy().addEventNotifier(new
EventNotifierSupport() {
+ @Override
+ public void notify(CamelEvent event) {
+ if (event instanceof CamelEvent.ExchangeFailureHandledEvent
+ || event instanceof
CamelEvent.ExchangeFailureHandlingEvent) {
+ texts.add(event.toString());
+ }
+ }
+ });
+ return context;
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ onException(IllegalStateException.class).handled(true)
+ .log("declined: ${exception.message}")
+ .to("mock:parked");
+
+ from("direct:start")
+ .throwException(new IllegalStateException("card
declined"));
+ }
+ };
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelShellCommandRegistry.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelShellCommandRegistry.java
new file mode 100644
index 000000000000..e57460a1b590
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelShellCommandRegistry.java
@@ -0,0 +1,90 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import org.jline.picocli.PicocliCommandRegistry;
+import org.jline.shell.Command;
+import picocli.CommandLine;
+
+/**
+ * The Camel commands of the shell ({@code camel shell}, and the shell panel
of the monitor). The prompt is
+ * {@code camel>}, so commands are typed without the prefix, but the docs and
the help say {@code camel ps}: a leading
+ * {@code camel} is accepted, and the rest of the line runs as the command it
names.
+ */
+public class CamelShellCommandRegistry extends PicocliCommandRegistry {
+
+ static final String PREFIX = "camel";
+
+ private final Command prefix = new Command() {
+ @Override
+ public String name() {
+ return PREFIX;
+ }
+
+ @Override
+ public String description() {
+ return "Runs the command that follows (camel ps is ps)";
+ }
+
+ @Override
+ // org.jline.shell.CommandSession, not the CommandSession this
registry inherits from CommandRegistry
+ public Object execute(org.jline.shell.CommandSession session, String[]
args) throws Exception {
+ if (args.length == 0) {
+ session.out().println("Type a command, with or without camel:
ps, or camel ps");
+ return null;
+ }
+ Command target = PREFIX.equals(args[0]) ? null :
CamelShellCommandRegistry.super.command(args[0]);
+ if (target == null) {
+ session.err().println("Unknown command: " + args[0]);
+ return null;
+ }
+ return target.execute(session, Arrays.copyOfRange(args, 1,
args.length));
+ }
+ };
+
+ public CamelShellCommandRegistry(CommandLine commandLine) {
+ // TODO: replace with new PicocliCommandRegistry(commandLine, "Camel")
when JLine merges #1947
+ super(commandLine);
+ }
+
+ @Override
+ public String name() {
+ return "Camel";
+ }
+
+ @Override
+ public Collection<Command> commands() {
+ List<Command> answer = new ArrayList<>(super.commands());
+ answer.add(prefix);
+ return answer;
+ }
+
+ @Override
+ public Command command(String name) {
+ return PREFIX.equals(name) ? prefix : super.command(name);
+ }
+
+ @Override
+ public boolean hasCommand(String name) {
+ return PREFIX.equals(name) || super.hasCommand(name);
+ }
+}
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 68e8af4fcdbe..f25e026343cb 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
@@ -57,13 +57,7 @@ public class Shell extends CamelCommand {
@Override
public Integer doCall() throws Exception {
- // TODO: replace with new PicocliCommandRegistry(commandLine, "Camel")
when JLine merges #1947
- PicocliCommandRegistry registry = new
PicocliCommandRegistry(CamelJBangMain.getCommandLine()) {
- @Override
- public String name() {
- return "Camel";
- }
- };
+ PicocliCommandRegistry registry = new
CamelShellCommandRegistry(CamelJBangMain.getCommandLine());
String homeDir = HomeHelper.resolveHomeDir();
Path history = Paths.get(homeDir, ".camel-jbang-history");
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/CamelShellCommandRegistryTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/CamelShellCommandRegistryTest.java
new file mode 100644
index 000000000000..6efcd7f61bd2
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/CamelShellCommandRegistryTest.java
@@ -0,0 +1,78 @@
+/*
+ * 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;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.nio.charset.StandardCharsets;
+
+import org.jline.shell.CommandSession;
+import org.junit.jupiter.api.Test;
+import picocli.CommandLine;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The shell's prompt is camel>, but the docs say camel ps: a leading camel
runs the rest of the line.
+ */
+class CamelShellCommandRegistryTest {
+
+ @CommandLine.Command(name = "root", subcommands = { Greet.class })
+ static class Root {
+ }
+
+ @CommandLine.Command(name = "greet")
+ static class Greet implements Runnable {
+ @Override
+ public void run() {
+ }
+ }
+
+ @Test
+ void camelIsACommandOfTheShell() {
+ CamelShellCommandRegistry registry = new CamelShellCommandRegistry(new
CommandLine(new Root()));
+
+ assertThat(registry.hasCommand("camel")).isTrue();
+ assertThat(registry.hasCommand("greet")).isTrue();
+ assertThat(registry.commands()).anyMatch(c ->
"camel".equals(c.name()));
+ }
+
+ @Test
+ void anUnknownCommandAfterCamelIsSaidSo() throws Exception {
+ CamelShellCommandRegistry registry = new CamelShellCommandRegistry(new
CommandLine(new Root()));
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+
+ registry.command("camel").execute(session(out), new String[] { "nope"
});
+
+ assertThat(out.toString(StandardCharsets.UTF_8)).contains("Unknown
command: nope");
+ }
+
+ @Test
+ void aBareCamelSaysHowToTypeACommand() throws Exception {
+ CamelShellCommandRegistry registry = new CamelShellCommandRegistry(new
CommandLine(new Root()));
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+
+ registry.command("camel").execute(session(out), new String[0]);
+
+ assertThat(out.toString(StandardCharsets.UTF_8)).contains("with or
without camel");
+ }
+
+ private static CommandSession session(ByteArrayOutputStream out) {
+ PrintStream ps = new PrintStream(out, true, StandardCharsets.UTF_8);
+ return new CommandSession(null, System.in, ps, ps);
+ }
+}
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 da1cd007a013..fb828063471f 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
@@ -66,7 +66,8 @@ class ActionsPopup {
MCP_SUBMENU,
BACK,
SCREENSHOT,
- TOGGLE_THEME,
+ // the next theme of the list (each step is saved to the user config);
toggle-theme still works
+ NEXT_THEME,
THEMES_SUBMENU,
RESET_SCREEN,
TAPE_RECORDING,
@@ -758,7 +759,7 @@ class ActionsPopup {
if (resetScreenAction != null) {
resetScreenAction.run();
}
- } else if (action == Action.TOGGLE_THEME) {
+ } else if (action == Action.NEXT_THEME) {
Theme.toggle();
refreshTheme();
showActionsMenu = false;
@@ -1458,6 +1459,10 @@ class ActionsPopup {
return false;
}
String normalized = name.strip().replace("-",
"_").toUpperCase(Locale.ROOT);
+ if ("TOGGLE_THEME".equals(normalized)) {
+ // the earlier name: it never toggled between two, it steps
through all themes
+ normalized = "NEXT_THEME";
+ }
Action action;
try {
action = Action.valueOf(normalized);
@@ -1478,7 +1483,7 @@ class ActionsPopup {
resetScreenAction.run();
}
}
- case TOGGLE_THEME -> {
+ case NEXT_THEME -> {
Theme.toggle();
refreshTheme();
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActivityTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActivityTab.java
index da3aa7c5b614..87d544f7757d 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActivityTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActivityTab.java
@@ -274,9 +274,10 @@ class ActivityTab extends AbstractTableTab {
oldestTs < Long.MAX_VALUE
? Span.styled(" Window: ", dim)
: Span.raw(""),
+ // newest to oldest, as ages: "2m18s - 2m20s ago" (the larger
first read as a range going backwards)
oldestTs < Long.MAX_VALUE
- ? Span.raw(TimeUtils.printSince(oldestTs)
- + " ... " + TimeUtils.printSince(newestTs))
+ ? Span.raw(TimeUtils.printSince(newestTs)
+ + " - " + TimeUtils.printSince(oldestTs) +
" ago")
: Span.raw("")));
String title = paused
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiLogPopup.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiLogPopup.java
index 575f3280b900..2eb9bb99b8ff 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiLogPopup.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiLogPopup.java
@@ -164,24 +164,31 @@ class AiLogPopup {
private void renderDetail(Frame frame, Rect area) {
AiPanel.LogEntry entry = entries.get(selected);
+ String content = entry.detail();
+ if (content != null && !content.isBlank()
+ && entry.level() != AiPanel.LogLevel.TOOL && entry.level() !=
AiPanel.LogLevel.RESULT) {
+ // an answer or a prompt is Markdown: shown as the AI panel shows
it (bold, code, lists), wrapped
+ Block block =
Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title("
Detail ").build();
+ frame.renderWidget(dev.tamboui.markdown.MarkdownView.builder()
+ .source(content)
+ .scroll(detailScroll)
+ .block(block)
+ .styles(Theme.chatMarkdownStyles())
+ .syntaxTheme(Theme.syntaxTheme())
+ .build(), area);
+ return;
+ }
List<Line> lines = new ArrayList<>();
String detail = entry.detail();
if (detail != null && !detail.isBlank()) {
- if (entry.level() == AiPanel.LogLevel.TOOL || entry.level() ==
AiPanel.LogLevel.RESULT) {
- lines.add(Line.from(Span.styled(
- entry.level() == AiPanel.LogLevel.TOOL
- ? TuiIcons.ARROW_RIGHT + " Arguments"
- : TuiIcons.ARROW_LEFT + " Result",
- (entry.level() == AiPanel.LogLevel.TOOL ?
Theme.warning() : Theme.success()).bold())));
- addJsonLines(lines, detail);
- } else {
- lines.add(Line.from(Span.styled(TuiIcons.ARROW_RIGHT + "
Content",
- Style.EMPTY.fg(Theme.accent()).bold())));
- for (String line : detail.split("\n", -1)) {
- lines.add(Line.from(Span.styled(" " + line,
Style.EMPTY.dim())));
- }
- }
+ // a tool call or its result (an answer or a prompt is shown as
Markdown above)
+ lines.add(Line.from(Span.styled(
+ entry.level() == AiPanel.LogLevel.TOOL
+ ? TuiIcons.ARROW_RIGHT + " Arguments"
+ : TuiIcons.ARROW_LEFT + " Result",
+ (entry.level() == AiPanel.LogLevel.TOOL ? Theme.warning()
: Theme.success()).bold())));
+ addJsonLines(lines, detail);
} else {
lines.add(Line.from(Span.styled("(no detail data)",
Style.EMPTY.dim())));
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureView.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureView.java
index 7b5dad3d9d70..05683cc87bc3 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureView.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureView.java
@@ -550,7 +550,7 @@ final class ArchitectureView {
List<Line> lines = GroupPreview.lines(g, capabilities, overview, w -
2, h - 2, r -> RouteKeys.display(dir, r),
scheme -> ProjectOverview.isRemote(scheme, catalog()));
h = Math.min(h, lines.size() + 2);
- Rect rect = new Rect(area.x() + area.width() - w - 2, area.y() +
area.height() - h - 1, w, h);
+ Rect rect = freeCorner(frame, area, w, h);
frame.renderWidget(Clear.INSTANCE, rect);
// how messages flow through the group, as the summary's flows between
routes: the group's name is on its box
Block block =
Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
@@ -560,6 +560,38 @@ final class ArchitectureView {
frame.renderWidget(Paragraph.builder().text(Text.from(lines)).build(),
block.inner(rect));
}
+ /**
+ * Where the Flow panel goes: the first corner of the diagram (bottom
right, bottom left, top right, top left) that
+ * the diagram leaves blank, so it does not hide a box; bottom right when
none is.
+ */
+ static Rect freeCorner(Frame frame, Rect area, int w, int h) {
+ int left = area.x() + 2;
+ int right = area.x() + area.width() - w - 2;
+ int top = area.y() + 1;
+ int bottom = area.y() + area.height() - h - 1;
+ Rect[] corners = {
+ new Rect(right, bottom, w, h), new Rect(left, bottom, w, h),
+ new Rect(right, top, w, h), new Rect(left, top, w, h) };
+ for (Rect r : corners) {
+ if (isBlank(frame, r)) {
+ return r;
+ }
+ }
+ return corners[0];
+ }
+
+ private static boolean isBlank(Frame frame, Rect r) {
+ for (int y = r.y(); y < r.y() + r.height(); y++) {
+ for (int x = r.x(); x < r.x() + r.width(); x++) {
+ String symbol = frame.buffer().get(x, y).symbol();
+ if (symbol != null && !symbol.isBlank()) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
private Line title(String integrationName) {
List<Span> spans = new ArrayList<>();
spans.add(Span.raw(" Architecture"));
@@ -657,7 +689,7 @@ final class ArchitectureView {
case ProjectCapabilities.UTILITY -> g.ai() ? "plumbing, partly " +
IntegrationSummary.AI_MARK : "plumbing";
case SYSTEM_IN -> "external: messages come in";
case SYSTEM_OUT -> "external: messages go out";
- default -> "not grouped yet: /overview groups it";
+ default -> "not grouped yet: the AI project overview groups the
routes (F8, then /overview)";
};
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AutocompletePopup.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AutocompletePopup.java
index e95fc700a6f8..ca317a5808ce 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AutocompletePopup.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AutocompletePopup.java
@@ -101,6 +101,7 @@ class AutocompletePopup {
// the row of the filtered list a divider is drawn above, -1 for none: the
properties of a Kamelet above it, the
// options of the kamelet component itself, for special uses only, below
it (CAMEL-25411)
private int dividerAt = -1;
+ private String dividerLabel = KAMELET_COMPONENT_OPTIONS;
static final String KAMELET_GROUP = "kamelet ";
static final String KAMELET_COMPONENT_OPTIONS = "kamelet component
options";
@@ -328,7 +329,7 @@ class AutocompletePopup {
for (int row = 0; row < filteredItems.size(); row++) {
CompletionItem ci = filteredItems.get(row);
if (row == dividerAt) {
- String text = "── " + KAMELET_COMPONENT_OPTIONS + " ";
+ String text = "── " + dividerLabel + " ";
items.add(ListItem.from(Line.from(Span.styled(
text + "─".repeat(Math.max(1, listRect.width() - 2 -
text.length())), dimStyle))));
}
@@ -578,6 +579,7 @@ class AutocompletePopup {
filteredItems.sort(Comparator.comparingInt(item ->
rank(item.key(), f)));
}
dividerAt = -1;
+ dividerLabel = KAMELET_COMPONENT_OPTIONS;
boolean kamelet =
filteredItems.stream().anyMatch(AutocompletePopup::isKameletProperty);
if (kamelet &&
!filteredItems.stream().allMatch(AutocompletePopup::isKameletProperty)) {
// the properties of the Kamelet stay above the divider while
filtering (the sort is stable)
@@ -588,6 +590,18 @@ class AutocompletePopup {
break;
}
}
+ } else if
(filteredItems.stream().anyMatch(AutocompletePopup::isAdvanced)
+ &&
!filteredItems.stream().allMatch(AutocompletePopup::isAdvanced)) {
+ // the common options first, the advanced ones (the catalog's
"advanced" groups) below a divider, as an
+ // alphabetical list put bridgeErrorHandler and exceptionHandler
before delay and period (CAMEL-25426)
+
filteredItems.sort(Comparator.comparing(AutocompletePopup::isAdvanced));
+ dividerLabel = "advanced";
+ for (int i = 0; i < filteredItems.size(); i++) {
+ if (isAdvanced(filteredItems.get(i))) {
+ dividerAt = i;
+ break;
+ }
+ }
}
listState.select(filteredItems.isEmpty() ? null : 0);
}
@@ -597,6 +611,11 @@ class AutocompletePopup {
* options of the kamelet component below the divider are for special
uses, and not what the title names
* (CAMEL-25411).
*/
+ /** An option of an advanced group of the catalog: "advanced", "consumer
(advanced)", "producer (advanced)". */
+ static boolean isAdvanced(CompletionItem item) {
+ return item.group() != null && item.group().contains("advanced");
+ }
+
String title() {
String label = titlePrefix != null ? titlePrefix : "Completions";
boolean kamelet =
allItems.stream().anyMatch(AutocompletePopup::isKameletProperty);
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DialogHelper.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DialogHelper.java
index d645a2ac92a7..565d2f38ae88 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DialogHelper.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DialogHelper.java
@@ -136,12 +136,19 @@ final class DialogHelper {
*/
static Rect renderConfirm(
Frame frame, Rect area, String title, String message, String
detail, String extraKey, String extraLabel) {
+ return renderConfirm(frame, area, title, message, detail, "Enter",
"confirm", extraKey, extraLabel);
+ }
+
+ /** As above, accepted with the given key (a destructive choice takes "y",
not Enter). */
+ static Rect renderConfirm(
+ Frame frame, Rect area, String title, String message, String
detail, String acceptKey, String acceptLabel,
+ String extraKey, String extraLabel) {
Style accent = Theme.warning();
String titleText = " " + title + " ";
String msg = message.trim();
Line hints = extraKey != null
- ? TuiHelper.hintLine("Enter", "confirm", extraKey, extraLabel,
"Esc", "cancel")
- : TuiHelper.hintLine("Enter", "confirm", "Esc", "cancel");
+ ? TuiHelper.hintLine(acceptKey, acceptLabel, extraKey,
extraLabel, "Esc", "cancel")
+ : TuiHelper.hintLine(acceptKey, acceptLabel, "Esc", "cancel");
int contentW = Math.max(msg.length(), Math.max(detail != null ?
detail.length() : 0, hints.width()));
int popupW = clampWidth(area, CONFIRM_MIN_WIDTH, Math.max(contentW +
6, titleText.length() + 4));
Rect popup = centered(area, popupW, CONFIRM_HEIGHT + (detail != null ?
1 : 0));
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DoctorPopup.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DoctorPopup.java
index bb33273a5362..910a3beb4dab 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DoctorPopup.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DoctorPopup.java
@@ -393,7 +393,8 @@ class DoctorPopup {
Span.raw(" " + icon)));
String models = OllamaDoctorSupport.formatModels(status.models());
result.add(Line.from(Span.styled(
- " models: " +
TuiHelper.truncate(models, 34),
+ // as wide as the rows above it, which end with their
status
+ " models: " +
TuiHelper.truncate(models, 22),
Style.EMPTY.dim())));
if (allSmall) {
result.add(Line.from(Span.styled(
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
index 136a02e2b69d..4b64ba726e07 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
@@ -249,7 +249,7 @@ class EndpointsTab extends AbstractTableTab {
cells.add(Cell.from(Span.styled(ep.component != null ?
ep.component : "", Style.EMPTY.fg(Theme.accent()))));
cells.add(Cell.from(ep.routeId != null ? ep.routeId : ""));
cells.add(Cell.from(Span.styled(arrow + dir, dirStyle)));
- cells.add(rightCell(ep.hits > 0 ? String.valueOf(ep.hits) : "",
8));
+ cells.add(rightCell(String.valueOf(ep.hits), 8));
if (hasSize) {
cells.add(rightCell(FlowHelper.sizeToString(ep.meanBodySize),
10));
cells.add(rightCell(FlowHelper.sizeToString(ep.meanHeadersSize), 10));
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ExampleBrowserPopup.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ExampleBrowserPopup.java
index 0ef2bf139cfe..30c36e9818df 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ExampleBrowserPopup.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ExampleBrowserPopup.java
@@ -221,8 +221,10 @@ class ExampleBrowserPopup {
}
// wide: the descriptions are sentences, and a wide terminal should
show them on one or two lines
int popupW = Math.max(80, Math.min(area.width() - 10, 170));
- int visibleItems = Math.max(10, catalog.size() + 10);
- int popupH = Math.min(visibleItems, Math.min(22, area.height() - 4));
+ List<ListItem> items = buildListItems(popupW - 4);
+ // as tall as the lines of the list (and its borders), so a short list
leaves no empty block below it
+ int lines = items.stream().mapToInt(ListItem::height).sum() + 2;
+ int popupH = Math.min(lines, Math.min(22, area.height() - 4));
int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
int y = area.top() + 2;
Rect popup = new Rect(x, y, Math.min(popupW, area.width()),
Math.min(popupH, area.height() - 2));
@@ -230,7 +232,6 @@ class ExampleBrowserPopup {
frame.renderWidget(Clear.INSTANCE, popup);
- List<ListItem> items = buildListItems(popupW - 4);
String title = currentFolder != null
? " " + ExampleHelper.getGroupTitle(currentFolder) + " (" +
folderExampleCount(currentFolder) + ") "
: " Run an Example (" + catalog.size() + ") ";
@@ -256,7 +257,7 @@ class ExampleBrowserPopup {
TuiHelper.hintLast(spans, "Esc", "back");
} else {
TuiHelper.hint(spans, "Enter/→", "open");
- TuiHelper.hint(spans, "1-9", "jump");
+ TuiHelper.hint(spans, "0-9", "jump");
TuiHelper.hint(spans, "d", "docs");
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/FileActionsPopup.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FileActionsPopup.java
index b34d154b5d09..6161cb4538b4 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FileActionsPopup.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FileActionsPopup.java
@@ -72,6 +72,10 @@ class FileActionsPopup {
private boolean visible;
private Phase phase = Phase.MENU;
+ // what the confirm step is for: DELETE, or a conversion (replace the
original, or keep both)
+ private Action confirmAction;
+ static final String REPLACE = "replace";
+ static final String KEEP = "keep";
private String targetName;
private boolean hasTarget;
@@ -191,8 +195,16 @@ class FileActionsPopup {
case NEW_FOLDER -> startInput(action, "New folder", "");
case RENAME -> startInput(action, "Rename", targetName);
case DUPLICATE -> startInput(action, "Duplicate",
SourceFileOps.suggestDuplicateName(targetName));
- case DELETE -> phase = Phase.CONFIRM;
- case COPY_PATH, CONVERT_YAML, CONVERT_XML, CONVERT_JAVA -> {
+ case DELETE -> {
+ confirmAction = action;
+ phase = Phase.CONFIRM;
+ }
+ // the converted file has the same routes: replace the original,
or keep both (and rename one later)
+ case CONVERT_YAML, CONVERT_XML, CONVERT_JAVA -> {
+ confirmAction = action;
+ phase = Phase.CONFIRM;
+ }
+ case COPY_PATH -> {
result = new Request(action, null);
close();
}
@@ -246,8 +258,13 @@ class FileActionsPopup {
private boolean handleConfirmKey(KeyEvent ke) {
// Delete is a destructive action: only an explicit "y" confirms it.
Enter must NOT delete. Esc (or "n")
// returns to the menu; any other key is swallowed so a stray
keystroke neither deletes nor dismisses.
+ if (confirmAction != Action.DELETE && ke.code() == KeyCode.CHAR &&
"k".equalsIgnoreCase(ke.string())) {
+ result = new Request(confirmAction, KEEP);
+ close();
+ return true;
+ }
if (ke.code() == KeyCode.CHAR && "y".equalsIgnoreCase(ke.string())) {
- result = new Request(Action.DELETE, null);
+ result = new Request(confirmAction, confirmAction == Action.DELETE
? null : REPLACE);
close();
return true;
}
@@ -314,6 +331,13 @@ class FileActionsPopup {
private void renderConfirm(Frame frame, Rect area) {
// Deleting a file is irreversible, so this dialog is error-styled and
deliberately accepts "y" only.
+ if (confirmAction != Action.DELETE) {
+ String dsl = confirmAction == Action.CONVERT_XML ? "XML" :
confirmAction == Action.CONVERT_YAML ? "YAML" : "Java";
+ this.popupRect = DialogHelper.renderConfirm(frame, area,
TuiIcons.CONVERT + " Convert to " + dsl,
+ "Replace " + targetName + " with the " + dsl + " file?",
+ "Keeping both leaves the same routes twice in the folder",
"y", "replace", "k", "keep both");
+ return;
+ }
this.popupRect = DialogHelper.renderConfirm(frame, area,
TuiIcons.DELETE + " Delete file?",
"Delete " + targetName + "?", true, "y", "delete");
}
@@ -332,7 +356,12 @@ class FileActionsPopup {
TuiHelper.hintLast(spans, "Esc", "back");
}
case CONFIRM -> {
- TuiHelper.hint(spans, "y", "delete");
+ if (confirmAction != Action.DELETE) {
+ TuiHelper.hint(spans, "y", "replace");
+ TuiHelper.hint(spans, "k", "keep both");
+ } else {
+ TuiHelper.hint(spans, "y", "delete");
+ }
TuiHelper.hintLast(spans, "Esc", "cancel");
}
default -> {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
index eaea39b466ff..3994e706f4a8 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
@@ -1357,7 +1357,8 @@ class HistoryTab extends AbstractTab {
String changes = computeTraceChanges(prev, entry);
rows.add(buildStepRow(i + 1, entry.inlineDepth,
entry.direction, entry.first, entry.last, entry.failed,
- entry.timestamp, entry.routeId, entry.nodeId,
entry.processor, desc, entry.elapsed, changes));
+ entry.timestamp, entry.routeId, entry.nodeId,
entry.processor, desc, entry.elapsed, changes,
+ area.width()));
}
String stepTitle
@@ -1367,7 +1368,8 @@ class HistoryTab extends AbstractTab {
Style tableHighlight = showFocus && detailFocused ?
Theme.selectionBg().dim() : Theme.selectionBg();
lastTraceStepArea = chunks.get(0);
detailSplit.setBorderPos(chunks.get(1).y());
- Table stepTable = buildStepTable(rows, stepTitle, showDescription,
tableBorderStyle, tableHighlight);
+ Table stepTable = buildStepTable(rows, stepTitle, showDescription,
tableBorderStyle, tableHighlight,
+ idColumnWidth(area.width()));
traceStepTableState.scrollToSelected(stepTable.viewportHeight(chunks.get(0)),
rows);
frame.renderStatefulWidget(stepTable, chunks.get(0),
traceStepTableState);
renderTableScrollbar(frame, lastTraceStepArea, stepTable,
traceStepTableState, traceStepScrollState,
@@ -1690,7 +1692,8 @@ class HistoryTab extends AbstractTab {
String changes = computeHistoryChanges(prev, entry);
rows.add(buildStepRow(i + 1, entry.inlineDepth,
entry.direction, entry.first, entry.last, entry.failed,
- entry.timestamp, entry.routeId, entry.nodeId,
entry.processor, desc, entry.elapsed, changes));
+ entry.timestamp, entry.routeId, entry.nodeId,
entry.processor, desc, entry.elapsed, changes,
+ area.width()));
}
Title historyTitle = buildHistoryTitle(current);
@@ -1699,7 +1702,8 @@ class HistoryTab extends AbstractTab {
Style tableHighlight = showFocus && detailFocused ?
Theme.selectionBg().dim() : Theme.selectionBg();
lastHistoryTableArea = chunks.get(0);
vSplit.setBorderPos(chunks.get(1).y());
- Table historyTable = buildStepTable(rows, historyTitle,
showDescription, tableBorderStyle, tableHighlight);
+ Table historyTable = buildStepTable(rows, historyTitle,
showDescription, tableBorderStyle, tableHighlight,
+ idColumnWidth(area.width()));
historyTableState.scrollToSelected(historyTable.viewportHeight(chunks.get(0)),
rows);
frame.renderStatefulWidget(historyTable, chunks.get(0),
historyTableState);
renderTableScrollbar(frame, lastHistoryTableArea, historyTable,
historyTableState, historyTableScrollState,
@@ -2055,11 +2059,23 @@ class HistoryTab extends AbstractTab {
return sortStyle(column, traceSort);
}
+ /** ROUTE and ID give way on a narrow screen, so the processor keeps room.
*/
+ static int idColumnWidth(int areaWidth) {
+ return areaWidth < 130 ? 16 : 25;
+ }
+
+ /** The width the PROCESSOR column gets: the borders, ">> ", 7 gaps and
the other columns. */
+ static int processorWidth(int areaWidth) {
+ // 2 of slack: the table sits a little inside the area of the tab
+ return Math.max(0, areaWidth - 2 - 3 - 7 - 2 - (3 + 4 + 12 + 2 *
idColumnWidth(areaWidth) + 4 + 11));
+ }
+
private static Row buildStepRow(
int stepNumber, int inlineDepth,
String direction, boolean first, boolean last, boolean failed,
String timestamp, String routeId, String nodeId, String processor,
- String description, long elapsed, String changes) {
+ String description, long elapsed, String changes, int areaWidth) {
+ int idWidth = idColumnWidth(areaWidth);
Style dirStyle;
if (first || last || !direction.isBlank()) {
dirStyle = failed ? Theme.error() : Theme.success();
@@ -2074,9 +2090,11 @@ class HistoryTab extends AbstractTab {
rightCell(String.valueOf(stepNumber), 3),
Cell.from(Span.styled(direction, dirStyle)),
Cell.from(timestamp != null ? TuiHelper.truncate(timestamp,
12) : ""),
- Cell.from(Span.styled(routeId != null ?
TuiHelper.truncate(routeId, 25) : "", Style.EMPTY.fg(Theme.accent()))),
- Cell.from(indent + (nodeId != null ?
TuiHelper.truncate(nodeId, 25) : "")),
- Cell.from(indent + display),
+ Cell.from(Span.styled(routeId != null ?
TuiHelper.truncate(routeId, idWidth) : "",
+ Style.EMPTY.fg(Theme.accent()))),
+ Cell.from(TuiHelper.truncate(indent + (nodeId != null ? nodeId
: ""), idWidth)),
+ // a long processor ends with an ellipsis where it is cut
+ Cell.from(TuiHelper.truncate(indent + display,
processorWidth(areaWidth))),
Cell.from(Line.from(changeSpans)),
rightCell(elapsedStr, 10));
}
@@ -2122,12 +2140,12 @@ class HistoryTab extends AbstractTab {
}
private static Table buildStepTable(List<Row> rows, Object title, boolean
descriptionMode) {
- return buildStepTable(rows, title, descriptionMode, Style.EMPTY,
Theme.selectionBg());
+ return buildStepTable(rows, title, descriptionMode, Style.EMPTY,
Theme.selectionBg(), 25);
}
private static Table buildStepTable(
List<Row> rows, Object title, boolean descriptionMode,
- Style borderStyle, Style highlightStyle) {
+ Style borderStyle, Style highlightStyle, int idWidth) {
Row header = Row.from(
rightCell("#", 3, Style.EMPTY.bold()),
Cell.from(Span.styled("", Style.EMPTY.bold())),
@@ -2148,8 +2166,8 @@ class HistoryTab extends AbstractTab {
Constraint.length(3),
Constraint.length(4),
Constraint.length(12),
- Constraint.length(25),
- Constraint.length(25),
+ Constraint.length(idWidth),
+ Constraint.length(idWidth),
Constraint.fill(),
Constraint.length(4),
Constraint.length(11))
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryLeakTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryLeakTab.java
index 24488ece1d90..c7b3d6835da1 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryLeakTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryLeakTab.java
@@ -520,7 +520,10 @@ class MemoryLeakTab extends AbstractTab {
Span.styled(formatDuration(entry.objectAge),
Style.EMPTY.fg(Theme.baseFg()))));
// Reference chain
- if (entry.referenceChain != null && !entry.referenceChain.isEmpty()) {
+ if (entry.referenceChain != null && entry.referenceChain.size() == 1) {
+ lines.add(Line.from(Span.raw("")));
+ lines.add(noChainLine());
+ } else if (entry.referenceChain != null &&
!entry.referenceChain.isEmpty()) {
lines.add(Line.from(Span.raw("")));
lines.add(Line.from(
Span.styled(" Reference Chain (Object → GC Root):",
Theme.muted())));
@@ -685,7 +688,10 @@ class MemoryLeakTab extends AbstractTab {
}
// reference chain from the entry
- if (entry.referenceChain != null && !entry.referenceChain.isEmpty()) {
+ if (entry.referenceChain != null && entry.referenceChain.size() == 1) {
+ lines.add(Line.from(Span.raw("")));
+ lines.add(noChainLine());
+ } else if (entry.referenceChain != null &&
!entry.referenceChain.isEmpty()) {
lines.add(Line.from(Span.raw("")));
lines.add(Line.from(
Span.styled(" Reference Chain (Object -> GC Root):",
Theme.muted())));
@@ -841,6 +847,16 @@ class MemoryLeakTab extends AbstractTab {
return key;
}
+ /**
+ * The chain of a sample that has only the object itself: JFR records the
path to a GC root only when the recording
+ * asks for it (the cutoff of jdk.OldObjectSample), so there is no chain
to show, and the heading would promise one.
+ */
+ private static Line noChainLine() {
+ return Line.from(Span.styled(" Reference chain: not recorded (JFR
records the path to a GC root only with its "
+ + "cutoff setting)",
+ Theme.muted()));
+ }
+
private static ChainLink holderFromChain(List<ChainLink> chain) {
if (chain == null || chain.size() < 2) {
return null;
@@ -962,7 +978,10 @@ class MemoryLeakTab extends AbstractTab {
// reference chain from the largest member
SampleEntry representative = sorted.get(0);
- if (representative.referenceChain != null &&
!representative.referenceChain.isEmpty()) {
+ if (representative.referenceChain != null &&
representative.referenceChain.size() == 1) {
+ lines.add(Line.from(Span.raw("")));
+ lines.add(noChainLine());
+ } else if (representative.referenceChain != null &&
!representative.referenceChain.isEmpty()) {
lines.add(Line.from(Span.raw("")));
lines.add(Line.from(
Span.styled(" Reference Chain (Object → GC Root):",
Theme.muted())));
@@ -1123,7 +1142,10 @@ class MemoryLeakTab extends AbstractTab {
// reference chain from the largest member
ComparisonEntry representative = sorted.get(0);
- if (representative.referenceChain != null &&
!representative.referenceChain.isEmpty()) {
+ if (representative.referenceChain != null &&
representative.referenceChain.size() == 1) {
+ lines.add(Line.from(Span.raw("")));
+ lines.add(noChainLine());
+ } else if (representative.referenceChain != null &&
!representative.referenceChain.isEmpty()) {
lines.add(Line.from(Span.raw("")));
lines.add(Line.from(
Span.styled(" Reference Chain (Object → GC Root):",
Theme.muted())));
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OllamaTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OllamaTab.java
index 4b8de6ce2a8d..1e5878154455 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OllamaTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OllamaTab.java
@@ -29,6 +29,7 @@ import java.util.Set;
import dev.tamboui.layout.Constraint;
import dev.tamboui.layout.Layout;
import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Overflow;
import dev.tamboui.style.Style;
import dev.tamboui.terminal.Frame;
import dev.tamboui.text.Line;
@@ -436,7 +437,9 @@ class OllamaTab extends AbstractTab {
lines.add(trend);
}
}
-
frame.renderWidget(Paragraph.builder().text(Text.from(lines)).block(block).build(),
area);
+ // wrap: the panel is narrow, and the sentences under the gauge are
cut otherwise ("once the runner is foun")
+
frame.renderWidget(Paragraph.builder().text(Text.from(lines)).overflow(Overflow.WRAP_WORD).block(block).build(),
+ area);
}
/**
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
index 82cfb767fafa..f7d566650535 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
@@ -1076,12 +1076,12 @@ class OverviewTab extends AbstractTab {
lines.add(Line.from(Span.styled("Load (1m/5m/15m):", dim)));
if (cpu != null) {
lines.add(Line.from(
- Span.styled("CPU: ", dim),
+ Span.styled("CPU: ", dim),
Span.raw(cpu.format("%.1f / %.1f / %.1f %%"))));
}
if (hasInfl) {
lines.add(Line.from(
- Span.styled("Infl: ", dim),
+ Span.styled("Inflight: ", dim),
Span.raw(sel.inflightLoad01 + " / " +
sel.inflightLoad05 + " / " + sel.inflightLoad15)));
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessTab.java
index 6ca0e4ebb8c1..0c38915f7304 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessTab.java
@@ -55,7 +55,12 @@ class ProcessTab extends AbstractTab {
@Override
public String description() {
- return "OS process information (PID, CPU, memory, file descriptors)";
+ return "The OS process: PID, user, Camel and Java versions, directory,
uptime and command line";
+ }
+
+ @Override
+ public String getHelpText() {
+ return DocHelper.loadHelpText("process");
}
@Override
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanel.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanel.java
index 907b0ee7f6b9..09535e3eeb13 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanel.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanel.java
@@ -48,6 +48,7 @@ import dev.tamboui.widgets.paragraph.Paragraph;
import dev.tamboui.widgets.scrollbar.Scrollbar;
import dev.tamboui.widgets.scrollbar.ScrollbarState;
import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.commands.CamelShellCommandRegistry;
import org.apache.camel.dsl.jbang.core.common.EnvironmentHelper;
import org.apache.camel.dsl.jbang.core.common.Printer;
import org.jline.builtins.InteractiveCommandGroup;
@@ -488,13 +489,7 @@ class ShellPanel {
private void runShell(LineDisciplineTerminal terminal, CommandLine
commandLine) {
try {
- // TODO: replace with new PicocliCommandRegistry(commandLine,
"Camel") when JLine merges #1947
- PicocliCommandRegistry registry = new
PicocliCommandRegistry(commandLine) {
- @Override
- public String name() {
- return "Camel";
- }
- };
+ PicocliCommandRegistry registry = new
CamelShellCommandRegistry(commandLine);
// Redirect command output (printer()) through the virtual terminal
// so it renders in the shell panel instead of the TUI's real
terminal
CamelJBangMain main = (CamelJBangMain) commandLine.getCommand();
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditHistory.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditHistory.java
index fac4bf77511b..ef44d5249589 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditHistory.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditHistory.java
@@ -55,9 +55,11 @@ final class SourceEditHistory {
if (undo.size() <= 1) {
return false;
}
+ // the snapshot taken just before the latest change: restoring the one
under it undid two changes at once,
+ // and put the cursor where the one before them was (line 1 for the
first)
+ Snapshot before = undo.pop();
redo.push(capture(state));
- undo.pop();
- restore(state, undo.peek());
+ restore(state, before);
return true;
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
index 5e9f1d98d82f..7bb5b0a2057c 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
@@ -811,9 +811,9 @@ class SourceTab extends AbstractTab {
TuiHelper.copyToClipboard(entry.path());
notify("Copied path to clipboard", false);
}
- case CONVERT_YAML -> convert(entry, "yaml");
- case CONVERT_XML -> convert(entry, "xml");
- case CONVERT_JAVA -> convert(entry, "java");
+ case CONVERT_YAML -> convert(entry, "yaml",
FileActionsPopup.REPLACE.equals(req.name()));
+ case CONVERT_XML -> convert(entry, "xml",
FileActionsPopup.REPLACE.equals(req.name()));
+ case CONVERT_JAVA -> convert(entry, "java",
FileActionsPopup.REPLACE.equals(req.name()));
}
} catch (Exception e) {
notify(e.getMessage() != null ? e.getMessage() : e.toString(),
true);
@@ -830,7 +830,7 @@ class SourceTab extends AbstractTab {
* Converts the route file to another DSL without running it
(CAMEL-25254), into a new file next to it which opens;
* what does not carry over is said at the top of the new file. An
existing file is not overwritten.
*/
- private void convert(FilesBrowser.FileEntry entry, String format) throws
IOException {
+ private void convert(FilesBrowser.FileEntry entry, String format, boolean
replace) throws IOException {
if (entry == null) {
return;
}
@@ -845,10 +845,21 @@ class SourceTab extends AbstractTab {
return;
}
Files.writeString(target, RouteDslConverter.withNotes(r.content(),
r.notes(), format), StandardCharsets.UTF_8);
+ if (replace) {
+ // the converted file takes the place of the original, so the
routes are not defined twice (CAMEL-25426)
+ SourceFileOps.delete(Path.of(entry.path()));
+ }
if (loadDirectory(currentDir, r.fileName())) {
openSelectedEntry();
}
- notify("Converted to " + r.fileName() + (r.notes().isEmpty() ? "" : ",
see the notes at its top"), false);
+ String notes = r.notes().isEmpty() ? "" : ", see the notes at its top";
+ if (replace) {
+ notify("Replaced " + entry.name() + " with " + r.fileName() +
notes, false);
+ } else {
+ // both stay, so the folder has the same routes twice: the next
run fails with duplicate route ids
+ notify("Converted to " + r.fileName() + notes + ". " + entry.name()
+ + " defines the same routes: delete or rename one of them
before the next run", true);
+ }
}
private void openSelectedEntry() {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
index 403a12dd88af..f0aab161f5df 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
@@ -625,6 +625,11 @@ class SourceViewer {
return dirty;
}
+ /** Whether the text differs from what was last loaded or saved. */
+ private boolean changedSinceSave() {
+ return originalEditText == null ||
!originalEditText.equals(editState.text());
+ }
+
/** Package-private for tests that drive the edit buffer directly. */
TextAreaState editState() {
return editState;
@@ -969,7 +974,15 @@ class SourceViewer {
private boolean handleEditKeyEvent(KeyEvent ke) {
if (validationErrors != null) {
- if (ke.isCancel() || ke.isKey(KeyCode.ENTER)) {
+ if (ke.isKey(KeyCode.F9) && ke.hasShift()) {
+ // the fix the popup offers: go to its line and apply it, as
Shift+F9 does there
+ int row = popupFixRow();
+ if (row >= 0) {
+ validationErrors = null;
+ SourceEditorNavigation.positionCursor(editState, row, 0);
+ applyQuickFix();
+ }
+ } else if (ke.isCancel() || ke.isKey(KeyCode.ENTER)) {
validationErrors = null;
} else if (ke.isUp()) {
validationErrorScroll = Math.max(0, validationErrorScroll - 1);
@@ -1026,7 +1039,8 @@ class SourceViewer {
}
if (ke.hasCtrl() && ke.isCharIgnoreCase('z') && !ke.hasShift()) {
if (editHistory.undo(editState)) {
- dirty = true;
+ // undone back to the saved text: not modified any more
+ dirty = changedSinceSave();
lineStatuses = null;
refreshEditFindMatches();
}
@@ -1034,7 +1048,7 @@ class SourceViewer {
}
if (ke.hasCtrl() && (ke.isCharIgnoreCase('y') ||
(ke.isCharIgnoreCase('z') && ke.hasShift()))) {
if (editHistory.redo(editState)) {
- dirty = true;
+ dirty = changedSinceSave();
lineStatuses = null;
refreshEditFindMatches();
}
@@ -2031,6 +2045,7 @@ class SourceViewer {
}
Files.writeString(editableFile, content, StandardCharsets.UTF_8);
dirty = false;
+ originalEditText = content;
Path path = editableFile;
boolean restoreMarkdownMode = markdownModeBeforeEdit;
int cursorRow = editState.cursorRow();
@@ -2140,6 +2155,18 @@ class SourceViewer {
}
}
+ /** The first line with a problem that has a fix, for the popup of a save
it blocked; -1 for none. */
+ private int popupFixRow() {
+ for (Map.Entry<Integer, String> e : new
java.util.TreeMap<>(visibleInlineErrors()).entrySet()) {
+ int row = e.getKey();
+ if (row >= 0 && row < editState.lineCount()
+ && QuickFixes.fixFor(e.getValue(), editState.getLine(row))
!= null) {
+ return row;
+ }
+ }
+ return -1;
+ }
+
/** The fix of the problem on the line of the cursor, when the problem
says it; null otherwise. */
private QuickFixes.Fix cursorFix() {
int row = editState.cursorRow();
@@ -3119,7 +3146,15 @@ class SourceViewer {
wrapText(msg, innerW, allLines);
}
allLines.add(Line.empty());
- allLines.add(TuiHelper.hintLine("Esc", "close"));
+ int fixRow = popupFixRow();
+ if (fixRow >= 0) {
+ // the fix the editor knows, offered here too: the panel at the
bottom says it only after the popup
+ QuickFixes.Fix fix =
QuickFixes.fixFor(visibleInlineErrors().get(fixRow), editState.getLine(fixRow));
+ allLines.add(TuiHelper.hintLine("Shift+F9", "fix line " + (fixRow
+ 1) + ": " + fix.label(),
+ "Esc", "close"));
+ } else {
+ allLines.add(TuiHelper.hintLine("Esc", "close"));
+ }
int contentH = allLines.size();
int popupH = Math.min(contentH + 2, area.height() - 4);
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolDefinitions.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolDefinitions.java
index 10b3675274d5..465f0a6271b4 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolDefinitions.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolDefinitions.java
@@ -287,7 +287,7 @@ final class TuiToolDefinitions {
"Invokes a TUI action by name or by its F2 menu label (as
listed in tui_get_options actions), "
+ "bypassing fragile key sequences. "
+ "Names: reset-stats, reset-screen, screenshot,
show-keystrokes, "
- + "tape-recording, doctor, caption, mcp-info,
mcp-log, toggle-theme.",
+ + "tape-recording, doctor, caption, mcp-info,
mcp-log, next-theme.",
Map.of("action", propDef("string",
"Action name in kebab-case (e.g. 'reset-stats') or
menu label (e.g. 'Run Doctor')")),
List.of("action"))));
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
index 0e8d17252930..a11d076ce17d 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
@@ -1084,7 +1084,7 @@ class TuiToolRegistry {
}
return "Unknown or unsupported action: " + action
+ ". Use a name (reset-stats, reset-screen, screenshot,
show-keystrokes, "
- + "tape-recording, doctor, caption, mcp-info, mcp-log,
toggle-theme) "
+ + "tape-recording, doctor, caption, mcp-info, mcp-log,
next-theme) "
+ "or a menu label from tui_get_options actions";
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/process.md
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/process.md
new file mode 100644
index 000000000000..92c971ce6fca
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/process.md
@@ -0,0 +1,28 @@
+# Process
+
+The Process tab shows the operating system process the selected
+integration runs in: who runs it, which Camel and Java it runs on, where,
+for how long, and the full command line that started it.
+
+This is useful to check which JVM, Camel version or profile an app really
+runs with, or to copy its command line to run it again by hand.
+
+## Fields
+
+- **PID** — The process id
+- **User** — The operating system user that runs the process
+- **Name** — The name of the integration
+- **Camel** — The Camel version
+- **Platform** — The runtime (Camel Main, Spring Boot, Quarkus) and its version
+- **Profile** — The profile it runs with (dev, prod, ...)
+- **Java** — The Java version, vendor and VM
+- **Directory** — The working directory of the process
+- **Uptime** — How long it has been running
+
+Below the fields, **Command Line** shows the command that started the
+process: the java executable, the JVM options and the arguments.
+
+## Keys
+
+- **w** — Wrap the command line, or show one argument per line
+- **PgUp / PgDn** — Scroll
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureFlowPanelTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureFlowPanelTest.java
new file mode 100644
index 000000000000..16a8d88bbb1e
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureFlowPanelTest.java
@@ -0,0 +1,50 @@
+/*
+ * 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 dev.tamboui.buffer.Buffer;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The Flow panel of the Architecture view goes where the diagram leaves room,
so it does not hide a capability box.
+ */
+class ArchitectureFlowPanelTest {
+
+ private final Rect area = new Rect(0, 0, 100, 30);
+
+ @Test
+ void theBottomRightIsUsedWhenItIsFree() {
+ Frame frame = Frame.forTesting(Buffer.empty(area));
+
+ assertThat(ArchitectureView.freeCorner(frame, area, 30,
8)).isEqualTo(new Rect(68, 21, 30, 8));
+ }
+
+ @Test
+ void aBoxInTheBottomRightMovesThePanelToAFreeCorner() {
+ Buffer buffer = Buffer.empty(area);
+ // a capability box in the bottom right
+ buffer.setString(75, 24, "Customer Notification", Style.EMPTY);
+ Frame frame = Frame.forTesting(buffer);
+
+ assertThat(ArchitectureView.freeCorner(frame, area, 30,
8)).isEqualTo(new Rect(2, 21, 30, 8));
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AutocompletePopupTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AutocompletePopupTest.java
index 20e1324c5831..32b1d91ae4aa 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AutocompletePopupTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AutocompletePopupTest.java
@@ -27,6 +27,29 @@ import static org.assertj.core.api.Assertions.assertThat;
class AutocompletePopupTest {
+ @Test
+ void theAdvancedOptionsComeAfterTheCommonOnes() {
+ var items = List.of(
+ new AutocompletePopup.CompletionItem(
+ "bridgeErrorHandler", "", "boolean", null, false, null,
+ "consumer (advanced)", false),
+ new AutocompletePopup.CompletionItem("delay", "", "integer",
null, false, null, "consumer", false),
+ new AutocompletePopup.CompletionItem(
+ "exchangePattern", "", "object", null, false, null,
+ "consumer (advanced)", false),
+ new AutocompletePopup.CompletionItem("period", "", "integer",
null, false, null, "consumer", false));
+ var popup = new AutocompletePopup(items, "", "");
+
+ // the first two are the common options, delay and period, in their
order; the advanced ones follow
+ popup.handleKeyEvent(KeyEvent.ofKey(KeyCode.DOWN, KeyModifiers.NONE));
+ popup.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE));
+ assertThat(popup.consumeSelectedItem().key()).isEqualTo("period");
+
+ var first = new AutocompletePopup(items, "", "");
+ first.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE));
+ assertThat(first.consumeSelectedItem().key()).isEqualTo("delay");
+ }
+
@Test
void escClosesPopup() {
var popup = new AutocompletePopup(sampleItems(), "", "");
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DoctorPopupOllamaTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DoctorPopupOllamaTest.java
index dbc20444463b..548311c1213c 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DoctorPopupOllamaTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DoctorPopupOllamaTest.java
@@ -69,7 +69,7 @@ class DoctorPopupOllamaTest {
DoctorPopup.addOllamaLines(lines, status);
assertThat(lineTexts(lines)).anyMatch(text -> text.contains("2
models"));
- assertThat(lineTexts(lines)).anyMatch(text -> text.contains("models:
qwen2.5:32b, llama3.2:latest"));
+ assertThat(lineTexts(lines)).anyMatch(text -> text.contains("models:
qwen2.5:32b, llama3"));
}
@Test
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceConvertTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceConvertTest.java
index d165ea03aaec..62a24cf73e30 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceConvertTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceConvertTest.java
@@ -33,6 +33,12 @@ class SourceConvertTest {
/** The action chosen after moving down the menu the given number of
times. */
private static FileActionsPopup.Action choose(String file, boolean
routeFile, int downs) {
+ FileActionsPopup.Request r = answer(file, routeFile, downs, 'y');
+ return r != null ? r.action() : null;
+ }
+
+ /** The request after choosing a menu entry and answering its confirm
step, if it has one, with the given key. */
+ private static FileActionsPopup.Request answer(String file, boolean
routeFile, int downs, char key) {
FileActionsPopup popup = new FileActionsPopup();
popup.open(file, true, routeFile);
for (int i = 0; i < downs; i++) {
@@ -40,7 +46,20 @@ class SourceConvertTest {
}
popup.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE));
FileActionsPopup.Request r = popup.consumeResult();
- return r != null ? r.action() : null;
+ if (r == null && popup.isVisible()) {
+ popup.handleKeyEvent(KeyEvent.ofChar(key, KeyModifiers.NONE));
+ r = popup.consumeResult();
+ }
+ return r;
+ }
+
+ @Test
+ void aConversionAsksWhetherToReplaceTheOriginal() {
+ // y replaces the original, so the routes are not defined twice; k
keeps both
+ assertThat(answer("orders.camel.yaml", true, 6, 'y'))
+ .isEqualTo(new
FileActionsPopup.Request(FileActionsPopup.Action.CONVERT_XML,
FileActionsPopup.REPLACE));
+ assertThat(answer("orders.camel.yaml", true, 6, 'k'))
+ .isEqualTo(new
FileActionsPopup.Request(FileActionsPopup.Action.CONVERT_XML,
FileActionsPopup.KEEP));
}
@Test
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditHistoryTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditHistoryTest.java
index f2d35b55ae7d..5dbb8028fe2b 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditHistoryTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditHistoryTest.java
@@ -46,6 +46,26 @@ class SourceEditHistoryTest {
assertThat(state.text()).isEqualTo("alpha\nbeta\n");
}
+ @Test
+ void undoTakesBackOneChangeAndPutsTheCursorWhereItWas() {
+ SourceEditorNavigation.positionCursor(state, 1, 4);
+ history.beforeChange(state);
+ state.insert('X');
+ history.beforeChange(state);
+ state.insert('Y');
+
+ assertThat(history.undo(state)).isTrue();
+ assertThat(state.getLine(1)).isEqualTo("betaX");
+ assertThat(history.undo(state)).isTrue();
+ assertThat(state.text()).isEqualTo("alpha\nbeta\n");
+ assertThat(state.cursorRow()).isEqualTo(1);
+ assertThat(state.cursorCol()).isEqualTo(4);
+ assertThat(history.undo(state)).isFalse();
+
+ assertThat(history.redo(state)).isTrue();
+ assertThat(state.getLine(1)).isEqualTo("betaX");
+ }
+
@Test
void redoReappliesUndoneChange() {
history.beforeChange(state);
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewerEditTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewerEditTest.java
index 5a0659bc0e0d..0d42c293e02d 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewerEditTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewerEditTest.java
@@ -102,6 +102,21 @@ class SourceViewerEditTest {
assertThat(viewer.isEditable()).isTrue();
}
+ @Test
+ void undoingEveryChangeLeavesTheFileUnmodified() {
+ viewer.loadFile(sourceFile);
+ viewer.enterEditMode();
+ viewer.handleKeyEvent(KeyEvent.ofChar('x', KeyModifiers.NONE));
+ assertThat(viewer.isDirty()).isTrue();
+
+ viewer.handleKeyEvent(KeyEvent.ofChar('z', KeyModifiers.CTRL));
+
+ assertThat(viewer.isDirty()).isFalse();
+ // redo brings the change back
+ viewer.handleKeyEvent(KeyEvent.ofChar('y', KeyModifiers.CTRL));
+ assertThat(viewer.isDirty()).isTrue();
+ }
+
@Test
void typingInEditModeDoesNotCloseViewer() {
viewer.loadFile(sourceFile);