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 7bafca9f9ef6 CAMEL-24260: TUI quick docs for Spring Boot configuration 
metadata
7bafca9f9ef6 is described below

commit 7bafca9f9ef625cb688807198ff2bdd58deda93a
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Jul 27 12:50:15 2026 +0200

    CAMEL-24260: TUI quick docs for Spring Boot configuration metadata
    
    CAMEL-24260: TUI gutter warning triangle for deprecated properties
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../camel/cli/connector/LocalCliConnector.java     |  19 ++++
 .../jbang/core/commands/tui/ConfigurationTab.java  |  31 +++++-
 .../dsl/jbang/core/commands/tui/DiagramTab.java    |   4 +-
 .../dsl/jbang/core/commands/tui/RoutesTab.java     |  42 ++++---
 .../dsl/jbang/core/commands/tui/SourceTab.java     |  95 ++++++++++++++--
 .../dsl/jbang/core/commands/tui/SourceViewer.java  |  61 ++++++++--
 .../commands/tui/SpringBootMetadataHelper.java     | 124 +++++++++++++++++++++
 7 files changed, 337 insertions(+), 39 deletions(-)

diff --git 
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
 
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
index ad33fe6a3e93..9dd068726e57 100644
--- 
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
+++ 
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
@@ -388,6 +388,8 @@ public class LocalCliConnector extends ServiceSupport 
implements CliConnector, C
                 doActionJfrMemoryLeakTask(root);
             } else if ("cli-debug".equals(action)) {
                 doActionCliDebug(root);
+            } else if ("spring-boot-configuration".equals(action)) {
+                doActionSpringBootConfigurationTask(root);
             }
         } catch (Exception e) {
             LOG.warn("Error executing action: {} due to: {}. This exception is 
ignored.", action != null ? action : af,
@@ -886,6 +888,23 @@ public class LocalCliConnector extends ServiceSupport 
implements CliConnector, C
         }
     }
 
+    private void doActionSpringBootConfigurationTask(JsonObject root) throws 
IOException {
+        DevConsole dc = 
camelContext.getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class)
+                .resolveById("spring-boot-configuration");
+        if (dc != null) {
+            Map<String, Object> params = new HashMap<>();
+            String filter = root.getString("filter");
+            if (filter != null) {
+                params.put("filter", filter);
+            }
+            JsonObject json = (JsonObject) dc.call(DevConsole.MediaType.JSON, 
params);
+            LOG.trace("Updating output file: {}", outputFile);
+            IOHelper.writeText(json.toJson(), outputFile);
+        } else {
+            IOHelper.writeText("{}", outputFile);
+        }
+    }
+
     private void doActionHeapDumpTask(JsonObject root) throws IOException {
         DevConsole dc = 
camelContext.getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class)
                 .resolveById("heap-dump");
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConfigurationTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConfigurationTab.java
index f0d9634a8539..4e4e1066b76c 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConfigurationTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConfigurationTab.java
@@ -61,6 +61,10 @@ class ConfigurationTab extends AbstractTableTab {
     private Map<String, BaseOptionModel> mainOptionsMap;
     private final Map<String, Map<String, BaseOptionModel>> 
componentOptionsCache = new HashMap<>();
 
+    // Spring Boot configuration metadata cache (lazy-loaded on-demand via IPC)
+    private Map<String, JsonObject> springBootMetadataCache;
+    private boolean springBootMetadataLoaded;
+
     ConfigurationTab(MonitorContext ctx) {
         super(ctx, "key", "value", "source");
     }
@@ -318,13 +322,36 @@ class ConfigurationTab extends AbstractTableTab {
                 String optionName = rest.substring(dot + 1);
                 Map<String, BaseOptionModel> compOptions = 
getComponentOptions(componentName);
                 if (compOptions != null) {
-                    return compOptions.get(optionName);
+                    BaseOptionModel opt = compOptions.get(optionName);
+                    if (opt != null) {
+                        return opt;
+                    }
                 }
             }
         }
+        // fallback to Spring Boot configuration metadata
+        ensureSpringBootMetadataCache();
+        if (springBootMetadataCache != null) {
+            JsonObject sbProp = springBootMetadataCache.get(key);
+            if (sbProp != null) {
+                return SpringBootMetadataHelper.toOptionModel(sbProp);
+            }
+        }
         return null;
     }
 
+    private void ensureSpringBootMetadataCache() {
+        if (springBootMetadataLoaded) {
+            return;
+        }
+        springBootMetadataLoaded = true;
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null || !"Spring Boot".equals(info.platform)) {
+            return;
+        }
+        springBootMetadataCache = SpringBootMetadataHelper.fetchMetadata(ctx, 
info.pid);
+    }
+
     private void initCatalog() {
         IntegrationInfo info = ctx.findSelectedIntegration();
         String version = info != null ? info.camelVersion : null;
@@ -342,6 +369,8 @@ class ConfigurationTab extends AbstractTableTab {
         catalogVersion = version;
         mainOptionsMap = new HashMap<>();
         componentOptionsCache.clear();
+        springBootMetadataCache = null;
+        springBootMetadataLoaded = false;
         MainModel mainModel = catalog.mainModel();
         if (mainModel != null) {
             for (MainModel.MainOptionModel opt : mainModel.getOptions()) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
index 7c7ac2116bac..6160d6260185 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
@@ -1420,7 +1420,7 @@ class DiagramTab extends AbstractTab {
 
     // ---- Quick doc (i toggle in source viewer) ----
 
-    private Map<Integer, List<String>> provideAllQuickDocs(List<JsonObject> 
cd) {
+    private Map<Integer, List<SourceViewer.DocEntry>> 
provideAllQuickDocs(List<JsonObject> cd) {
         if (cachedRouteDetail == null || cd.isEmpty()) {
             return Map.of();
         }
@@ -1433,7 +1433,7 @@ class DiagramTab extends AbstractTab {
         IntegrationInfo info = ctx.findSelectedIntegration();
         CamelCatalog catalog = info != null ? getCatalog(info) : null;
 
-        Map<Integer, List<String>> result = new LinkedHashMap<>();
+        Map<Integer, List<SourceViewer.DocEntry>> result = new 
LinkedHashMap<>();
         for (JsonObject proc : processors) {
             Integer line = proc.getInteger("line");
             if (line == null || line <= 0) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
index 6590530ad796..a8142f5fe957 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
@@ -2058,7 +2058,7 @@ class RoutesTab extends AbstractTab {
 
     // ---- Quick doc (q toggle in source viewer) ----
 
-    private Map<Integer, List<String>> provideAllQuickDocs(List<JsonObject> 
cd) {
+    private Map<Integer, List<SourceViewer.DocEntry>> 
provideAllQuickDocs(List<JsonObject> cd) {
         if (cachedRouteDetail == null || cd.isEmpty()) {
             return Map.of();
         }
@@ -2071,7 +2071,7 @@ class RoutesTab extends AbstractTab {
         IntegrationInfo info = ctx.findSelectedIntegration();
         CamelCatalog catalog = info != null ? getCatalog(info) : null;
 
-        Map<Integer, List<String>> result = new LinkedHashMap<>();
+        Map<Integer, List<SourceViewer.DocEntry>> result = new 
LinkedHashMap<>();
         for (JsonObject proc : processors) {
             Integer line = proc.getInteger("line");
             if (line == null || line <= 0) {
@@ -2111,7 +2111,7 @@ class RoutesTab extends AbstractTab {
     }
 
     static void buildEndpointInlineDoc(
-            Map<Integer, List<String>> result, List<JsonObject> cd,
+            Map<Integer, List<SourceViewer.DocEntry>> result, List<JsonObject> 
cd,
             CamelCatalog catalog, String endpointUri, int eipIdx) {
 
         String component = endpointUri.contains(":") ? 
endpointUri.substring(0, endpointUri.indexOf(':')) : endpointUri;
@@ -2131,8 +2131,8 @@ class RoutesTab extends AbstractTab {
         }
 
         int beforeSize = result.size();
-        List<String> titleLines = new ArrayList<>();
-        titleLines.add(compTitle + " — " + desc);
+        List<SourceViewer.DocEntry> titleLines = new ArrayList<>();
+        titleLines.add(SourceViewer.DocEntry.of(compTitle + " — " + desc));
         result.put(eipIdx, titleLines);
 
         inlineParameterDocs(result, cd, eipIdx, optionDocs);
@@ -2145,7 +2145,7 @@ class RoutesTab extends AbstractTab {
     }
 
     private static void clusterEndpointOptions(
-            List<String> docLines, CamelCatalog catalog,
+            List<SourceViewer.DocEntry> docLines, CamelCatalog catalog,
             String endpointUri, Map<String, BaseOptionModel> optionDocs) {
         try {
             Map<String, String> props = 
catalog.endpointProperties(endpointUri);
@@ -2155,7 +2155,10 @@ class RoutesTab extends AbstractTab {
                     if (optModel != null) {
                         String optDoc = formatOptionDoc(optModel);
                         if (optDoc != null) {
-                            docLines.add(entry.getKey() + ": " + 
entry.getValue() + " — " + optDoc);
+                            String text = entry.getKey() + ": " + 
entry.getValue() + " — " + optDoc;
+                            docLines.add(optModel.isDeprecated()
+                                    ? SourceViewer.DocEntry.deprecated(text)
+                                    : SourceViewer.DocEntry.of(text));
                         }
                     }
                 }
@@ -2166,7 +2169,7 @@ class RoutesTab extends AbstractTab {
     }
 
     static void buildEipInlineDoc(
-            Map<Integer, List<String>> result, List<JsonObject> cd,
+            Map<Integer, List<SourceViewer.DocEntry>> result, List<JsonObject> 
cd,
             CamelCatalog catalog, String type, JsonObject opts, int eipIdx) {
 
         EipModel model = catalog != null ? catalog.eipModel(type) : null;
@@ -2182,7 +2185,7 @@ class RoutesTab extends AbstractTab {
             }
         }
 
-        List<String> titleLines = new ArrayList<>();
+        List<SourceViewer.DocEntry> titleLines = new ArrayList<>();
         if (model != null && model.getTitle() != null) {
             String eipTitle = model.getTitle();
             if (compModel != null && compModel.getTitle() != null) {
@@ -2194,9 +2197,9 @@ class RoutesTab extends AbstractTab {
             } else {
                 desc = model.getDescription() != null ? 
truncateText(model.getDescription(), 80) : "";
             }
-            titleLines.add(eipTitle + " — " + desc);
+            titleLines.add(SourceViewer.DocEntry.of(eipTitle + " — " + desc));
         } else {
-            titleLines.add(type);
+            titleLines.add(SourceViewer.DocEntry.of(type));
         }
 
         int beforeSize = result.size();
@@ -2228,7 +2231,7 @@ class RoutesTab extends AbstractTab {
     }
 
     private static void clusterEipOptions(
-            List<String> docLines, JsonObject opts,
+            List<SourceViewer.DocEntry> docLines, JsonObject opts,
             Map<String, BaseOptionModel> optionDocs, boolean skipUri) {
         for (Map.Entry<String, Object> entry : opts.entrySet()) {
             if (skipUri && "uri".equals(entry.getKey())) {
@@ -2238,7 +2241,10 @@ class RoutesTab extends AbstractTab {
             if (optModel != null) {
                 String optDoc = formatOptionDoc(optModel);
                 if (optDoc != null) {
-                    docLines.add(entry.getKey() + ": " + entry.getValue() + " 
— " + optDoc);
+                    String text = entry.getKey() + ": " + entry.getValue() + " 
— " + optDoc;
+                    docLines.add(optModel.isDeprecated()
+                            ? SourceViewer.DocEntry.deprecated(text)
+                            : SourceViewer.DocEntry.of(text));
                 }
             }
         }
@@ -2261,7 +2267,7 @@ class RoutesTab extends AbstractTab {
     }
 
     static void inlineParameterDocs(
-            Map<Integer, List<String>> result, List<JsonObject> cd,
+            Map<Integer, List<SourceViewer.DocEntry>> result, List<JsonObject> 
cd,
             int eipIdx, Map<String, BaseOptionModel> optionDocs) {
 
         if (optionDocs.isEmpty()) {
@@ -2302,7 +2308,9 @@ class RoutesTab extends AbstractTab {
                             String docLine = formatOptionDoc(doc);
                             if (docLine != null) {
                                 int docIdx = lastContinuationLine(cd, i, 
indent);
-                                result.put(docIdx, List.of(docLine));
+                                result.put(docIdx, List.of(doc.isDeprecated()
+                                        ? 
SourceViewer.DocEntry.deprecated(docLine)
+                                        : SourceViewer.DocEntry.of(docLine)));
                             }
                         }
                     }
@@ -2336,7 +2344,9 @@ class RoutesTab extends AbstractTab {
                 String docLine = formatOptionDoc(doc);
                 if (docLine != null) {
                     int docIdx = lastContinuationLine(cd, i, indent);
-                    result.put(docIdx, List.of(docLine));
+                    result.put(docIdx, List.of(doc.isDeprecated()
+                            ? SourceViewer.DocEntry.deprecated(docLine)
+                            : SourceViewer.DocEntry.of(docLine)));
                 }
             }
         }
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 bafa66b66f53..601c2ab12859 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
@@ -26,9 +26,11 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -92,6 +94,10 @@ class SourceTab extends AbstractTab {
     private final Map<String, Map<String, BaseOptionModel>> 
languageOptionsCache = new HashMap<>();
     private final Map<String, Map<String, BaseOptionModel>> 
dataformatOptionsCache = new HashMap<>();
 
+    // Spring Boot configuration metadata cache (lazy-loaded on-demand via IPC)
+    private Map<String, JsonObject> springBootMetadataCache;
+    private boolean springBootMetadataLoaded;
+
     private static final Pattern YAML_URI_PATTERN = Pattern.compile(
             
"^\\s*-?\\s*(?:uri|from|to|toD|wireTap|enrich|pollEnrich|deadLetterChannel):\\s*\"?([a-zA-Z][a-zA-Z0-9+.-]*:[^\"\\s]*)");
     private static final Pattern YAML_EIP_PATTERN = Pattern.compile(
@@ -476,14 +482,17 @@ class SourceTab extends AbstractTab {
                 loadDirectory(Path.of(entry.path()));
             } else {
                 Path filePath = Path.of(entry.path());
-                sourceViewer.loadFile(filePath);
                 if (isCamelSourceFile(filePath)) {
                     
sourceViewer.setQuickDocProvider(this::provideCamelQuickDocs);
+                    sourceViewer.setDeprecatedLineScanner(null);
                 } else if (isPropertiesFile(filePath)) {
                     
sourceViewer.setQuickDocProvider(this::providePropertiesQuickDocs);
+                    
sourceViewer.setDeprecatedLineScanner(this::scanDeprecatedProperties);
                 } else {
                     sourceViewer.setQuickDocProvider(null);
+                    sourceViewer.setDeprecatedLineScanner(null);
                 }
+                sourceViewer.loadFile(filePath);
                 focusOnViewer = true;
             }
         }
@@ -525,13 +534,13 @@ class SourceTab extends AbstractTab {
         }
     }
 
-    private Map<Integer, List<String>> provideCamelQuickDocs(List<JsonObject> 
codeData) {
+    private Map<Integer, List<SourceViewer.DocEntry>> 
provideCamelQuickDocs(List<JsonObject> codeData) {
         CamelCatalog catalog = getCatalog();
         if (catalog == null || codeData.isEmpty()) {
             return Map.of();
         }
 
-        Map<Integer, List<String>> result = new LinkedHashMap<>();
+        Map<Integer, List<SourceViewer.DocEntry>> result = new 
LinkedHashMap<>();
         for (int i = 0; i < codeData.size(); i++) {
             String code = codeData.get(i).getString("code");
             if (code == null) {
@@ -561,14 +570,14 @@ class SourceTab extends AbstractTab {
         return 
path.getFileName().toString().toLowerCase().endsWith(".properties");
     }
 
-    private Map<Integer, List<String>> 
providePropertiesQuickDocs(List<JsonObject> codeData) {
+    private Map<Integer, List<SourceViewer.DocEntry>> 
providePropertiesQuickDocs(List<JsonObject> codeData) {
         CamelCatalog catalog = getCatalog();
         if (catalog == null || codeData.isEmpty()) {
             return Map.of();
         }
         ensureMainOptionsCache(catalog);
 
-        Map<Integer, List<String>> result = new LinkedHashMap<>();
+        Map<Integer, List<SourceViewer.DocEntry>> result = new 
LinkedHashMap<>();
         for (int i = 0; i < codeData.size(); i++) {
             String code = codeData.get(i).getString("code");
             if (code == null) {
@@ -583,14 +592,68 @@ class SourceTab extends AbstractTab {
                 continue;
             }
             String key = trimmed.substring(0, eq).trim();
-            if (!key.startsWith("camel.")) {
-                continue;
-            }
             BaseOptionModel opt = lookupPropertyOption(catalog, key);
             if (opt != null) {
                 String doc = RoutesTab.formatOptionDoc(opt);
                 if (doc != null) {
-                    result.put(i, List.of(doc));
+                    result.put(i, List.of(opt.isDeprecated()
+                            ? SourceViewer.DocEntry.deprecated(doc)
+                            : SourceViewer.DocEntry.of(doc)));
+                }
+                continue;
+            }
+            // fallback to Spring Boot configuration metadata for non-camel 
properties
+            ensureSpringBootMetadataCache();
+            if (springBootMetadataCache != null) {
+                JsonObject sbProp = springBootMetadataCache.get(key);
+                if (sbProp != null) {
+                    String doc = SpringBootMetadataHelper.formatDoc(sbProp);
+                    if (doc != null) {
+                        boolean deprecated = 
Boolean.TRUE.equals(sbProp.get("deprecated"));
+                        result.put(i, List.of(deprecated
+                                ? SourceViewer.DocEntry.deprecated(doc)
+                                : SourceViewer.DocEntry.of(doc)));
+                    }
+                }
+            }
+        }
+        return result;
+    }
+
+    private Set<Integer> scanDeprecatedProperties(List<JsonObject> codeData) {
+        CamelCatalog catalog = getCatalog();
+        if (catalog == null || codeData.isEmpty()) {
+            return Set.of();
+        }
+        ensureMainOptionsCache(catalog);
+
+        Set<Integer> result = new HashSet<>();
+        for (int i = 0; i < codeData.size(); i++) {
+            String code = codeData.get(i).getString("code");
+            if (code == null) {
+                continue;
+            }
+            String trimmed = code.trim();
+            if (trimmed.isEmpty() || trimmed.startsWith("#") || 
trimmed.startsWith("!")) {
+                continue;
+            }
+            int eq = trimmed.indexOf('=');
+            if (eq <= 0) {
+                continue;
+            }
+            String key = trimmed.substring(0, eq).trim();
+            BaseOptionModel opt = lookupPropertyOption(catalog, key);
+            if (opt != null) {
+                if (opt.isDeprecated()) {
+                    result.add(i);
+                }
+                continue;
+            }
+            ensureSpringBootMetadataCache();
+            if (springBootMetadataCache != null) {
+                JsonObject sbProp = springBootMetadataCache.get(key);
+                if (sbProp != null && 
Boolean.TRUE.equals(sbProp.get("deprecated"))) {
+                    result.add(i);
                 }
             }
         }
@@ -663,6 +726,8 @@ class SourceTab extends AbstractTab {
             componentOptionsCache.clear();
             languageOptionsCache.clear();
             dataformatOptionsCache.clear();
+            springBootMetadataCache = null;
+            springBootMetadataLoaded = false;
             propsCatalogVersion = version;
         }
         if (mainOptionsCache == null) {
@@ -678,6 +743,18 @@ class SourceTab extends AbstractTab {
         }
     }
 
+    private void ensureSpringBootMetadataCache() {
+        if (springBootMetadataLoaded) {
+            return;
+        }
+        springBootMetadataLoaded = true;
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null || !"Spring Boot".equals(info.platform)) {
+            return;
+        }
+        springBootMetadataCache = SpringBootMetadataHelper.fetchMetadata(ctx, 
info.pid);
+    }
+
     private void renderFileList(Frame frame, Rect area) {
         Style fileBorderStyle = focusOnViewer ? Theme.muted() : 
Style.EMPTY.fg(Theme.accent());
         if (entries.isEmpty()) {
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 679e39ea674b..710da4eb5fe3 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
@@ -21,6 +21,7 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.IntConsumer;
@@ -58,9 +59,24 @@ import org.apache.camel.util.json.Jsoner;
  */
 class SourceViewer {
 
+    record DocEntry(String text, boolean deprecated) {
+        static DocEntry of(String text) {
+            return new DocEntry(text, false);
+        }
+
+        static DocEntry deprecated(String text) {
+            return new DocEntry(text, true);
+        }
+    }
+
     @FunctionalInterface
     interface QuickDocProvider {
-        Map<Integer, List<String>> provideAll(List<JsonObject> codeData);
+        Map<Integer, List<DocEntry>> provideAll(List<JsonObject> codeData);
+    }
+
+    @FunctionalInterface
+    interface DeprecatedLineScanner {
+        Set<Integer> scan(List<JsonObject> codeData);
     }
 
     private boolean visible;
@@ -92,7 +108,9 @@ class SourceViewer {
     private int markdownScroll;
     private QuickDocProvider quickDocProvider;
     private boolean quickDocEnabled;
-    private Map<Integer, List<String>> quickDocEntries = 
Collections.emptyMap();
+    private Map<Integer, List<DocEntry>> quickDocEntries = 
Collections.emptyMap();
+    private DeprecatedLineScanner deprecatedLineScanner;
+    private Set<Integer> deprecatedLines = Collections.emptySet();
     private Style titleStyle;
     private Style borderStyle;
     private boolean focused = true;
@@ -123,6 +141,7 @@ class SourceViewer {
         onLineSelected = null;
         quickDocEnabled = false;
         quickDocEntries = Collections.emptyMap();
+        deprecatedLines = Collections.emptySet();
     }
 
     void reset() {
@@ -150,6 +169,8 @@ class SourceViewer {
         quickDocProvider = null;
         quickDocEnabled = false;
         quickDocEntries = Collections.emptyMap();
+        deprecatedLineScanner = null;
+        deprecatedLines = Collections.emptySet();
     }
 
     void setOnLineSelected(IntConsumer callback) {
@@ -183,6 +204,10 @@ class SourceViewer {
         this.quickDocProvider = provider;
     }
 
+    void setDeprecatedLineScanner(DeprecatedLineScanner scanner) {
+        this.deprecatedLineScanner = scanner;
+    }
+
     boolean handleKeyEvent(KeyEvent ke) {
         if (!visible) {
             return false;
@@ -420,7 +445,7 @@ class SourceViewer {
             for (int i = lines.size() - 1; i >= 0; i--) {
                 visualFromEnd += wrapRowCount(lines.get(i), contentWidth);
                 if (quickDocEnabled) {
-                    List<String> docs = quickDocEntries.get(i);
+                    List<DocEntry> docs = quickDocEntries.get(i);
                     if (docs != null) {
                         visualFromEnd += docs.size();
                     }
@@ -451,11 +476,11 @@ class SourceViewer {
         for (int i = scrollY; i < lines.size() && visible.size() < 
visibleLines; i++) {
             String raw = lines.get(i);
             boolean isSelected = (i == selectedLine);
-            Line line = highlightSourceLine(raw, hSkip, isSelected, 
inner.width());
+            Line line = highlightSourceLine(raw, i, hSkip, isSelected, 
inner.width());
             line = search.applyHighlights(line, i, currentMatchLine);
             visible.add(line);
 
-            List<String> docLines = quickDocEnabled ? quickDocEntries.get(i) : 
null;
+            List<DocEntry> docLines = quickDocEnabled ? quickDocEntries.get(i) 
: null;
             if (docLines != null) {
                 String code = i < codeData.size() && 
codeData.get(i).get("code") != null
                         ? codeData.get(i).get("code").toString()
@@ -464,8 +489,8 @@ class SourceViewer {
                 while (si < code.length() && code.charAt(si) == ' ') {
                     si++;
                 }
-                for (String docText : docLines) {
-                    for (Line docLine : renderQuickDocLines(docText, si, 
gutterWidth, inner.width())) {
+                for (DocEntry docEntry : docLines) {
+                    for (Line docLine : renderQuickDocLines(docEntry, si, 
gutterWidth, inner.width())) {
                         if (visible.size() >= visibleLines) {
                             break;
                         }
@@ -576,6 +601,7 @@ class SourceViewer {
                 rawMarkdownContent = null;
                 markdownMode = false;
             }
+            scanDeprecatedLines();
         } catch (java.io.IOException e) {
             title = fileName;
             lines = List.of("(Failed to read file: " + e.getMessage() + ")");
@@ -966,7 +992,7 @@ class SourceViewer {
         };
     }
 
-    private Line highlightSourceLine(String raw, int hSkip, boolean 
isSelected, int viewportWidth) {
+    private Line highlightSourceLine(String raw, int lineIndex, int hSkip, 
boolean isSelected, int viewportWidth) {
         int prefixEnd = 0;
         while (prefixEnd < raw.length() && (raw.charAt(prefixEnd) == ' ' || 
Character.isDigit(raw.charAt(prefixEnd)))) {
             prefixEnd++;
@@ -976,6 +1002,7 @@ class SourceViewer {
         String code = raw.substring(prefixEnd);
 
         Line highlighted = SyntaxHighlighter.highlightLine(code, language);
+        boolean isDeprecated = deprecatedLines.contains(lineIndex);
 
         List<Span> spans = new ArrayList<>();
         Style selBg = focused ? Theme.selectionBg() : 
Theme.selectionBg().dim();
@@ -988,7 +1015,9 @@ class SourceViewer {
                 spans.add(Span.styled(s.content(), s.style().patch(selBg)));
             }
         } else {
-            spans.add(Span.raw("   "));
+            spans.add(isDeprecated
+                    ? Span.styled(" ⚠ ", Theme.warning())
+                    : Span.raw("   "));
             if (!prefix.isEmpty()) {
                 spans.add(Span.styled(prefix, Style.EMPTY.dim()));
             }
@@ -1093,7 +1122,8 @@ class SourceViewer {
         return 0;
     }
 
-    private List<Line> renderQuickDocLines(String text, int sourceIndent, int 
gutterWidth, int viewportWidth) {
+    private List<Line> renderQuickDocLines(DocEntry entry, int sourceIndent, 
int gutterWidth, int viewportWidth) {
+        String text = entry.text();
         String prefix = " ".repeat(gutterWidth + 5 + sourceIndent);
         String marker = "ℹ ";
         Style docStyle = Style.EMPTY.dim().italic();
@@ -1148,7 +1178,7 @@ class SourceViewer {
         for (int i = fromLine; i < toLine && i < lines.size(); i++) {
             count += wrapRowCount(lines.get(i), contentWidth);
             if (quickDocEnabled) {
-                List<String> docs = quickDocEntries.get(i);
+                List<DocEntry> docs = quickDocEntries.get(i);
                 if (docs != null) {
                     count += docs.size();
                 }
@@ -1164,6 +1194,15 @@ class SourceViewer {
         return (line.length() + contentWidth - 1) / contentWidth;
     }
 
+    private void scanDeprecatedLines() {
+        if (deprecatedLineScanner != null && !codeData.isEmpty()) {
+            Set<Integer> result = deprecatedLineScanner.scan(codeData);
+            deprecatedLines = result != null ? result : Collections.emptySet();
+        } else {
+            deprecatedLines = Collections.emptySet();
+        }
+    }
+
     private static String objToString(Object o) {
         return o != null ? o.toString() : "";
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SpringBootMetadataHelper.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SpringBootMetadataHelper.java
new file mode 100644
index 000000000000..0593b5a81672
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SpringBootMetadataHelper.java
@@ -0,0 +1,124 @@
+/*
+ * 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.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.tooling.model.BaseOptionModel;
+import org.apache.camel.tooling.model.MainModel;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+/**
+ * Helper for fetching and formatting Spring Boot configuration metadata from 
a running integration via the CLI
+ * connector IPC mechanism.
+ */
+final class SpringBootMetadataHelper {
+
+    private SpringBootMetadataHelper() {
+    }
+
+    static Map<String, JsonObject> fetchMetadata(MonitorContext ctx, String 
pid) {
+        JsonObject root = new JsonObject();
+        root.put("action", "spring-boot-configuration");
+
+        JsonObject response = ctx.executeAction(pid, root, 5000);
+        if (response == null) {
+            return Map.of();
+        }
+
+        Object propsObj = response.get("properties");
+        if (!(propsObj instanceof JsonArray arr)) {
+            return Map.of();
+        }
+
+        Map<String, JsonObject> result = new HashMap<>();
+        for (int i = 0; i < arr.size(); i++) {
+            Object item = arr.get(i);
+            if (item instanceof JsonObject prop) {
+                String name = prop.getString("name");
+                if (name != null && !name.isEmpty()) {
+                    result.put(name, prop);
+                }
+            }
+        }
+        return result;
+    }
+
+    static String formatDoc(JsonObject prop) {
+        StringBuilder sb = new StringBuilder();
+        String description = prop.getString("description");
+        if (description != null && !description.isEmpty()) {
+            sb.append(RoutesTab.truncateText(description, 80));
+        }
+
+        List<String> meta = new ArrayList<>();
+        String type = prop.getString("type");
+        if (type != null) {
+            int lastDot = type.lastIndexOf('.');
+            if (lastDot >= 0) {
+                type = type.substring(lastDot + 1);
+            }
+            meta.add(type);
+        }
+        Object defaultValue = prop.get("defaultValue");
+        if (defaultValue != null) {
+            meta.add("default: " + defaultValue);
+        }
+        if (!meta.isEmpty()) {
+            if (!sb.isEmpty()) {
+                sb.append(" ");
+            }
+            sb.append("(").append(String.join(", ", meta)).append(")");
+        }
+
+        if (Boolean.TRUE.equals(prop.get("deprecated"))) {
+            sb.append(" [deprecated]");
+        }
+
+        return !sb.isEmpty() ? sb.toString() : null;
+    }
+
+    static BaseOptionModel toOptionModel(JsonObject prop) {
+        MainModel.MainOptionModel model = new MainModel.MainOptionModel();
+        model.setName(prop.getString("name"));
+        model.setDescription(prop.getString("description"));
+
+        String type = prop.getString("type");
+        if (type != null) {
+            int lastDot = type.lastIndexOf('.');
+            if (lastDot >= 0) {
+                type = type.substring(lastDot + 1);
+            }
+            model.setType(type);
+        }
+
+        Object defaultValue = prop.get("defaultValue");
+        if (defaultValue != null) {
+            model.setDefaultValue(defaultValue.toString());
+        }
+
+        if (Boolean.TRUE.equals(prop.get("deprecated"))) {
+            model.setDeprecated(true);
+        }
+
+        return model;
+    }
+}


Reply via email to