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() : "";
     }

Reply via email to