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