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

davsclaus pushed a commit to branch CAMEL-23648-run-folder
in repository https://gitbox.apache.org/repos/asf/camel.git

commit ad88468df388ff77fe943a68c70e1ade2caa5055
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Jun 1 09:40:26 2026 +0200

    CAMEL-23648: camel-jbang - TUI source code syntax highlighting
    
    Co-Authored-By: Claude <[email protected]>
---
 .../dsl/jbang/core/commands/tui/RoutesTab.java     |  51 +++-
 .../jbang/core/commands/tui/SyntaxHighlighter.java | 291 +++++++++++++++++++++
 2 files changed, 340 insertions(+), 2 deletions(-)

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 0e363d62e26f..48de04ec6473 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
@@ -44,6 +44,8 @@ import dev.tamboui.widgets.table.Row;
 import dev.tamboui.widgets.table.Table;
 import dev.tamboui.widgets.table.TableState;
 import org.apache.camel.dsl.jbang.core.common.PathUtils;
+import org.apache.camel.support.LoggerHelper;
+import org.apache.camel.util.FileUtil;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
 import org.apache.camel.util.json.Jsoner;
@@ -81,6 +83,7 @@ class RoutesTab implements MonitorTab {
     private boolean showSource;
     private List<String> sourceLines = Collections.emptyList();
     private String sourceTitle;
+    private SyntaxHighlighter.Language sourceLanguage = 
SyntaxHighlighter.Language.PLAIN;
     private int sourceScroll;
     private int sourceScrollX;
     private final ScrollbarState sourceVScrollState = new ScrollbarState();
@@ -732,7 +735,7 @@ class RoutesTab implements MonitorTab {
         List<Line> visible = new ArrayList<>();
         for (int i = sourceScroll; i < end; i++) {
             String raw = sourceLines.get(i);
-            visible.add(TuiHelper.ansiToLine(raw, sourceScrollX));
+            visible.add(highlightSourceLine(raw, sourceScrollX));
         }
         
frame.renderWidget(Paragraph.builder().text(Text.from(visible)).build(), inner);
 
@@ -748,6 +751,48 @@ class RoutesTab implements MonitorTab {
         }
     }
 
+    private Line highlightSourceLine(String raw, int hSkip) {
+        // Split line number prefix from code content
+        int prefixEnd = 0;
+        while (prefixEnd < raw.length() && (raw.charAt(prefixEnd) == ' ' || 
Character.isDigit(raw.charAt(prefixEnd)))) {
+            prefixEnd++;
+        }
+
+        String prefix = raw.substring(0, prefixEnd);
+        String code = raw.substring(prefixEnd);
+
+        Line highlighted = SyntaxHighlighter.highlightLine(code, 
sourceLanguage);
+
+        // Prepend dim line-number prefix
+        List<Span> spans = new ArrayList<>();
+        if (!prefix.isEmpty()) {
+            spans.add(Span.styled(prefix, Style.EMPTY.dim()));
+        }
+        spans.addAll(highlighted.spans());
+
+        Line full = Line.from(spans);
+
+        // Apply horizontal scroll by skipping characters from spans
+        if (hSkip <= 0) {
+            return full;
+        }
+        List<Span> scrolled = new ArrayList<>();
+        int skipped = 0;
+        for (Span span : full.spans()) {
+            String content = span.content();
+            if (skipped >= hSkip) {
+                scrolled.add(span);
+            } else if (skipped + content.length() > hSkip) {
+                int offset = hSkip - skipped;
+                scrolled.add(Span.styled(content.substring(offset), 
span.style()));
+                skipped = hSkip;
+            } else {
+                skipped += content.length();
+            }
+        }
+        return scrolled.isEmpty() ? Line.from(List.of(Span.raw(""))) : 
Line.from(scrolled);
+    }
+
     // ---- Sorting ----
 
     private int sortRoute(RouteInfo a, RouteInfo b) {
@@ -1081,7 +1126,9 @@ class RoutesTab implements MonitorTab {
             if (!showSource) {
                 return;
             }
-            sourceTitle = location != null ? routeId + "  " + location : 
routeId;
+            String displayLoc = location != null ? 
FileUtil.stripPath(LoggerHelper.sourceNameOnly(location)) : null;
+            sourceTitle = displayLoc != null ? routeId + "  " + displayLoc : 
routeId;
+            sourceLanguage = SyntaxHighlighter.detectLanguage(location);
             sourceLines = lines;
             sourceScroll = scrollTo;
         });
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SyntaxHighlighter.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SyntaxHighlighter.java
new file mode 100644
index 000000000000..dbd3b68ccdd2
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SyntaxHighlighter.java
@@ -0,0 +1,291 @@
+/*
+ * 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.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import dev.tamboui.style.Color;
+import dev.tamboui.style.Style;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import org.apache.camel.util.FileUtil;
+
+class SyntaxHighlighter {
+
+    enum Language {
+        JAVA,
+        YAML,
+        XML,
+        PLAIN
+    }
+
+    // Java patterns (ordered by priority — comments first)
+    private static final Pattern JAVA_LINE_COMMENT = Pattern.compile("//.*");
+    private static final Pattern JAVA_STRING = 
Pattern.compile("\"(?:[^\"\\\\]|\\\\.)*\"");
+    private static final Pattern JAVA_ANNOTATION = Pattern.compile("@\\w+");
+    private static final Pattern JAVA_MODIFIER = Pattern.compile(
+            
"\\b(abstract|class|extends|final|implements|import|instanceof|interface|native|package|private|protected|public|static|strictfp|super|synchronized|throws|volatile|enum|record|sealed|permits|non-sealed)\\b");
+    private static final Pattern JAVA_KEYWORD = Pattern.compile(
+            
"\\b(break|case|catch|continue|default|do|else|finally|for|if|return|switch|throw|try|while|yield|var)\\b");
+    private static final Pattern JAVA_TYPE = Pattern.compile(
+            
"\\b(boolean|byte|char|double|float|int|long|new|short|this|transient|void)\\b");
+    private static final Pattern JAVA_BOOLEAN_NULL = 
Pattern.compile("\\b(true|false|null)\\b");
+    private static final Pattern JAVA_NUMBER = 
Pattern.compile("\\b\\d+\\.?\\d*[fFdDlL]?\\b");
+
+    // YAML patterns
+    private static final Pattern YAML_COMMENT = Pattern.compile("(^|\\s)#.*$");
+    private static final Pattern YAML_KEY = 
Pattern.compile("^(\\s*-?\\s*)([\\w./${}\\-]+)\\s*:");
+    private static final Pattern YAML_BOOLEAN_NULL = 
Pattern.compile(":\\s+(true|false|null)\\s*$");
+    private static final Pattern YAML_NUMBER = 
Pattern.compile(":\\s+(\\d+\\.?\\d*)\\s*$");
+    private static final Pattern YAML_STRING_VALUE = 
Pattern.compile("\"(?:[^\"\\\\]|\\\\.)*\"|'[^']*'");
+
+    // XML patterns
+    private static final Pattern XML_COMMENT = Pattern.compile("<!--.*?-->");
+    private static final Pattern XML_OPEN_TAG = 
Pattern.compile("</?[\\w:.-]+");
+    private static final Pattern XML_CLOSE_BRACKET = Pattern.compile("/?>|>");
+    private static final Pattern XML_ATTR_VALUE = 
Pattern.compile("=\"[^\"]*\"");
+    private static final Pattern XML_ATTR_NAME = 
Pattern.compile("\\s([\\w:.-]+)=");
+    private static final Pattern XML_ENTITY = Pattern.compile("&[^;]+;");
+
+    // Java styles
+    private static final Style JAVA_COMMENT_STYLE = 
Style.EMPTY.fg(Color.LIGHT_BLUE);
+    private static final Style JAVA_STRING_STYLE = Style.EMPTY.fg(Color.RED);
+    private static final Style JAVA_ANNOTATION_STYLE = 
Style.EMPTY.fg(Color.MAGENTA);
+    private static final Style JAVA_MODIFIER_STYLE = 
Style.EMPTY.fg(Color.CYAN);
+    private static final Style JAVA_KEYWORD_STYLE = Style.EMPTY.fg(Color.RED);
+    private static final Style JAVA_TYPE_STYLE = Style.EMPTY.fg(Color.GREEN);
+    private static final Style JAVA_BOOLEAN_STYLE = 
Style.EMPTY.fg(Color.YELLOW);
+    private static final Style JAVA_NUMBER_STYLE = 
Style.EMPTY.fg(Color.YELLOW);
+
+    // YAML styles
+    private static final Style YAML_COMMENT_STYLE = 
Style.EMPTY.fg(Color.LIGHT_BLUE);
+    private static final Style YAML_KEY_STYLE = Style.EMPTY.fg(Color.RED);
+    private static final Style YAML_VALUE_STYLE = Style.EMPTY.fg(Color.GREEN);
+    private static final Style YAML_SPECIAL_STYLE = 
Style.EMPTY.fg(Color.YELLOW);
+    private static final Style YAML_SEPARATOR_STYLE = 
Style.EMPTY.fg(Color.WHITE).bold();
+
+    // XML styles
+    private static final Style XML_COMMENT_STYLE = 
Style.EMPTY.fg(Color.YELLOW);
+    private static final Style XML_TAG_STYLE = Style.EMPTY.fg(Color.CYAN);
+    private static final Style XML_ATTR_NAME_STYLE = 
Style.EMPTY.fg(Color.MAGENTA);
+    private static final Style XML_ATTR_VALUE_STYLE = 
Style.EMPTY.fg(Color.GREEN);
+    private static final Style XML_ENTITY_STYLE = Style.EMPTY.fg(Color.RED);
+
+    private SyntaxHighlighter() {
+    }
+
+    static Language detectLanguage(String filename) {
+        if (filename == null || filename.isEmpty()) {
+            return Language.PLAIN;
+        }
+        // Strip line number suffixes (e.g., "MyRoute.java:42")
+        String name = filename;
+        int colon = name.lastIndexOf(':');
+        if (colon > 0) {
+            String after = name.substring(colon + 1);
+            if (!after.isEmpty() && 
after.chars().allMatch(Character::isDigit)) {
+                name = name.substring(0, colon);
+            }
+        }
+        String ext = FileUtil.onlyExt(name);
+        if (ext == null) {
+            return Language.PLAIN;
+        }
+        ext = ext.toLowerCase();
+        return switch (ext) {
+            case "java" -> Language.JAVA;
+            case "yaml", "yml", "camel.yaml", "camel.yml" -> Language.YAML;
+            case "xml", "camel.xml" -> Language.XML;
+            default -> Language.PLAIN;
+        };
+    }
+
+    static Line highlightLine(String text, Language lang) {
+        if (text == null || text.isEmpty() || lang == Language.PLAIN) {
+            return Line.from(List.of(Span.raw(text != null ? text : "")));
+        }
+
+        return switch (lang) {
+            case JAVA -> highlightJava(text);
+            case YAML -> highlightYaml(text);
+            case XML -> highlightXml(text);
+            default -> Line.from(List.of(Span.raw(text)));
+        };
+    }
+
+    private static Line highlightJava(String text) {
+        int len = text.length();
+        Style[] charStyles = new Style[len];
+
+        // Priority order: comments > strings > annotations > keywords > 
numbers
+        applyPattern(charStyles, text, JAVA_LINE_COMMENT, JAVA_COMMENT_STYLE);
+        applyPattern(charStyles, text, JAVA_STRING, JAVA_STRING_STYLE);
+        applyPattern(charStyles, text, JAVA_ANNOTATION, JAVA_ANNOTATION_STYLE);
+        applyPattern(charStyles, text, JAVA_MODIFIER, JAVA_MODIFIER_STYLE);
+        applyPattern(charStyles, text, JAVA_KEYWORD, JAVA_KEYWORD_STYLE);
+        applyPattern(charStyles, text, JAVA_TYPE, JAVA_TYPE_STYLE);
+        applyPattern(charStyles, text, JAVA_BOOLEAN_NULL, JAVA_BOOLEAN_STYLE);
+        applyPattern(charStyles, text, JAVA_NUMBER, JAVA_NUMBER_STYLE);
+
+        return buildLine(text, charStyles);
+    }
+
+    private static Line highlightYaml(String text) {
+        int len = text.length();
+        Style[] charStyles = new Style[len];
+
+        // Comments have highest priority
+        applyPattern(charStyles, text, YAML_COMMENT, YAML_COMMENT_STYLE);
+
+        // Key portion (before colon)
+        Matcher keyMatcher = YAML_KEY.matcher(text);
+        if (keyMatcher.find()) {
+            int keyStart = keyMatcher.start(2);
+            int keyEnd = keyMatcher.end(2);
+            for (int i = keyStart; i < keyEnd && i < len; i++) {
+                if (charStyles[i] == null) {
+                    charStyles[i] = YAML_KEY_STYLE;
+                }
+            }
+            // Colon separator
+            int colonIdx = text.indexOf(':', keyEnd);
+            if (colonIdx >= 0 && colonIdx < len && charStyles[colonIdx] == 
null) {
+                charStyles[colonIdx] = YAML_SEPARATOR_STYLE;
+            }
+        }
+
+        // String values
+        applyPattern(charStyles, text, YAML_STRING_VALUE, YAML_VALUE_STYLE);
+
+        // Special values (boolean, null, numbers) after colon
+        applyPatternGroup(charStyles, text, YAML_BOOLEAN_NULL, 1, 
YAML_SPECIAL_STYLE);
+        applyPatternGroup(charStyles, text, YAML_NUMBER, 1, 
YAML_SPECIAL_STYLE);
+
+        // List markers
+        Matcher listMarker = Pattern.compile("^(\\s*)(-)(\\s)").matcher(text);
+        if (listMarker.find()) {
+            int dashIdx = listMarker.start(2);
+            if (dashIdx < len && charStyles[dashIdx] == null) {
+                charStyles[dashIdx] = YAML_SEPARATOR_STYLE;
+            }
+        }
+
+        // Value text (after colon+space, non-special, non-quoted)
+        int colonPos = text.indexOf(':');
+        if (colonPos >= 0 && colonPos + 1 < len) {
+            int valueStart = colonPos + 1;
+            while (valueStart < len && text.charAt(valueStart) == ' ') {
+                valueStart++;
+            }
+            if (valueStart < len) {
+                boolean hasSpecial = false;
+                for (int i = valueStart; i < len; i++) {
+                    if (charStyles[i] != null) {
+                        hasSpecial = true;
+                        break;
+                    }
+                }
+                if (!hasSpecial) {
+                    for (int i = valueStart; i < len; i++) {
+                        charStyles[i] = YAML_VALUE_STYLE;
+                    }
+                }
+            }
+        }
+
+        return buildLine(text, charStyles);
+    }
+
+    private static Line highlightXml(String text) {
+        int len = text.length();
+        Style[] charStyles = new Style[len];
+
+        // Comments highest priority
+        applyPattern(charStyles, text, XML_COMMENT, XML_COMMENT_STYLE);
+
+        // Attribute values (before tag names so tags don't override)
+        applyPattern(charStyles, text, XML_ATTR_VALUE, XML_ATTR_VALUE_STYLE);
+
+        // Attribute names
+        Matcher attrMatcher = XML_ATTR_NAME.matcher(text);
+        while (attrMatcher.find()) {
+            int start = attrMatcher.start(1);
+            int end = attrMatcher.end(1);
+            for (int i = start; i < end; i++) {
+                if (charStyles[i] == null) {
+                    charStyles[i] = XML_ATTR_NAME_STYLE;
+                }
+            }
+        }
+
+        // Tag names
+        applyPattern(charStyles, text, XML_OPEN_TAG, XML_TAG_STYLE);
+        applyPattern(charStyles, text, XML_CLOSE_BRACKET, XML_TAG_STYLE);
+
+        // Entity references
+        applyPattern(charStyles, text, XML_ENTITY, XML_ENTITY_STYLE);
+
+        return buildLine(text, charStyles);
+    }
+
+    private static void applyPattern(Style[] charStyles, String text, Pattern 
pattern, Style style) {
+        Matcher m = pattern.matcher(text);
+        while (m.find()) {
+            for (int i = m.start(); i < m.end(); i++) {
+                if (charStyles[i] == null) {
+                    charStyles[i] = style;
+                }
+            }
+        }
+    }
+
+    private static void applyPatternGroup(Style[] charStyles, String text, 
Pattern pattern, int group, Style style) {
+        Matcher m = pattern.matcher(text);
+        while (m.find()) {
+            for (int i = m.start(group); i < m.end(group); i++) {
+                if (charStyles[i] == null) {
+                    charStyles[i] = style;
+                }
+            }
+        }
+    }
+
+    private static Line buildLine(String text, Style[] charStyles) {
+        List<Span> spans = new ArrayList<>();
+        int len = text.length();
+        int i = 0;
+
+        while (i < len) {
+            Style current = charStyles[i];
+            int start = i;
+            while (i < len && charStyles[i] == current) {
+                i++;
+            }
+            String segment = text.substring(start, i);
+            if (current != null) {
+                spans.add(Span.styled(segment, current));
+            } else {
+                spans.add(Span.raw(segment));
+            }
+        }
+
+        return spans.isEmpty() ? Line.from(List.of(Span.raw(text))) : 
Line.from(spans);
+    }
+}

Reply via email to