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 f67ad68c22e2 CAMEL-23598: TUI screenshot action (Shift+F5) to capture
screen as ASCII art (#23408)
f67ad68c22e2 is described below
commit f67ad68c22e277b14b9103e25c161982c440d963
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu May 21 12:07:41 2026 +0200
CAMEL-23598: TUI screenshot action (Shift+F5) to capture screen as ASCII
art (#23408)
* CAMEL-23598: TUI screenshot action (Shift+F5) to capture screen as ASCII
art
Co-Authored-By: Claude <[email protected]>
* CAMEL-23598: Save screenshot in both plain text and ANSI color formats
Co-Authored-By: Claude <[email protected]>
* CAMEL-23598: Add screenshot to F2 actions menu
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* CAMEL-23598: Defer F2 screenshot until after dialog is closed
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* CAMEL-23598: Rename menu item to Take Screenshot
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* CAMEL-23595: Add hasCitrusTests metadata to example catalog
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude <[email protected]>
---
.../apache/camel/dsl/jbang/core/commands/Run.java | 8 +++-
.../camel/dsl/jbang/core/common/ExampleHelper.java | 5 ++
.../examples/camel-jbang-example-catalog.json | 21 +++++++++
.../dsl/jbang/core/common/ExampleHelperTest.java | 10 ++++
.../dsl/jbang/core/commands/tui/ActionsPopup.java | 28 ++++++++---
.../dsl/jbang/core/commands/tui/CamelMonitor.java | 54 +++++++++++++++++++++-
6 files changed, 117 insertions(+), 9 deletions(-)
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
index d0938d501d2e..f0b64906eedf 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
@@ -418,11 +418,17 @@ public class Run extends CamelCommand {
} else {
icons.append(" ");
}
+ if (ExampleHelper.hasCitrusTests(entry)) {
+ icons.append("๐งช");
+ } else {
+ icons.append(" ");
+ }
printer().printf(" %s %-30s %s%n", icons, eName, desc);
}
}
printer().println();
- printer().println(" ๐ฆ = bundled (works offline) ๐ = online (fetched
from GitHub) ๐ณ = requires Docker");
+ printer().println(
+ " ๐ฆ = bundled (works offline) ๐ = online (fetched from
GitHub) ๐ณ = requires Docker ๐งช = Citrus tests");
printer().println();
printer().println("Usage: camel run --example=<name>");
printer().println(" camel run --example=<name> --dev");
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ExampleHelper.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ExampleHelper.java
index 3ce60b684c1e..faf401146db3 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ExampleHelper.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ExampleHelper.java
@@ -124,6 +124,11 @@ public final class ExampleHelper {
return docker != null && docker;
}
+ public static boolean hasCitrusTests(JsonObject entry) {
+ Boolean citrus = entry.getBoolean("hasCitrusTests");
+ return citrus != null && citrus;
+ }
+
@SuppressWarnings("unchecked")
public static List<String> getFiles(JsonObject entry) {
Collection<String> files = (Collection<String>) entry.get("files");
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/camel-jbang-example-catalog.json
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/camel-jbang-example-catalog.json
index cdd5c70d7a03..a45a48e1027b 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/camel-jbang-example-catalog.json
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/camel-jbang-example-catalog.json
@@ -11,6 +11,7 @@
],
"bundled": false,
"requiresDocker": false,
+ "hasCitrusTests": false,
"files": [
"README.adoc",
"application.properties",
@@ -32,6 +33,7 @@
],
"bundled": false,
"requiresDocker": false,
+ "hasCitrusTests": true,
"files": [
"README.adoc",
"application.properties",
@@ -54,6 +56,7 @@
],
"bundled": false,
"requiresDocker": false,
+ "hasCitrusTests": true,
"files": [
"README.adoc",
"application.properties",
@@ -71,6 +74,7 @@
],
"bundled": true,
"requiresDocker": false,
+ "hasCitrusTests": false,
"files": [
"README.adoc",
"route.camel.yaml"
@@ -89,6 +93,7 @@
],
"bundled": true,
"requiresDocker": false,
+ "hasCitrusTests": false,
"files": [
"README.adoc",
"cron-log.camel.yaml"
@@ -107,6 +112,7 @@
],
"bundled": false,
"requiresDocker": true,
+ "hasCitrusTests": false,
"files": [
"README.adoc",
"application.properties",
@@ -128,6 +134,7 @@
],
"bundled": false,
"requiresDocker": false,
+ "hasCitrusTests": false,
"files": [
"README.adoc",
"application.properties",
@@ -150,6 +157,7 @@
],
"bundled": false,
"requiresDocker": true,
+ "hasCitrusTests": true,
"files": [
"README.adoc",
"application.properties",
@@ -170,6 +178,7 @@
],
"bundled": true,
"requiresDocker": false,
+ "hasCitrusTests": false,
"files": [
"README.adoc",
"application.properties",
@@ -189,6 +198,7 @@
],
"bundled": false,
"requiresDocker": false,
+ "hasCitrusTests": false,
"files": [
"README.adoc",
"application.properties",
@@ -208,6 +218,7 @@
],
"bundled": false,
"requiresDocker": false,
+ "hasCitrusTests": false,
"files": [
"README.adoc",
"application.properties",
@@ -226,6 +237,7 @@
],
"bundled": false,
"requiresDocker": true,
+ "hasCitrusTests": true,
"files": [
"README.adoc",
"application.properties",
@@ -248,6 +260,7 @@
],
"bundled": false,
"requiresDocker": false,
+ "hasCitrusTests": false,
"files": [
"README.adoc",
"application.properties",
@@ -267,6 +280,7 @@
],
"bundled": false,
"requiresDocker": false,
+ "hasCitrusTests": true,
"files": [
"README.adoc",
"application.properties",
@@ -287,6 +301,7 @@
],
"bundled": false,
"requiresDocker": false,
+ "hasCitrusTests": true,
"files": [
"README.adoc",
"application.properties",
@@ -307,6 +322,7 @@
],
"bundled": true,
"requiresDocker": false,
+ "hasCitrusTests": false,
"files": [
"README.adoc",
"rest-api.camel.yaml"
@@ -324,6 +340,7 @@
],
"bundled": true,
"requiresDocker": false,
+ "hasCitrusTests": false,
"files": [
"Greeter.java",
"README.adoc",
@@ -344,6 +361,7 @@
],
"bundled": false,
"requiresDocker": false,
+ "hasCitrusTests": true,
"files": [
"README.adoc",
"analyzer/application-dev.properties",
@@ -390,6 +408,7 @@
],
"bundled": false,
"requiresDocker": true,
+ "hasCitrusTests": false,
"files": [
"README.adoc",
"application.properties",
@@ -409,6 +428,7 @@
],
"bundled": true,
"requiresDocker": false,
+ "hasCitrusTests": false,
"files": [
"README.adoc",
"timer-log.camel.yaml"
@@ -426,6 +446,7 @@
],
"bundled": true,
"requiresDocker": false,
+ "hasCitrusTests": false,
"files": [
"README.adoc",
"consumer.camel.yaml",
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ExampleHelperTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ExampleHelperTest.java
index 1e6473bcfd0f..09a8ce533be9 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ExampleHelperTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ExampleHelperTest.java
@@ -144,6 +144,16 @@ class ExampleHelperTest {
assertEquals("https://github.com/apache/camel-jbang-examples/tree/main/aws/aws-sqs",
url);
}
+ @Test
+ void shouldDetectCitrusTests() {
+ List<JsonObject> catalog = ExampleHelper.loadCatalog();
+ JsonObject mqtt = ExampleHelper.findExample(catalog, "mqtt");
+ assertTrue(ExampleHelper.hasCitrusTests(mqtt));
+
+ JsonObject circuitBreaker = ExampleHelper.findExample(catalog,
"circuit-breaker");
+ assertFalse(ExampleHelper.hasCitrusTests(circuitBreaker));
+ }
+
@Test
void shouldGetExampleNames() {
List<JsonObject> catalog = ExampleHelper.loadCatalog();
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 4b1aea0c0805..964db24c4712 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
@@ -54,7 +54,12 @@ import static
org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.hintLa
class ActionsPopup {
+ private static final int ACTION_RUN_EXAMPLE = 0;
+ private static final int ACTION_SCREENSHOT = 1;
+ private static final int ACTION_COUNT = 2;
+
private final Supplier<Set<String>> runningNames;
+ private final Runnable screenshotAction;
private boolean showActionsMenu;
private final ListState actionsMenuState = new ListState();
@@ -72,8 +77,9 @@ class ActionsPopup {
private boolean launchNotificationError;
private long launchNotificationExpiry;
- ActionsPopup(Supplier<Set<String>> runningNames) {
+ ActionsPopup(Supplier<Set<String>> runningNames, Runnable
screenshotAction) {
this.runningNames = runningNames;
+ this.screenshotAction = screenshotAction;
}
boolean isVisible() {
@@ -148,9 +154,15 @@ class ActionsPopup {
} else if (ke.isUp()) {
actionsMenuState.selectPrevious();
} else if (ke.isDown()) {
- actionsMenuState.selectNext(1);
+ actionsMenuState.selectNext(ACTION_COUNT);
} else if (ke.isConfirm()) {
- openExampleBrowser();
+ Integer sel = actionsMenuState.selected();
+ if (sel != null && sel == ACTION_SCREENSHOT) {
+ showActionsMenu = false;
+ screenshotAction.run();
+ } else {
+ openExampleBrowser();
+ }
}
return true;
}
@@ -200,14 +212,15 @@ class ActionsPopup {
private void renderActionsMenu(Frame frame, Rect area) {
int popupW = 32;
- int popupH = 3;
+ int popupH = 2 + ACTION_COUNT;
int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
int y = area.top() + Math.max(0, (area.height() - popupH) / 2);
Rect popup = new Rect(x, y, Math.min(popupW, area.width()),
Math.min(popupH, area.height()));
frame.renderWidget(Clear.INSTANCE, popup);
ListWidget list = ListWidget.builder()
- .items(ListItem.from(" Run an example..."))
+ .items(ListItem.from(" Run an example..."),
+ ListItem.from(" Take Screenshot"))
.highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue())
.highlightSymbol("")
.scrollMode(ScrollMode.NONE)
@@ -264,8 +277,9 @@ class ActionsPopup {
String desc = ex.getStringOrDefault("description", "");
boolean docker = ExampleHelper.requiresDocker(ex);
boolean bundled = ExampleHelper.isBundled(ex);
+ boolean citrus = ExampleHelper.hasCitrusTests(ex);
- String icons = (bundled ? "๐ฆ" : "๐") + (docker ? "๐ณ" : " ");
+ String icons = (bundled ? "๐ฆ" : "๐") + (docker ? "๐ณ" : " ") +
(citrus ? "๐งช" : " ");
int nameCol = Math.min(30, width / 3);
String padded = String.format("%-" + nameCol + "s",
TuiHelper.truncate(name, nameCol));
String prefix = " " + icons + " " + padded + " ";
@@ -286,7 +300,7 @@ class ActionsPopup {
}
}
items.add(ListItem.from(""));
- items.add(ListItem.from(" ๐ฆ = bundled (offline) ๐ = online (GitHub)
๐ณ = Docker")
+ items.add(ListItem.from(" ๐ฆ = bundled (offline) ๐ = online (GitHub)
๐ณ = Docker ๐งช = Citrus tests")
.style(Style.EMPTY.dim()));
return items;
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
index 85abf5961ae1..0bb354a258ac 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
@@ -23,7 +23,9 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.time.Instant;
+import java.time.LocalDateTime;
import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
@@ -39,6 +41,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
+import dev.tamboui.buffer.Buffer;
+import dev.tamboui.export.ExportRequest;
import dev.tamboui.layout.Constraint;
import dev.tamboui.layout.Layout;
import dev.tamboui.layout.Rect;
@@ -192,12 +196,17 @@ public class CamelMonitor extends CamelCommand {
private volatile long lastRefresh;
private boolean showKillConfirm;
+ private volatile Buffer lastBuffer;
+ private volatile String screenshotMessage;
+ private volatile long screenshotMessageTime;
+ private volatile boolean pendingScreenshot;
private final ActionsPopup actionsPopup = new ActionsPopup(
() -> data.get().stream()
.filter(i -> !i.vanishing && i.name != null)
.map(i -> i.name)
- .collect(Collectors.toSet()));
+ .collect(Collectors.toSet()),
+ () -> pendingScreenshot = true);
private final AtomicBoolean refreshInProgress = new AtomicBoolean(false);
private TuiRunner runner;
@@ -377,6 +386,12 @@ public class CamelMonitor extends CamelCommand {
return true;
}
+ // Screenshot: Shift+F5
+ if (ke.isKey(KeyCode.F5) && ke.hasShift()) {
+ takeScreenshot();
+ return true;
+ }
+
// Tab-specific keys โ delegate to active tab first
int tab = tabsState.selected();
MonitorTab activeTab = activeTab();
@@ -651,6 +666,13 @@ public class CamelMonitor extends CamelCommand {
}
actionsPopup.render(frame, mainChunks.get(4));
renderFooter(frame, mainChunks.get(5));
+
+ lastBuffer = frame.buffer();
+
+ if (pendingScreenshot) {
+ pendingScreenshot = false;
+ takeScreenshot();
+ }
}
private void renderHeader(Frame frame, Rect area) {
@@ -1455,6 +1477,16 @@ public class CamelMonitor extends CamelCommand {
}
private void renderFooter(Frame frame, Rect area) {
+ // Show screenshot flash message briefly
+ String msg = screenshotMessage;
+ if (msg != null && System.currentTimeMillis() - screenshotMessageTime
< 3000) {
+ frame.renderWidget(
+ Paragraph.from(Line.from(Span.styled(" " + msg,
Style.EMPTY.fg(Color.GREEN)))),
+ area);
+ return;
+ }
+ screenshotMessage = null;
+
List<Span> spans = new ArrayList<>();
MonitorTab tab = activeTab();
@@ -2732,6 +2764,26 @@ public class CamelMonitor extends CamelCommand {
return ph.info().startInstant().map(Instant::toEpochMilli).orElse(0L);
}
+ private void takeScreenshot() {
+ Buffer buf = lastBuffer;
+ if (buf == null) {
+ return;
+ }
+ try {
+ String timestamp =
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss"));
+ String baseName = "camel-tui-screenshot-" + timestamp;
+ Path txtPath = Path.of(baseName + ".txt");
+ Path ansPath = Path.of(baseName + ".ans");
+ ExportRequest.export(buf).text().toFile(txtPath);
+ ExportRequest.export(buf).text().options(o ->
o.styles(true)).toFile(ansPath);
+ screenshotMessage = "Screenshot saved to " +
txtPath.toAbsolutePath() + " (and .ans with colors)";
+ screenshotMessageTime = System.currentTimeMillis();
+ } catch (IOException e) {
+ screenshotMessage = "Screenshot failed: " + e.getMessage();
+ screenshotMessageTime = System.currentTimeMillis();
+ }
+ }
+
private static String objToString(Object o) {
return o != null ? o.toString() : "";
}