This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch feature/CAMEL-23771-tui-dsl-format-tabs in repository https://gitbox.apache.org/repos/asf/camel.git
commit 31572e28e855cd0df74ef68d088cbb174db265e9 Author: Claus Ibsen <[email protected]> AuthorDate: Tue Jun 16 16:38:21 2026 +0200 CAMEL-23771: Add DSL format tabs to TUI source viewer The source viewer (c key) now shows YAML/Java/XML format tabs in the title bar when viewing route source code. Press Y/J/X to switch between DSL representations. The original source format is marked with *. Original tab loads from SourceDevConsole (real file), converted tabs load from RouteDumpDevConsole (model dump). Format tabs only appear for route source viewing, not generic file viewing. Co-Authored-By: Claude <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../dsl/jbang/core/commands/tui/SourceViewer.java | 223 ++++++++++++++++++++- 1 file changed, 222 insertions(+), 1 deletion(-) 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 8a10d434c6a3..03e6d3b6132a 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 @@ -39,6 +39,7 @@ import dev.tamboui.tui.event.KeyCode; import dev.tamboui.tui.event.KeyEvent; 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 dev.tamboui.widgets.scrollbar.Scrollbar; import dev.tamboui.widgets.scrollbar.ScrollbarState; @@ -74,6 +75,11 @@ class SourceViewer { private final Map<String, CachedSource> sourceCache = new ConcurrentHashMap<>(); private boolean wordWrap; private final SearchHighlighter search = new SearchHighlighter(); + private String currentFormat; + private String originalFormat; + private String currentRouteId; + private MonitorContext currentCtx; + private String currentPid; private record CachedSource( List<String> lines, List<JsonObject> codeData, @@ -102,6 +108,11 @@ class SourceViewer { sourceCache.clear(); wordWrap = false; search.reset(); + currentFormat = null; + originalFormat = null; + currentRouteId = null; + currentCtx = null; + currentPid = null; } void setOnLineSelected(IntConsumer callback) { @@ -133,6 +144,20 @@ class SourceViewer { onLineSelected = null; return true; } + if (currentRouteId != null) { + if (ke.isChar('Y') || ke.isChar('y')) { + switchFormat("yaml"); + return true; + } + if (ke.isChar('J') || ke.isChar('j')) { + switchFormat("java"); + return true; + } + if (ke.isChar('X') || ke.isChar('x')) { + switchFormat("xml"); + return true; + } + } if (search.handleKeyEvent(ke)) { int matchLine = search.currentMatchLine(); if (matchLine >= 0) { @@ -199,7 +224,7 @@ class SourceViewer { void render(Frame frame, Rect area) { Block block = Block.builder() .borderType(BorderType.ROUNDED) - .title(" Source [" + (title != null ? title : "") + "] ") + .title(buildTitle()) .build(); Rect inner = block.inner(area); frame.renderWidget(block, area); @@ -282,6 +307,11 @@ class SourceViewer { MonitorContext.hint(spans, "Esc/c", "close"); } MonitorContext.hint(spans, "↑↓", "navigate"); + if (currentRouteId != null) { + MonitorContext.hint(spans, "Y", "yaml"); + MonitorContext.hint(spans, "J", "java"); + MonitorContext.hint(spans, "X", "xml"); + } search.renderSearchHints(spans); MonitorContext.hint(spans, "w", "wrap" + (wordWrap ? " [on]" : " [off]")); if (!wordWrap) { @@ -297,6 +327,11 @@ class SourceViewer { * Load source for a route, scrolling to the given source line number. */ void loadFile(Path filePath) { + currentRouteId = null; + currentFormat = null; + originalFormat = null; + currentCtx = null; + currentPid = null; String fileName = filePath.getFileName().toString(); try { List<String> rawLines = java.nio.file.Files.readAllLines(filePath, java.nio.charset.StandardCharsets.UTF_8); @@ -342,6 +377,9 @@ class SourceViewer { } String pid = ctx.selectedPid; + currentRouteId = routeId; + currentCtx = ctx; + currentPid = pid; String cacheKey = pid + ":" + routeId; CachedSource cached = sourceCache.get(cacheKey); if (cached == null && sourceLocationHint != null) { @@ -407,6 +445,11 @@ class SourceViewer { scrollX = 0; pendingScroll = true; visible = true; + String fmt = languageToFormat(cached.language); + if (fmt != null) { + originalFormat = fmt; + currentFormat = fmt; + } } private void loadInBackground(MonitorContext ctx, String pid, String routeId, int targetLine) { @@ -489,6 +532,10 @@ class SourceViewer { if (sourceLocation != null) { sourceCache.put(pid + ":loc:" + sourceLocation, cached); } + String fmt = languageToFormat(lang); + if (fmt != null) { + sourceCache.put(pid + ":" + routeId + ":" + fmt, cached); + } applyResult(ctx, routeId, sourceLocation, result, codeLines, scrollTo, cursorLine); } @@ -511,9 +558,183 @@ class SourceViewer { selectedLine = Math.max(0, cursorLine); scrollY = 0; pendingScroll = true; + if (currentRouteId != null) { + String fmt = languageToFormat(language); + if (fmt != null) { + originalFormat = fmt; + currentFormat = fmt; + } + } + }); + } + + private Title buildTitle() { + String info = title != null ? title : ""; + if (currentRouteId == null) { + return Title.from(Line.from(List.of(Span.raw(" Source [" + info + "] ")))); + } + + List<Span> spans = new ArrayList<>(); + spans.add(Span.raw(" Source [" + info + "] ")); + + String[] formats = { "yaml", "java", "xml" }; + String[] labels = { "YAML", "Java", "XML" }; + + for (int i = 0; i < formats.length; i++) { + if (i > 0) { + spans.add(Span.styled(" │ ", Style.EMPTY.dim())); + } + String label = labels[i]; + if (formats[i].equals(originalFormat)) { + label += "*"; + } + if (formats[i].equals(currentFormat)) { + spans.add(Span.styled(label, Style.EMPTY.bold())); + } else { + spans.add(Span.styled(label, Style.EMPTY.dim())); + } + } + spans.add(Span.raw(" ")); + + return Title.from(Line.from(spans)); + } + + private void switchFormat(String format) { + if (format.equals(currentFormat)) { + return; + } + if (currentPid == null || currentRouteId == null || currentCtx == null) { + return; + } + + String cacheKey = currentPid + ":" + currentRouteId + ":" + format; + CachedSource cached = sourceCache.get(cacheKey); + if (cached != null) { + lines = cached.lines; + codeData = cached.codeData; + language = cached.language; + currentFormat = format; + selectedLine = findLicenseHeaderEnd(codeData); + scrollY = 0; + scrollX = 0; + pendingScroll = true; + search.reset(); + return; + } + + if (!loading.compareAndSet(false, true)) { + return; + } + + lines = List.of("(Loading " + format + " format...)"); + currentFormat = format; + scrollY = 0; + scrollX = 0; + + MonitorContext ctx = currentCtx; + String pid = currentPid; + String routeId = currentRouteId; + ctx.runner.scheduler().execute(() -> { + try { + if (format.equals(originalFormat)) { + loadInBackground(ctx, pid, routeId, 0); + } else { + loadFormatInBackground(ctx, pid, routeId, format); + } + } finally { + loading.set(false); + } + }); + } + + private void loadFormatInBackground(MonitorContext ctx, String pid, String routeId, String format) { + Path outputFile = ctx.getOutputFile(pid); + PathUtils.deleteFile(outputFile); + + JsonObject root = new JsonObject(); + root.put("action", "route-dump"); + root.put("filter", routeId); + root.put("format", format); + + Path actionFile = ctx.getActionFile(pid); + PathUtils.writeTextSafely(root.toJson(), actionFile); + + JsonObject jo = pollJsonResponse(outputFile, 5000); + PathUtils.deleteFile(outputFile); + + if (jo == null) { + applyFormatResult(ctx, format, List.of("(No response from integration)"), Collections.emptyList()); + return; + } + + JsonArray routes = (JsonArray) jo.get("routes"); + if (routes == null || routes.isEmpty()) { + applyFormatResult(ctx, format, + List.of("(No dump available for route: " + routeId + ")"), Collections.emptyList()); + return; + } + + JsonObject routeObj = (JsonObject) routes.get(0); + List<JsonObject> codeLines = routeObj.getCollection("code"); + if (codeLines == null || codeLines.isEmpty()) { + applyFormatResult(ctx, format, List.of("(No code available)"), Collections.emptyList()); + return; + } + + List<String> result = new ArrayList<>(); + int lineNumWidth = String.valueOf(codeLines.size()).length(); + for (int i = 0; i < codeLines.size(); i++) { + String code = Jsoner.unescape(objToString(codeLines.get(i).get("code"))); + result.add(String.format("%" + lineNumWidth + "d %s", i + 1, code)); + } + + SyntaxHighlighter.Language lang = formatToLanguage(format); + CachedSource cached = new CachedSource(result, codeLines, null, lang); + sourceCache.put(pid + ":" + routeId + ":" + format, cached); + + applyFormatResult(ctx, format, result, codeLines); + } + + private void applyFormatResult( + MonitorContext ctx, String format, + List<String> resultLines, List<JsonObject> codeLines) { + if (ctx.runner == null) { + return; + } + ctx.runner.runOnRenderThread(() -> { + if (!visible) { + return; + } + language = formatToLanguage(format); + lines = resultLines; + codeData = codeLines; + currentFormat = format; + selectedLine = findLicenseHeaderEnd(codeLines); + scrollY = 0; + scrollX = 0; + pendingScroll = true; + search.reset(); }); } + private static String languageToFormat(SyntaxHighlighter.Language lang) { + return switch (lang) { + case YAML -> "yaml"; + case JAVA -> "java"; + case XML -> "xml"; + default -> null; + }; + } + + private static SyntaxHighlighter.Language formatToLanguage(String format) { + return switch (format) { + case "yaml" -> SyntaxHighlighter.Language.YAML; + case "java" -> SyntaxHighlighter.Language.JAVA; + case "xml" -> SyntaxHighlighter.Language.XML; + default -> SyntaxHighlighter.Language.PLAIN; + }; + } + private Line highlightSourceLine(String raw, int hSkip, boolean isSelected, int viewportWidth) { int prefixEnd = 0; while (prefixEnd < raw.length() && (raw.charAt(prefixEnd) == ' ' || Character.isDigit(raw.charAt(prefixEnd)))) {
