This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch tui-run-options-and-features
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 5e52e71b22a39b570774b7b55e1aad09fca221de
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu May 21 21:38:26 2026 +0200

    camel-tui: Add Doctor dialog to F2 actions menu
    
    Runs 7 environment checks (Java, Camel, JBang, Maven, Container, Ports,
    Disk Space) and displays results in a centered popup with emoji status
    indicators.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 .../dsl/jbang/core/commands/tui/ActionsPopup.java  |  25 +-
 .../dsl/jbang/core/commands/tui/DoctorPopup.java   | 283 +++++++++++++++++++++
 2 files changed, 305 insertions(+), 3 deletions(-)

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 bb12dfcec2b6..5bf884e24fc3 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
@@ -61,7 +61,8 @@ class ActionsPopup {
     private static final int ACTION_SHOW_DOCS = 1;
     private static final int ACTION_SCREENSHOT = 2;
     private static final int ACTION_SHOW_KEYSTROKES = 3;
-    private static final int ACTION_COUNT = 4;
+    private static final int ACTION_DOCTOR = 4;
+    private static final int ACTION_COUNT = 5;
 
     private final Supplier<Set<String>> runningNames;
     private final Supplier<List<IntegrationInfo>> integrations;
@@ -90,6 +91,8 @@ class ActionsPopup {
     private String docTitle;
     private int docScroll;
 
+    private final DoctorPopup doctorPopup = new DoctorPopup();
+
     private final List<PendingLaunch> pendingLaunches = new ArrayList<>();
     private String launchNotification;
     private boolean launchNotificationError;
@@ -109,7 +112,8 @@ class ActionsPopup {
     }
 
     boolean isVisible() {
-        return showActionsMenu || showExampleBrowser || showNameInput || 
showDocPicker || showDocViewer;
+        return showActionsMenu || showExampleBrowser || showNameInput || 
showDocPicker || showDocViewer
+                || doctorPopup.isVisible();
     }
 
     void open() {
@@ -123,6 +127,7 @@ class ActionsPopup {
         showNameInput = false;
         showDocPicker = false;
         showDocViewer = false;
+        doctorPopup.close();
     }
 
     String notification() {
@@ -209,6 +214,9 @@ class ActionsPopup {
             }
             return true;
         }
+        if (doctorPopup.handleKeyEvent(ke)) {
+            return true;
+        }
         if (showActionsMenu) {
             if (ke.isCancel()) {
                 showActionsMenu = false;
@@ -229,6 +237,9 @@ class ActionsPopup {
                     } else if (sel == ACTION_SHOW_KEYSTROKES) {
                         showActionsMenu = false;
                         toggleKeystrokes.run();
+                    } else if (sel == ACTION_DOCTOR) {
+                        showActionsMenu = false;
+                        doctorPopup.open();
                     }
                 }
             }
@@ -253,9 +264,16 @@ class ActionsPopup {
         if (showDocViewer) {
             renderDocViewer(frame, area);
         }
+        if (doctorPopup.isVisible()) {
+            doctorPopup.render(frame, area);
+        }
     }
 
     void renderFooter(List<Span> spans) {
+        if (doctorPopup.isVisible()) {
+            doctorPopup.renderFooter(spans);
+            return;
+        }
         if (showDocViewer) {
             hint(spans, "↑↓", "scroll");
             hintLast(spans, "Esc", "back");
@@ -311,7 +329,8 @@ class ActionsPopup {
                 .items(ListItem.from("  Run an example..."),
                         ListItem.from("  Show Documentation"),
                         ListItem.from("  Take Screenshot"),
-                        ListItem.from(keystrokeLabel))
+                        ListItem.from(keystrokeLabel),
+                        ListItem.from("  🩺 Run Doctor"))
                 .highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue())
                 .highlightSymbol("")
                 .scrollMode(ScrollMode.NONE)
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
new file mode 100644
index 000000000000..865ff567dd89
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DoctorPopup.java
@@ -0,0 +1,283 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.io.File;
+import java.io.OutputStream;
+import java.net.ServerSocket;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.text.Text;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.widgets.Clear;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Title;
+import dev.tamboui.widgets.paragraph.Paragraph;
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.dsl.jbang.core.common.VersionHelper;
+import org.apache.camel.tooling.maven.MavenDownloaderImpl;
+import org.apache.camel.tooling.maven.MavenResolutionException;
+
+import static 
org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.hintLast;
+
+class DoctorPopup {
+
+    private boolean visible;
+    private List<Line> lines;
+
+    boolean isVisible() {
+        return visible;
+    }
+
+    void open() {
+        lines = new ArrayList<>();
+        checkJava(lines);
+        checkCamelVersion(lines);
+        checkJBang(lines);
+        checkMavenRepository(lines);
+        checkContainerRuntime(lines);
+        checkCommonPorts(lines);
+        checkDiskSpace(lines);
+        visible = true;
+    }
+
+    void close() {
+        visible = false;
+    }
+
+    boolean handleKeyEvent(KeyEvent ke) {
+        if (visible) {
+            if (ke.isCancel()) {
+                visible = false;
+            }
+            return true;
+        }
+        return false;
+    }
+
+    void render(Frame frame, Rect area) {
+        if (lines == null || lines.isEmpty()) {
+            return;
+        }
+        int popupW = Math.min(62, area.width() - 4);
+        int popupH = Math.min(lines.size() + 2, area.height() - 4);
+        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);
+        Paragraph para = Paragraph.builder()
+                .text(Text.from(lines.toArray(Line[]::new)))
+                .block(Block.builder()
+                        .borderType(BorderType.ROUNDED)
+                        .title(" 🩺 Doctor ")
+                        .titleBottom(Title.from(Line.from(
+                                Span.styled(" Esc", 
MonitorContext.HINT_KEY_STYLE), Span.raw(" back "))))
+                        .build())
+                .build();
+        frame.renderWidget(para, popup);
+    }
+
+    void renderFooter(List<Span> spans) {
+        hintLast(spans, "Esc", "back");
+    }
+
+    // ---- Checks ----
+
+    private void checkJava(List<Line> result) {
+        String version = System.getProperty("java.version");
+        String vendor = System.getProperty("java.vendor", "");
+        int major = Runtime.version().feature();
+        String status;
+        String emoji;
+        if (major >= 21) {
+            status = null;
+            emoji = "✅";
+        } else if (major >= 17) {
+            status = "Consider upgrading to 21 or 25";
+            emoji = "⚠️";
+        } else {
+            status = "17+ required";
+            emoji = "❌";
+        }
+        result.add(Line.from(
+                Span.raw("  ☕ "),
+                Span.styled(String.format("%-14s", "Java"), 
Style.EMPTY.bold()),
+                Span.raw(String.format("%-30s", version + " (" + vendor + 
")")),
+                Span.raw(" " + emoji)));
+        if (status != null) {
+            result.add(Line.from(Span.styled("                    " + status, 
Style.EMPTY.dim())));
+        }
+    }
+
+    private void checkCamelVersion(List<Line> result) {
+        try {
+            CamelCatalog catalog = new DefaultCamelCatalog();
+            String version = catalog.getCatalogVersion();
+            result.add(Line.from(
+                    Span.raw("  🐪 "),
+                    Span.styled(String.format("%-14s", "Camel"), 
Style.EMPTY.bold()),
+                    Span.raw(String.format("%-30s", version)),
+                    Span.raw(" ✅")));
+        } catch (Exception e) {
+            result.add(Line.from(
+                    Span.raw("  🐪 "),
+                    Span.styled(String.format("%-14s", "Camel"), 
Style.EMPTY.bold()),
+                    Span.raw(String.format("%-30s", "Not detected")),
+                    Span.raw(" ❌")));
+        }
+    }
+
+    private void checkJBang(List<Line> result) {
+        String version = VersionHelper.getJBangVersion();
+        if (version != null) {
+            result.add(Line.from(
+                    Span.raw("  📦 "),
+                    Span.styled(String.format("%-14s", "JBang"), 
Style.EMPTY.bold()),
+                    Span.raw(String.format("%-30s", version)),
+                    Span.raw(" ✅")));
+        } else {
+            result.add(Line.from(
+                    Span.raw("  📦 "),
+                    Span.styled(String.format("%-14s", "JBang"), 
Style.EMPTY.bold()),
+                    Span.raw(String.format("%-30s", "Not detected")),
+                    Span.raw(" ⚠️")));
+        }
+    }
+
+    private void checkMavenRepository(List<Line> result) {
+        try (MavenDownloaderImpl downloader = new MavenDownloaderImpl()) {
+            downloader.build();
+            CamelCatalog catalog = new DefaultCamelCatalog();
+            String version = catalog.getCatalogVersion();
+            downloader.resolveArtifacts(
+                    List.of("org.apache.camel:camel-api:" + version),
+                    Set.of(), false, false);
+            result.add(Line.from(
+                    Span.raw("  🔧 "),
+                    Span.styled(String.format("%-14s", "Maven"), 
Style.EMPTY.bold()),
+                    Span.raw(String.format("%-30s", "Artifact resolution")),
+                    Span.raw(" ✅")));
+        } catch (MavenResolutionException e) {
+            result.add(Line.from(
+                    Span.raw("  🔧 "),
+                    Span.styled(String.format("%-14s", "Maven"), 
Style.EMPTY.bold()),
+                    Span.raw(String.format("%-30s", "Resolution failed")),
+                    Span.raw(" ❌")));
+            result.add(Line.from(Span.styled("                    " + 
TuiHelper.truncate(e.getMessage(), 40),
+                    Style.EMPTY.dim())));
+        } catch (Exception e) {
+            result.add(Line.from(
+                    Span.raw("  🔧 "),
+                    Span.styled(String.format("%-14s", "Maven"), 
Style.EMPTY.bold()),
+                    Span.raw(String.format("%-30s", "Error")),
+                    Span.raw(" ❌")));
+            result.add(Line.from(Span.styled("                    " + 
TuiHelper.truncate(e.getMessage(), 40),
+                    Style.EMPTY.dim())));
+        }
+    }
+
+    private void checkContainerRuntime(List<Line> result) {
+        for (String cmd : new String[] { "docker", "podman" }) {
+            try {
+                Process p = new ProcessBuilder(cmd, "info")
+                        .redirectErrorStream(true)
+                        .start();
+                p.getInputStream().transferTo(OutputStream.nullOutputStream());
+                int exit = p.waitFor();
+                if (exit == 0) {
+                    String name = Character.toUpperCase(cmd.charAt(0)) + 
cmd.substring(1);
+                    result.add(Line.from(
+                            Span.raw("  🐳 "),
+                            Span.styled(String.format("%-14s", "Container"), 
Style.EMPTY.bold()),
+                            Span.raw(String.format("%-30s", name + " 
running")),
+                            Span.raw(" ✅")));
+                    return;
+                }
+            } catch (Exception e) {
+                // not found, try next
+            }
+        }
+        result.add(Line.from(
+                Span.raw("  🐳 "),
+                Span.styled(String.format("%-14s", "Container"), 
Style.EMPTY.bold()),
+                Span.raw(String.format("%-30s", "Not found (optional)")),
+                Span.raw(" ⚠️")));
+    }
+
+    private void checkCommonPorts(List<Line> result) {
+        StringBuilder conflicts = new StringBuilder();
+        for (int port : new int[] { 8080, 8443, 9090 }) {
+            if (isPortInUse(port)) {
+                if (!conflicts.isEmpty()) {
+                    conflicts.append(", ");
+                }
+                conflicts.append(port);
+            }
+        }
+        if (!conflicts.isEmpty()) {
+            result.add(Line.from(
+                    Span.raw("  🔌 "),
+                    Span.styled(String.format("%-14s", "Ports"), 
Style.EMPTY.bold()),
+                    Span.raw(String.format("%-30s", "In use: " + conflicts)),
+                    Span.raw(" ⚠️")));
+        } else {
+            result.add(Line.from(
+                    Span.raw("  🔌 "),
+                    Span.styled(String.format("%-14s", "Ports"), 
Style.EMPTY.bold()),
+                    Span.raw(String.format("%-30s", "8080, 8443, 9090 free")),
+                    Span.raw(" ✅")));
+        }
+    }
+
+    private static boolean isPortInUse(int port) {
+        try (ServerSocket ss = new ServerSocket(port)) {
+            ss.setReuseAddress(true);
+            return false;
+        } catch (Exception e) {
+            return true;
+        }
+    }
+
+    private void checkDiskSpace(List<Line> result) {
+        File tmpDir = new File(System.getProperty("java.io.tmpdir"));
+        long free = tmpDir.getFreeSpace();
+        long mb = free / (1024 * 1024);
+        long gb = mb / 1024;
+        String emoji = mb > 500 ? "✅" : "⚠️";
+        String unit = gb > 10 ? "GB" : "MB";
+        long value = gb > 0 ? gb : mb;
+        result.add(Line.from(
+                Span.raw("  💾 "),
+                Span.styled(String.format("%-14s", "Disk Space"), 
Style.EMPTY.bold()),
+                Span.raw(String.format("%-30s", value + " " + unit + " free in 
temp dir")),
+                Span.raw(" " + emoji)));
+        if (mb <= 500) {
+            result.add(Line.from(Span.styled("                    Low disk 
space may cause issues",
+                    Style.EMPTY.dim())));
+        }
+    }
+}

Reply via email to