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 51785f2e967f CAMEL-24259: camel-jbang - TUI add Source tab with file
explorer and inline docs
51785f2e967f is described below
commit 51785f2e967f4ad91009aabb1de9e54182760c44
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Jul 26 15:58:16 2026 +0200
CAMEL-24259: camel-jbang - TUI add Source tab with file explorer and inline
docs
Replace HTTP as a primary tab with a new Source tab (key 2) featuring
a left-right split layout with file explorer and source viewer. HTTP
moves to the More submenu with cycling duplicate mnemonic support.
- File explorer with directory navigation, info panel showing
size/modified/type
- Source viewer with syntax highlighting and Space to cycle format
(YAML/Java/XML)
- Tab key toggles focus between panels with dimmed selection on unfocused
panel
- Inline Camel documentation via i toggle for Camel source files
- F1 help text and enriched JSON data for AI agents
- Remove Tab/Shift+Tab cycling between primary tabs (use number keys
instead)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../dsl/jbang/core/commands/tui/CamelMonitor.java | 66 +-
.../dsl/jbang/core/commands/tui/McpFacade.java | 4 +-
.../dsl/jbang/core/commands/tui/PopupManager.java | 33 +-
.../dsl/jbang/core/commands/tui/SourceTab.java | 679 +++++++++++++++++++++
.../dsl/jbang/core/commands/tui/SourceViewer.java | 37 +-
.../dsl/jbang/core/commands/tui/TabRegistry.java | 35 +-
.../dsl/jbang/core/commands/tui/TuiHelper.java | 4 +-
.../dsl/jbang/core/commands/tui/TuiIcons.java | 5 +-
.../jbang/core/commands/tui/TabBarRenderTest.java | 18 +-
.../jbang/core/commands/tui/TabRegistryTest.java | 40 +-
.../dsl/jbang/core/commands/tui/TuiIconsTest.java | 2 +-
11 files changed, 816 insertions(+), 107 deletions(-)
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
index 403d094ff55b..14f4418c8838 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
@@ -691,8 +691,11 @@ public class CamelMonitor extends CamelCommand {
}
return true;
}
- boolean probeEditing = tabRegistry.selectedTabIndex() == TAB_HTTP
+ boolean probeEditing = tabRegistry.selectedTabIndex() == TAB_MORE
+ && tabRegistry.getActiveMoreTab() == tabRegistry.httpTab()
&& tabRegistry.httpTab().isProbeMode();
+ boolean sourceSearchActive = tabRegistry.selectedTabIndex() ==
TAB_SOURCE
+ && tabRegistry.sourceTab().isSourceViewerSearchActive();
boolean logSearchActive = tabRegistry.selectedTabIndex() == TAB_LOG
&& tabRegistry.logTab().isSearchInputActive();
boolean spanFilterActive = tabRegistry.selectedTabIndex() == TAB_MORE
@@ -713,8 +716,9 @@ public class CamelMonitor extends CamelCommand {
boolean catalogFilterActive = tabRegistry.selectedTabIndex() ==
TAB_MORE
&& tabRegistry.getActiveMoreTab() == tabRegistry.catalogTab()
&& tabRegistry.catalogTab().isFilterInputActive();
- boolean textEditing = probeEditing || logSearchActive ||
spanFilterActive || beanFilterActive
- || classpathFilterActive || mavenDepsFilterActive ||
sqlInputActive || catalogFilterActive;
+ boolean textEditing = probeEditing || sourceSearchActive ||
logSearchActive || spanFilterActive
+ || beanFilterActive || classpathFilterActive ||
mavenDepsFilterActive || sqlInputActive
+ || catalogFilterActive;
if (!textEditing && (ke.isCharIgnoreCase('q') || ke.isCtrlC())) {
runner.quit();
return true;
@@ -728,23 +732,23 @@ public class CamelMonitor extends CamelCommand {
return tabRegistry.handleTabKey(TAB_OVERVIEW, ctx,
dataService);
}
if (ke.isChar('2')) {
+ return tabRegistry.handleTabKey(TAB_SOURCE, ctx, dataService);
+ }
+ if (ke.isChar('3')) {
return tabRegistry.handleTabKey(TAB_LOG, ctx, dataService);
}
if (!isInfraSelected()) {
- if (ke.isChar('3')) {
- return tabRegistry.handleTabKey(TAB_ACTIVITY, ctx,
dataService);
- }
if (ke.isChar('4')) {
- return tabRegistry.handleTabKey(TAB_DIAGRAM, ctx,
dataService);
+ return tabRegistry.handleTabKey(TAB_ACTIVITY, ctx,
dataService);
}
if (ke.isChar('5')) {
- return tabRegistry.handleTabKey(TAB_ROUTES, ctx,
dataService);
+ return tabRegistry.handleTabKey(TAB_DIAGRAM, ctx,
dataService);
}
if (ke.isChar('6')) {
- return tabRegistry.handleTabKey(TAB_ENDPOINTS, ctx,
dataService);
+ return tabRegistry.handleTabKey(TAB_ROUTES, ctx,
dataService);
}
if (ke.isChar('7')) {
- return tabRegistry.handleTabKey(TAB_HTTP, ctx,
dataService);
+ return tabRegistry.handleTabKey(TAB_ENDPOINTS, ctx,
dataService);
}
if (ke.isChar('8')) {
return tabRegistry.handleTabKey(TAB_HISTORY, ctx,
dataService);
@@ -759,26 +763,6 @@ public class CamelMonitor extends CamelCommand {
}
MonitorTab activeMonitorTab = tabRegistry.activeTab();
boolean overlayActive = activeMonitorTab != null &&
activeMonitorTab.isOverlayActive();
- if (ke.isFocusPrevious() && !textEditing && !overlayActive) {
- if (isInfraSelected()) {
- int prev = tabRegistry.selectedTabIndex() == TAB_OVERVIEW ?
TAB_LOG : TAB_OVERVIEW;
- tabsState.select(prev);
- } else {
- int prev = (tabRegistry.selectedTabIndex() - 1 + NUM_TABS) %
NUM_TABS;
- tabRegistry.handleTabKey(prev, ctx, dataService);
- }
- return true;
- }
- if (ke.isFocusNext() && !textEditing && !overlayActive) {
- if (isInfraSelected()) {
- int next = tabRegistry.selectedTabIndex() == TAB_OVERVIEW ?
TAB_LOG : TAB_OVERVIEW;
- tabsState.select(next);
- } else {
- int next = (tabRegistry.selectedTabIndex() + 1) % NUM_TABS;
- tabRegistry.handleTabKey(next, ctx, dataService);
- }
- return true;
- }
if (ke.isKey(KeyCode.F5) && ke.hasShift()) {
recordingManager.takeScreenshot();
return true;
@@ -1164,6 +1148,10 @@ public class CamelMonitor extends CamelCommand {
filesBrowser.handlePaste(pe.text());
return true;
}
+ if (tabRegistry.sourceTab().isSourceViewerSearchActive()) {
+ tabRegistry.sourceTab().handlePaste(pe.text());
+ return true;
+ }
if (tabRegistry.getActiveMoreTab() == tabRegistry.sqlQueryTab()
&& tabRegistry.sqlQueryTab().isInputActive()) {
tabRegistry.sqlQueryTab().handlePaste(pe.text());
@@ -1474,7 +1462,7 @@ public class CamelMonitor extends CamelCommand {
// Infra mode: only Overview and Log tabs
Line[] labels = {
Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_OVERVIEW,
"1", "Overview")),
- Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_LOG, "2",
"Log"))
+ Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_LOG, "3",
"Log"))
};
// Map real tab index to infra tab index for highlight
@@ -1498,12 +1486,12 @@ public class CamelMonitor extends CamelCommand {
Line[] labels = {
Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_OVERVIEW,
"1", "Overview")),
- Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_LOG, "2",
"Log")),
- Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_ACTIVITY,
"3", "Activity")),
- Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_DIAGRAM, "4",
"Diagram")),
- Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_ROUTES, "5",
"Route")),
- Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_ENDPOINTS,
"6", "Endpoint")),
- Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_HTTP, "7",
"HTTP")),
+ Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_SOURCE, "2",
"Source")),
+ Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_LOG, "3",
"Log")),
+ Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_ACTIVITY,
"4", "Activity")),
+ Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_DIAGRAM, "5",
"Diagram")),
+ Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_ROUTES, "6",
"Route")),
+ Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_ENDPOINTS,
"7", "Endpoint")),
Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_INSPECT, "8",
"Inspect")),
Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_ERRORS, "9",
"Errors")),
Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_MORE, "0",
TuiIcons.moreTabLabel())),
@@ -1615,10 +1603,6 @@ public class CamelMonitor extends CamelCommand {
if (endpointCount > 0) {
badgeTexts[TAB_ENDPOINTS] = "(" + endpointCount + ")";
}
- int httpCount = hasSelection ? sel.httpEndpoints.size() : 0;
- if (httpCount > 0) {
- badgeTexts[TAB_HTTP] = "(" + httpCount + ")";
- }
boolean hasTraces = hasSelection &&
!dataService.traces().get().isEmpty();
if (hasTraces) {
badgeTexts[TAB_HISTORY] = "(*)";
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
index 1aef9c262ee7..3f7151ea6c89 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
@@ -83,8 +83,8 @@ class McpFacade {
// Tab name constants
static final String[] TAB_NAMES = {
- "Overview", "Log", "Activity", "Diagram", "Routes",
- "Endpoints", "HTTP", "Inspect", "Errors", "More"
+ "Overview", "Source", "Log", "Activity", "Diagram",
+ "Routes", "Endpoints", "Inspect", "Errors", "More"
};
private final MonitorContext ctx;
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java
index a880e3cbf78c..3992063df94f 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java
@@ -77,6 +77,8 @@ class PopupManager {
private final ListState morePopupState = new ListState();
private final ScrollbarState moreScrollbarState = new ScrollbarState();
private int lastMoreSelection;
+ private char lastShortcutLetter;
+ private int lastShortcutIndex = -1;
private Line[] currentTabLabels;
// Kill confirm
@@ -654,18 +656,43 @@ class PopupManager {
int morePopupShortcut(KeyEvent ke) {
List<TabRegistry.MoreTab> tabs = moreTabsSupplier.get();
+ List<Integer> matches = new ArrayList<>();
+ char pressedUpper = 0;
+
for (int i = 0; i < tabs.size(); i++) {
int idx = tabs.get(i).mnemonicIndex();
if (idx < 0) {
continue;
}
char letter = tabs.get(i).displayName().charAt(idx);
- // trigger on either case so Shift+letter works too
if (ke.isChar(Character.toLowerCase(letter)) ||
ke.isChar(Character.toUpperCase(letter))) {
- return i;
+ if (pressedUpper == 0) {
+ pressedUpper = Character.toUpperCase(letter);
+ }
+ matches.add(i);
}
}
- return -1;
+
+ if (matches.isEmpty()) {
+ return -1;
+ }
+ if (matches.size() == 1) {
+ lastShortcutLetter = pressedUpper;
+ lastShortcutIndex = matches.get(0);
+ return matches.get(0);
+ }
+
+ // cycle through duplicate mnemonics
+ if (pressedUpper == lastShortcutLetter && lastShortcutIndex >= 0) {
+ int pos = matches.indexOf(lastShortcutIndex);
+ int next = (pos + 1) % matches.size();
+ lastShortcutIndex = matches.get(next);
+ return matches.get(next);
+ }
+
+ lastShortcutLetter = pressedUpper;
+ lastShortcutIndex = matches.get(0);
+ return matches.get(0);
}
List<IntegrationInfo> getNonVanishingIntegrations() {
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
new file mode 100644
index 000000000000..e71a8d621e58
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
@@ -0,0 +1,679 @@
+/*
+ * 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.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Layout;
+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.tui.event.MouseEvent;
+import dev.tamboui.tui.event.MouseEventKind;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Borders;
+import dev.tamboui.widgets.block.Title;
+import dev.tamboui.widgets.list.ListItem;
+import dev.tamboui.widgets.list.ListState;
+import dev.tamboui.widgets.list.ListWidget;
+import dev.tamboui.widgets.list.ScrollMode;
+import dev.tamboui.widgets.paragraph.Paragraph;
+import dev.tamboui.widgets.scrollbar.Scrollbar;
+import dev.tamboui.widgets.scrollbar.ScrollbarState;
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.dsl.jbang.core.common.CatalogLoader;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+/**
+ * Source tab showing a file browser in the top panel and a source code viewer
in the bottom panel. Replaces the HTTP
+ * tab as primary tab 7.
+ */
+class SourceTab extends AbstractTab {
+
+ private Path rootDir;
+ private Path currentDir;
+ private final ListState listState = new ListState();
+ private final ScrollbarState listScrollState = new ScrollbarState();
+ private List<FilesBrowser.FileEntry> entries = Collections.emptyList();
+ private final SourceViewer sourceViewer = new SourceViewer();
+ private boolean focusOnViewer;
+
+ private int leftPanelWidth = -1;
+ private final DragSplit hSplit = new DragSplit();
+ private Rect leftArea;
+ private Rect rightArea;
+
+ private final Map<String, CamelCatalog> catalogCache = new HashMap<>();
+
+ 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(
+
"^\\s*-?\\s*(aggregate|bean|choice|circuitBreaker|claim[Cc]heck|convertBodyTo|convertHeaderTo|"
+ +
"delay|dynamicRouter|enrich|filter|idempotentConsumer|"
+ +
"intercept|kamelet|loadBalance|log|loop|marshal|multicast|"
+ +
"onException|otherwise|pipeline|pollEnrich|process|"
+ +
"recipientList|removeHeader|removeHeaders|removeProperty|"
+ +
"resequence|rollback|routingSlip|saga|sample|"
+ +
"script|serviceCall|setBody|setHeader|setProperty|"
+ +
"sort|split|step|stop|threads|throttle|"
+ +
"to|toD|transform|unmarshal|validate|when|wireTap)\\s*:");
+
+ SourceTab(MonitorContext ctx) {
+ super(ctx);
+ }
+
+ boolean isSourceViewerSearchActive() {
+ return sourceViewer.isSearchInputActive();
+ }
+
+ void handlePaste(String text) {
+ sourceViewer.handlePaste(text);
+ }
+
+ // ---- MonitorTab ----
+
+ @Override
+ public void onTabSelected() {
+ refreshFiles();
+ }
+
+ @Override
+ public void onIntegrationChanged() {
+ rootDir = null;
+ currentDir = null;
+ entries = Collections.emptyList();
+ sourceViewer.reset();
+ focusOnViewer = false;
+ leftPanelWidth = -1;
+ refreshFiles();
+ }
+
+ @Override
+ public boolean handleKeyEvent(KeyEvent ke) {
+ if (ke.isKey(dev.tamboui.tui.event.KeyCode.TAB)) {
+ if (sourceViewer.isVisible()) {
+ focusOnViewer = !focusOnViewer;
+ }
+ return true;
+ }
+
+ if (focusOnViewer && sourceViewer.isVisible()) {
+ boolean wasVisible = sourceViewer.isVisible();
+ if (sourceViewer.handleKeyEvent(ke)) {
+ if (wasVisible && !sourceViewer.isVisible()) {
+ focusOnViewer = false;
+ }
+ return true;
+ }
+ if (ke.isCancel()) {
+ focusOnViewer = false;
+ return true;
+ }
+ }
+
+ if (!focusOnViewer) {
+ return handleFileListKey(ke);
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean handleMouseEvent(MouseEvent me, Rect area) {
+ if (hSplit.handleMouse(me, me.x())) {
+ if (hSplit.isDragging() && me.kind() == MouseEventKind.DRAG) {
+ leftPanelWidth = Math.max(15, Math.min(me.x() - area.x(),
area.width() - 20));
+ }
+ return true;
+ }
+
+ if (leftArea != null && TuiHelper.contains(leftArea, me.x(), me.y())) {
+ focusOnViewer = false;
+ if (me.kind() == MouseEventKind.SCROLL_UP) {
+ listState.selectPrevious();
+ return true;
+ }
+ if (me.kind() == MouseEventKind.SCROLL_DOWN) {
+ listState.selectNext(entries.size());
+ return true;
+ }
+ if (me.isClick()) {
+ int innerTop = leftArea.top() + 1;
+ int clicked = listState.offset() + (me.y() - innerTop);
+ if (clicked >= 0 && clicked < entries.size()) {
+ listState.select(clicked);
+ }
+ return true;
+ }
+ return true;
+ }
+
+ if (rightArea != null && TuiHelper.contains(rightArea, me.x(),
me.y())) {
+ if (sourceViewer.isVisible()) {
+ focusOnViewer = true;
+ return sourceViewer.handleMouseEvent(me);
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean handleEscape() {
+ if (sourceViewer.isSearchInputActive()) {
+ return false;
+ }
+ if (focusOnViewer) {
+ focusOnViewer = false;
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void navigateUp() {
+ listState.selectPrevious();
+ }
+
+ @Override
+ public void navigateDown() {
+ listState.selectNext(entries.size());
+ }
+
+ @Override
+ public void render(Frame frame, Rect area) {
+ if (ctx.selectedPid == null) {
+ renderNoSelection(frame, area);
+ return;
+ }
+
+ if (leftPanelWidth < 0) {
+ leftPanelWidth = Math.max(25, Math.min(35, area.width() * 25 /
100));
+ }
+ leftPanelWidth = Math.max(15, Math.min(leftPanelWidth, area.width() -
20));
+
+ List<Rect> chunks = Layout.horizontal()
+ .constraints(Constraint.length(leftPanelWidth),
Constraint.fill())
+ .split(area);
+
+ leftArea = chunks.get(0);
+ rightArea = chunks.get(1);
+
+ int infoHeight = 6;
+ List<Rect> leftChunks = Layout.vertical()
+ .constraints(Constraint.fill(), Constraint.length(infoHeight))
+ .split(leftArea);
+
+ renderFileList(frame, leftChunks.get(0));
+ renderInfoPanel(frame, leftChunks.get(1));
+ hSplit.setBorderPos(rightArea.x());
+ renderSourcePanel(frame, rightArea);
+ }
+
+ @Override
+ public void renderFooter(List<Span> spans) {
+ if (focusOnViewer && sourceViewer.isVisible()) {
+ sourceViewer.renderFooter(spans);
+ TuiHelper.hint(spans, "Tab", "files");
+ } else {
+ TuiHelper.hint(spans, TuiIcons.HINT_SCROLL, "navigate");
+ TuiHelper.hint(spans, "Enter", "open");
+ if (currentDir != null && rootDir != null &&
!currentDir.equals(rootDir)) {
+ TuiHelper.hint(spans, "Bksp", "parent");
+ }
+ if (sourceViewer.isVisible()) {
+ TuiHelper.hint(spans, "Tab", "viewer");
+ }
+ }
+ }
+
+ @Override
+ public String description() {
+ return "Browse and view source files of the integration";
+ }
+
+ @Override
+ public String getHelpText() {
+ return """
+ # Source
+
+ Browse the source files of the selected integration and view
their content
+ with syntax highlighting. The tab has a left-right split
layout with a file
+ explorer on the left and a source viewer on the right.
+
+ ## File List (left panel)
+ - **Up/Down** — navigate files
+ - **Enter** — open file or directory
+ - **Backspace** — go to parent directory
+
+ ## Source Viewer (right panel)
+ - **Up/Down** — scroll through source code
+ - **Space** — cycle format (YAML/Java/XML) for Camel routes
+ - **i** — toggle inline Camel documentation for Camel source
files
+ - **/** — search in source
+ - **h** — highlight text
+ - **n/N** — next/previous match
+ - **w** — toggle word wrap
+ - **Esc/c** — close source viewer
+
+ ## General
+ - **Tab** — toggle focus between file list and source viewer
+ - The focused panel title is highlighted; the unfocused panel
dims
+ - Drag the split border with the mouse to resize panels
+ """;
+ }
+
+ @Override
+ public boolean isOverlayActive() {
+ return focusOnViewer && sourceViewer.isSearchInputActive();
+ }
+
+ @Override
+ public JsonObject getTableDataAsJson() {
+ JsonObject json = new JsonObject();
+ json.put("currentDir", currentDir != null ? currentDir.toString() :
null);
+
+ var filesArray = new JsonArray();
+ for (FilesBrowser.FileEntry entry : entries) {
+ JsonObject file = new JsonObject();
+ file.put("name", entry.name());
+ file.put("directory", entry.directory());
+ if (!entry.directory()) {
+ file.put("size", entry.size());
+ file.put("sizeFormatted",
FilesBrowser.formatFileSize(entry.size()));
+ }
+ file.put("path", entry.path());
+ filesArray.add(file);
+ }
+ json.put("files", filesArray);
+
+ Integer sel = listState.selected();
+ if (sel != null && sel < entries.size()) {
+ FilesBrowser.FileEntry selected = entries.get(sel);
+ json.put("selectedFile", selected.name());
+ json.put("selectedIndex", sel);
+ Path path = Path.of(selected.path());
+ json.put("isCamelSource", isCamelSourceFile(path));
+ try {
+ BasicFileAttributes attrs = Files.readAttributes(path,
BasicFileAttributes.class);
+ if (!selected.directory()) {
+ json.put("fileSize", attrs.size());
+ json.put("fileSizeFormatted",
FilesBrowser.formatFileSize(attrs.size()));
+ int dot = selected.name().lastIndexOf('.');
+ if (dot > 0) {
+ json.put("fileType", selected.name().substring(dot +
1));
+ }
+ }
+ json.put("lastModified", attrs.lastModifiedTime().toInstant()
+ .atZone(ZoneId.systemDefault()).format(DATE_FMT));
+ } catch (IOException e) {
+ // ignore
+ }
+ }
+ json.put("totalFiles", entries.size());
+ json.put("focusOnViewer", focusOnViewer);
+ json.put("viewerOpen", sourceViewer.isVisible());
+ return json;
+ }
+
+ // ---- Internals ----
+
+ private void refreshFiles() {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ if (info == null) {
+ return;
+ }
+ Path dir = FilesBrowser.resolveSourceDirectory(info);
+ if (dir == null || !Files.isDirectory(dir)) {
+ return;
+ }
+ if (rootDir == null) {
+ rootDir = dir;
+ currentDir = dir;
+ }
+ if (currentDir != null) {
+ loadDirectory(currentDir);
+ }
+ }
+
+ private boolean loadDirectory(Path dir) {
+ List<FilesBrowser.FileEntry> dirs = new ArrayList<>();
+ List<FilesBrowser.FileEntry> files = new ArrayList<>();
+ try (var stream = Files.list(dir)) {
+ stream.limit(200)
+ .forEach(p -> {
+ String name = p.getFileName().toString();
+ if (Files.isDirectory(p) && !name.startsWith(".")) {
+ dirs.add(new
FilesBrowser.FileEntry(TuiIcons.FOLDER, name, -1, p.toString(), true));
+ } else if (Files.isRegularFile(p)) {
+ String emoji = TuiHelper.fileEmoji(p);
+ long size = 0;
+ try {
+ size = Files.size(p);
+ } catch (IOException e) {
+ // ignore
+ }
+ files.add(new FilesBrowser.FileEntry(emoji, name,
size, p.toString(), false));
+ }
+ });
+ } catch (IOException e) {
+ return false;
+ }
+ dirs.sort(Comparator.comparing(FilesBrowser.FileEntry::name,
String.CASE_INSENSITIVE_ORDER));
+ files.sort(Comparator.comparing(FilesBrowser.FileEntry::name,
String.CASE_INSENSITIVE_ORDER));
+
+ List<FilesBrowser.FileEntry> found = new ArrayList<>();
+ if (!dir.equals(rootDir)) {
+ found.add(new FilesBrowser.FileEntry(TuiIcons.FOLDER, "..", -1,
dir.getParent().toString(), true));
+ }
+ found.addAll(dirs);
+ found.addAll(files);
+
+ if (found.isEmpty()) {
+ return false;
+ }
+ entries = found;
+ listState.select(0);
+ currentDir = dir;
+ return true;
+ }
+
+ private boolean handleFileListKey(KeyEvent ke) {
+ if (ke.isUp()) {
+ listState.selectPrevious();
+ return true;
+ }
+ if (ke.isDown()) {
+ listState.selectNext(entries.size());
+ return true;
+ }
+ if (ke.isPageUp()) {
+ for (int i = 0; i < 20; i++) {
+ listState.selectPrevious();
+ }
+ return true;
+ }
+ if (ke.isPageDown()) {
+ for (int i = 0; i < 20; i++) {
+ listState.selectNext(entries.size());
+ }
+ return true;
+ }
+ if (ke.isHome()) {
+ listState.select(0);
+ return true;
+ }
+ if (ke.isEnd()) {
+ listState.select(entries.size() - 1);
+ return true;
+ }
+ if (ke.isDeleteBackward()) {
+ if (currentDir != null && !currentDir.equals(rootDir)) {
+ loadDirectory(currentDir.getParent());
+ }
+ return true;
+ }
+ if (ke.isConfirm()) {
+ openSelectedEntry();
+ return true;
+ }
+ return false;
+ }
+
+ private void openSelectedEntry() {
+ Integer sel = listState.selected();
+ if (sel != null && sel < entries.size()) {
+ FilesBrowser.FileEntry entry = entries.get(sel);
+ if (entry.directory()) {
+ loadDirectory(Path.of(entry.path()));
+ } else {
+ Path filePath = Path.of(entry.path());
+ sourceViewer.loadFile(filePath);
+ if (isCamelSourceFile(filePath)) {
+
sourceViewer.setQuickDocProvider(this::provideCamelQuickDocs);
+ }
+ focusOnViewer = true;
+ }
+ }
+ }
+
+ private boolean isCamelSourceFile(Path path) {
+ String name = path.getFileName().toString().toLowerCase();
+ if (name.endsWith(".camel.yaml") || name.endsWith(".camel.yml")
+ || name.endsWith(".kamelet.yaml") ||
name.endsWith(".kamelet.yml")) {
+ return true;
+ }
+ if (name.endsWith(".yaml") || name.endsWith(".yml")) {
+ return TuiHelper.isCamelYaml(path);
+ }
+ if (name.endsWith(".xml")) {
+ return TuiHelper.isCamelXml(path);
+ }
+ return false;
+ }
+
+ private CamelCatalog getCatalog() {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ if (info == null || info.camelVersion == null) {
+ return null;
+ }
+ String version = info.camelVersion;
+ CamelCatalog cached = catalogCache.get(version);
+ if (cached != null) {
+ return cached;
+ }
+ try {
+ cached = CatalogLoader.loadCatalog(null, version, true);
+ if (cached != null) {
+ catalogCache.put(version, cached);
+ }
+ return cached;
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ private Map<Integer, List<String>> provideCamelQuickDocs(List<JsonObject>
codeData) {
+ CamelCatalog catalog = getCatalog();
+ if (catalog == null || codeData.isEmpty()) {
+ return Map.of();
+ }
+
+ Map<Integer, List<String>> result = new LinkedHashMap<>();
+ for (int i = 0; i < codeData.size(); i++) {
+ String code = codeData.get(i).getString("code");
+ if (code == null) {
+ continue;
+ }
+
+ Matcher uriMatcher = YAML_URI_PATTERN.matcher(code);
+ if (uriMatcher.find()) {
+ String uri = uriMatcher.group(1);
+ if (uri.endsWith("\"")) {
+ uri = uri.substring(0, uri.length() - 1);
+ }
+ RoutesTab.buildEndpointInlineDoc(result, codeData, catalog,
uri, i);
+ continue;
+ }
+
+ Matcher eipMatcher = YAML_EIP_PATTERN.matcher(code);
+ if (eipMatcher.find()) {
+ String eipName = eipMatcher.group(1);
+ RoutesTab.buildEipInlineDoc(result, codeData, catalog,
eipName, null, i);
+ }
+ }
+ return result;
+ }
+
+ private void renderFileList(Frame frame, Rect area) {
+ if (entries.isEmpty()) {
+ String noFilesMsg = rootDir == null ? "No source directory found"
: "No files found";
+ frame.renderWidget(
+ Paragraph.builder()
+ .text(Text.from(Line.from(Span.styled(" " +
noFilesMsg, Style.EMPTY.dim()))))
+ .block(Block.builder()
+
.borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .title(Title.from(Line.from(
+ Span.styled(" Files ",
+ focusOnViewer ?
Style.EMPTY.fg(Theme.accent()) : Theme.title()))))
+ .build())
+ .build(),
+ area);
+ return;
+ }
+
+ String panelTitle = " Files";
+ if (rootDir != null && currentDir != null &&
!currentDir.equals(rootDir)) {
+ panelTitle += ": " + rootDir.relativize(currentDir);
+ }
+ panelTitle += " ";
+
+ ListItem[] items = new ListItem[entries.size()];
+ for (int i = 0; i < entries.size(); i++) {
+ FilesBrowser.FileEntry entry = entries.get(i);
+ String label = " " + entry.emoji() + " " + entry.name();
+ if (entry.directory()) {
+ boolean dimDir = entry.name().startsWith(".") ||
"target".equals(entry.name());
+ Style dirStyle = dimDir ? Style.EMPTY.fg(Theme.accent()).dim()
: Style.EMPTY.fg(Theme.accent());
+ items[i] = ListItem.from(Line.from(Span.styled(label,
dirStyle)));
+ } else {
+ items[i] = ListItem.from(Line.from(Span.raw(label)));
+ }
+ }
+
+ Style titleStyle = focusOnViewer ? Style.EMPTY.fg(Theme.accent()) :
Theme.title();
+ ListWidget list = ListWidget.builder()
+ .items(items)
+ .highlightStyle(focusOnViewer ? Theme.selectionBg().dim() :
Theme.selectionBg())
+ .highlightSymbol("")
+ .scrollMode(ScrollMode.AUTO_SCROLL)
+ .block(Block.builder()
+ .borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .title(Title.from(Line.from(Span.styled(panelTitle,
titleStyle))))
+ .build())
+ .build();
+ frame.renderStatefulWidget(list, area, listState);
+
+ int visibleRows = area.height() - 2;
+ if (entries.size() > visibleRows) {
+ Rect scrollRect = new Rect(
+ area.x() + area.width() - 1,
+ area.y() + 1,
+ 1,
+ visibleRows);
+ listScrollState.contentLength(entries.size());
+ listScrollState.viewportContentLength(visibleRows);
+ listScrollState.position(listState.offset());
+ frame.renderStatefulWidget(Scrollbar.builder().build(),
scrollRect, listScrollState);
+ }
+ }
+
+ private static final DateTimeFormatter DATE_FMT =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+ private void renderInfoPanel(Frame frame, Rect area) {
+ List<Line> lines = new ArrayList<>();
+ Integer sel = listState.selected();
+ if (sel != null && sel < entries.size()) {
+ FilesBrowser.FileEntry entry = entries.get(sel);
+ Path path = Path.of(entry.path());
+ try {
+ BasicFileAttributes attrs = Files.readAttributes(path,
BasicFileAttributes.class);
+ if (!entry.directory()) {
+ lines.add(Line.from(
+ Span.styled(" Size: ", Style.EMPTY.dim()),
+
Span.raw(FilesBrowser.formatFileSize(attrs.size()))));
+ }
+ String modified = attrs.lastModifiedTime().toInstant()
+ .atZone(ZoneId.systemDefault()).format(DATE_FMT);
+ lines.add(Line.from(
+ Span.styled(" Modified: ", Style.EMPTY.dim()),
+ Span.raw(modified)));
+ if (!entry.directory()) {
+ String ext = "";
+ int dot = entry.name().lastIndexOf('.');
+ if (dot > 0) {
+ ext = entry.name().substring(dot + 1);
+ }
+ if (!ext.isEmpty()) {
+ lines.add(Line.from(
+ Span.styled(" Type: ", Style.EMPTY.dim()),
+ Span.raw(ext)));
+ }
+ }
+ } catch (IOException e) {
+ // ignore
+ }
+ }
+
+ frame.renderWidget(
+ Paragraph.builder()
+ .text(Text.from(lines))
+ .block(Block.builder()
+
.borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .title(Title.from(Line.from(
+ Span.styled(" Info ", focusOnViewer ?
Style.EMPTY.fg(Theme.accent()) : Theme.title()))))
+ .build())
+ .build(),
+ area);
+ }
+
+ private void renderSourcePanel(Frame frame, Rect area) {
+ Style sourceTitleStyle = focusOnViewer ? Theme.title() :
Style.EMPTY.fg(Theme.accent());
+ if (sourceViewer.isVisible()) {
+ sourceViewer.setTitleStyle(sourceTitleStyle);
+ sourceViewer.setFocused(focusOnViewer);
+ sourceViewer.render(frame, area);
+ } else {
+ List<Line> lines = new ArrayList<>();
+ lines.add(Line.from(Span.raw("")));
+ lines.add(Line.from(Span.styled(" Select a file and press Enter
to view source", Style.EMPTY.dim())));
+
+ frame.renderWidget(
+ Paragraph.builder()
+ .text(Text.from(lines))
+ .block(Block.builder()
+
.borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .title(Title.from(Line.from(
+ Span.styled(" Source ",
sourceTitleStyle))))
+ .build())
+ .build(),
+ area);
+ }
+ }
+}
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 83bf0e67da04..cc8aadae919e 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
@@ -93,6 +93,8 @@ class SourceViewer {
private QuickDocProvider quickDocProvider;
private boolean quickDocEnabled;
private Map<Integer, List<String>> quickDocEntries =
Collections.emptyMap();
+ private Style titleStyle;
+ private boolean focused = true;
private record CachedSource(
List<String> lines, List<JsonObject> codeData,
@@ -103,6 +105,14 @@ class SourceViewer {
return visible;
}
+ void setTitleStyle(Style style) {
+ this.titleStyle = style;
+ }
+
+ void setFocused(boolean focused) {
+ this.focused = focused;
+ }
+
void hide() {
visible = false;
onLineSelected = null;
@@ -193,7 +203,7 @@ class SourceViewer {
onLineSelected = null;
return true;
}
- if (isMarkdownFile && ke.isKey(KeyCode.TAB)) {
+ if (isMarkdownFile && ke.isChar(' ')) {
markdownMode = !markdownMode;
return true;
}
@@ -213,7 +223,7 @@ class SourceViewer {
}
return true;
}
- if (currentRouteId != null && ke.isKey(KeyCode.TAB)) {
+ if (currentRouteId != null && ke.isChar(' ')) {
String[] formats = { "yaml", "java", "xml" };
int idx = 0;
for (int i = 0; i < formats.length; i++) {
@@ -222,11 +232,7 @@ class SourceViewer {
break;
}
}
- if (ke.hasShift()) {
- idx = (idx - 1 + formats.length) % formats.length;
- } else {
- idx = (idx + 1) % formats.length;
- }
+ idx = (idx + 1) % formats.length;
quickDocEntries = Collections.emptyMap();
switchFormat(formats[idx]);
return true;
@@ -475,7 +481,7 @@ class SourceViewer {
if (markdownMode) {
TuiHelper.hint(spans, "Esc/c", "close");
TuiHelper.hint(spans, TuiIcons.HINT_SCROLL, "scroll");
- TuiHelper.hint(spans, "Tab", "format");
+ TuiHelper.hint(spans, "Space", "format");
TuiHelper.hint(spans, "PgUp/PgDn", "page");
return;
}
@@ -493,7 +499,7 @@ class SourceViewer {
}
TuiHelper.hint(spans, TuiIcons.HINT_SCROLL, "navigate");
if (isMarkdownFile || currentRouteId != null) {
- TuiHelper.hint(spans, "Tab", "format");
+ TuiHelper.hint(spans, "Space", "format");
}
search.renderSearchHints(spans);
TuiHelper.hint(spans, "w", "wrap" + (wordWrap ? " [on]" : " [off]"));
@@ -755,9 +761,10 @@ class SourceViewer {
private Title buildTitle() {
String info = title != null ? title : "";
+ Style ts = titleStyle != null ? titleStyle : Style.EMPTY;
if (isMarkdownFile) {
List<Span> spans = new ArrayList<>();
- spans.add(Span.raw(" Source [" + info + "] "));
+ spans.add(Span.styled(" Source [" + info + "] ", ts));
String mdLabel = "Markdown*";
if (markdownMode) {
spans.add(Span.styled(mdLabel, Style.EMPTY.bold()));
@@ -774,11 +781,11 @@ class SourceViewer {
return Title.from(Line.from(spans));
}
if (currentRouteId == null) {
- return Title.from(Line.from(List.of(Span.raw(" Source [" + info +
"] "))));
+ return Title.from(Line.from(List.of(Span.styled(" Source [" + info
+ "] ", ts))));
}
List<Span> spans = new ArrayList<>();
- spans.add(Span.raw(" Source [" + info + "] "));
+ spans.add(Span.styled(" Source [" + info + "] ", ts));
String[] formats = { "yaml", "java", "xml" };
String[] labels = { "YAML", "Java", "XML" };
@@ -946,11 +953,11 @@ class SourceViewer {
Line highlighted = SyntaxHighlighter.highlightLine(code, language);
List<Span> spans = new ArrayList<>();
- Style selBg = Theme.selectionBg();
+ Style selBg = focused ? Theme.selectionBg() :
Theme.selectionBg().dim();
if (isSelected) {
- spans.add(Span.styled(">> ", Theme.label().bold()));
+ spans.add(Span.styled(">> ", focused ? Theme.label().bold() :
Theme.label().dim()));
if (!prefix.isEmpty()) {
- spans.add(Span.styled(prefix,
Theme.label().bold().patch(selBg)));
+ spans.add(Span.styled(prefix, (focused ? Theme.label().bold()
: Theme.label().dim()).patch(selBg)));
}
for (Span s : highlighted.spans()) {
spans.add(Span.styled(s.content(), s.style().patch(selBg)));
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
index 0fdc0a31873c..5f52269fb948 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
@@ -29,12 +29,12 @@ class TabRegistry {
// Tab indices
static final int TAB_OVERVIEW = 0;
- static final int TAB_LOG = 1;
- static final int TAB_ACTIVITY = 2;
- static final int TAB_DIAGRAM = 3;
- static final int TAB_ROUTES = 4;
- static final int TAB_ENDPOINTS = 5;
- static final int TAB_HTTP = 6;
+ static final int TAB_SOURCE = 1;
+ static final int TAB_LOG = 2;
+ static final int TAB_ACTIVITY = 3;
+ static final int TAB_DIAGRAM = 4;
+ static final int TAB_ROUTES = 5;
+ static final int TAB_ENDPOINTS = 6;
static final int TAB_HISTORY = 7;
static final int TAB_ERRORS = 8;
static final int TAB_MORE = 9;
@@ -75,6 +75,7 @@ class TabRegistry {
private RouteControllerTab routeControllerTab;
private EndpointsTab endpointsTab;
private HttpTab httpTab;
+ private SourceTab sourceTab;
private HealthTab healthTab;
private HistoryTab historyTab;
private CircuitBreakerTab circuitBreakerTab;
@@ -129,6 +130,7 @@ class TabRegistry {
sqlTraceTab = new SqlTraceTab(ctx);
endpointsTab = new EndpointsTab(ctx, dataService.metrics());
httpTab = new HttpTab(ctx);
+ sourceTab = new SourceTab(ctx);
healthTab = new HealthTab(ctx);
historyTab = new HistoryTab(ctx, dataService.traces(),
dataService.traceFilePositions());
circuitBreakerTab = new CircuitBreakerTab(ctx, dataService.metrics());
@@ -165,6 +167,7 @@ class TabRegistry {
new MoreTab(TuiIcons.TAB_BROWSE, "Browse", "Bro&wse",
browseTab, "Routing"),
new MoreTab(TuiIcons.TAB_CIRCUIT_BREAKER, "Circuit Breaker",
"&Circuit Breaker", circuitBreakerTab, "Routing"),
new MoreTab(TuiIcons.TAB_CONSUMERS, "Consumers", "Co&nsumers",
consumersTab, "Routing"),
+ new MoreTab(TuiIcons.TAB_HTTP, "HTTP", "&HTTP", httpTab,
"Routing"),
new MoreTab(TuiIcons.TAB_INFLIGHT, "Inflight", "In&flight",
inflightTab, "Routing"),
new MoreTab(TuiIcons.TAB_PRODUCERS, "Producers", "Prod&ucers",
producersTab, "Routing"),
new MoreTab(
@@ -208,7 +211,7 @@ class TabRegistry {
case TAB_DIAGRAM -> diagramTab;
case TAB_ROUTES -> routesTab;
case TAB_ENDPOINTS -> endpointsTab;
- case TAB_HTTP -> httpTab;
+ case TAB_SOURCE -> sourceTab;
case TAB_HISTORY -> historyTab;
case TAB_ERRORS -> errorsTab;
case TAB_MORE -> activeMoreTab;
@@ -301,7 +304,7 @@ class TabRegistry {
void resetIntegrationTabState(DataRefreshService dataService, FilesBrowser
filesBrowser) {
diagramTab.onIntegrationChanged();
routesTab.onIntegrationChanged();
- httpTab.onIntegrationChanged();
+ sourceTab.onIntegrationChanged();
logTab.onIntegrationChanged();
historyTab.onIntegrationChanged();
for (MoreTab mt : moreTabs) {
@@ -338,6 +341,10 @@ class TabRegistry {
return httpTab;
}
+ SourceTab sourceTab() {
+ return sourceTab;
+ }
+
HealthTab healthTab() {
return healthTab;
}
@@ -447,14 +454,14 @@ class TabRegistry {
List<TabEntry> entries = new ArrayList<>();
entries.add(
new TabEntry(icon(TAB_OVERVIEW), "Overview",
overviewTab.description(), "1", TAB_OVERVIEW, -1, overviewTab));
- entries.add(new TabEntry(icon(TAB_LOG), "Log", logTab.description(),
"2", TAB_LOG, -1, logTab));
+ entries.add(new TabEntry(icon(TAB_SOURCE), "Source",
sourceTab.description(), "2", TAB_SOURCE, -1, sourceTab));
+ entries.add(new TabEntry(icon(TAB_LOG), "Log", logTab.description(),
"3", TAB_LOG, -1, logTab));
entries.add(
- new TabEntry(icon(TAB_ACTIVITY), "Activity",
activityTab.description(), "3", TAB_ACTIVITY, -1, activityTab));
- entries.add(new TabEntry(icon(TAB_DIAGRAM), "Diagram",
diagramTab.description(), "4", TAB_DIAGRAM, -1, diagramTab));
- entries.add(new TabEntry(icon(TAB_ROUTES), "Routes",
routesTab.description(), "5", TAB_ROUTES, -1, routesTab));
+ new TabEntry(icon(TAB_ACTIVITY), "Activity",
activityTab.description(), "4", TAB_ACTIVITY, -1, activityTab));
+ entries.add(new TabEntry(icon(TAB_DIAGRAM), "Diagram",
diagramTab.description(), "5", TAB_DIAGRAM, -1, diagramTab));
+ entries.add(new TabEntry(icon(TAB_ROUTES), "Routes",
routesTab.description(), "6", TAB_ROUTES, -1, routesTab));
entries.add(new TabEntry(
- icon(TAB_ENDPOINTS), "Endpoints", endpointsTab.description(),
"6", TAB_ENDPOINTS, -1, endpointsTab));
- entries.add(new TabEntry(icon(TAB_HTTP), "HTTP",
httpTab.description(), "7", TAB_HTTP, -1, httpTab));
+ icon(TAB_ENDPOINTS), "Endpoints", endpointsTab.description(),
"7", TAB_ENDPOINTS, -1, endpointsTab));
entries.add(new TabEntry(icon(TAB_HISTORY), "Inspect",
historyTab.description(), "8", TAB_HISTORY, -1, historyTab));
entries.add(new TabEntry(icon(TAB_ERRORS), "Errors",
errorsTab.description(), "9", TAB_ERRORS, -1, errorsTab));
for (int i = 0; i < moreTabs.size(); i++) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
index 16c5a4c624c4..d062c3f34f40 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
@@ -818,7 +818,7 @@ final class TuiHelper {
return TuiIcons.CLIPBOARD;
}
- private static boolean isCamelYaml(Path path) {
+ static boolean isCamelYaml(Path path) {
try {
String content = Files.readString(path, StandardCharsets.UTF_8);
for (String marker : CAMEL_YAML_MARKERS) {
@@ -832,7 +832,7 @@ final class TuiHelper {
return false;
}
- private static boolean isCamelXml(Path path) {
+ static boolean isCamelXml(Path path) {
try {
String content = Files.readString(path, StandardCharsets.UTF_8);
for (String marker : CAMEL_XML_MARKERS) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
index ad41009894f1..3e96996b5b6c 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
@@ -117,6 +117,7 @@ final class TuiIcons {
static final String TAB_ROUTES = "🔀";
static final String TAB_ENDPOINTS = ENDPOINT;
static final String TAB_HTTP = ONLINE;
+ static final String TAB_SOURCE = COMPUTER;
static final String TAB_HEALTH = DOCTOR;
static final String TAB_INSPECT = GO_TO;
static final String TAB_ERRORS = FAIL;
@@ -152,8 +153,8 @@ final class TuiIcons {
/** Icons for {@link TabRegistry#TAB_OVERVIEW}..{@link
TabRegistry#TAB_MORE} (in order). */
static final List<String> PRIMARY_TAB_ICONS = List.of(
- TAB_OVERVIEW, TAB_LOG, TAB_ACTIVITY, TAB_DIAGRAM, TAB_ROUTES,
- TAB_ENDPOINTS, TAB_HTTP, TAB_INSPECT, TAB_ERRORS, TAB_MORE);
+ TAB_OVERVIEW, TAB_SOURCE, TAB_LOG, TAB_ACTIVITY, TAB_DIAGRAM,
+ TAB_ROUTES, TAB_ENDPOINTS, TAB_INSPECT, TAB_ERRORS, TAB_MORE);
/** Marker placed immediately before a label's keyboard-shortcut letter
(Windows-style mnemonic). */
static final char MNEMONIC_MARKER = '&';
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabBarRenderTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabBarRenderTest.java
index 9f149007bae8..07b0e153bbd2 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabBarRenderTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabBarRenderTest.java
@@ -33,7 +33,7 @@ class TabBarRenderTest {
@Test
void primaryTabHeadersStartWithIconAndTwoSpaces() {
- String[] names = { "Overview", "Log", "Diagram", "Route", "Endpoint",
"HTTP", "Health", "Inspect", "Errors" };
+ String[] names = { "Overview", "Source", "Log", "Activity", "Diagram",
"Route", "Endpoint", "Inspect", "Errors" };
for (int i = 0; i < names.length; i++) {
String header
=
Line.from(TuiIcons.primaryTabHeader(TuiIcons.PRIMARY_TAB_ICONS.get(i),
String.valueOf(i + 1), names[i]))
@@ -53,8 +53,8 @@ class TabBarRenderTest {
@Test
void routeAndTopLabelsHaveEqualWidth() {
// toggling Top mode must not shift the tab bar
- int route = Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_ROUTES,
"4", "Route")).width();
- int top = Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_ROUTES,
"4", " Top ")).width();
+ int route = Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_ROUTES,
"6", "Route")).width();
+ int top = Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_ROUTES,
"6", " Top ")).width();
assertEquals(route, top, "Route and Top tab cells must be the same
width");
}
@@ -79,12 +79,12 @@ class TabBarRenderTest {
private static Line[] fullTabLabels(String routesLabel) {
return new Line[] {
Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_OVERVIEW,
"1", "Overview")),
- Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_LOG, "2",
"Log")),
- Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_DIAGRAM, "3",
"Diagram")),
- Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_ROUTES, "4",
routesLabel)),
- Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_ENDPOINTS,
"5", "Endpoint")),
- Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_HTTP, "6",
"HTTP")),
- Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_HEALTH, "7",
"Health")),
+ Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_SOURCE, "2",
"Source")),
+ Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_LOG, "3",
"Log")),
+ Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_ACTIVITY,
"4", "Activity")),
+ Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_DIAGRAM, "5",
"Diagram")),
+ Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_ROUTES, "6",
routesLabel)),
+ Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_ENDPOINTS,
"7", "Endpoint")),
Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_INSPECT, "8",
"Inspect")),
Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_ERRORS, "9",
"Errors")),
Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_MORE, "0",
TuiIcons.moreTabLabel())),
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistryTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistryTest.java
index 86d153cac9c6..6b384a904836 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistryTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistryTest.java
@@ -18,7 +18,6 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
import java.nio.file.Path;
import java.util.List;
-import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import dev.tamboui.text.CharWidth;
@@ -82,8 +81,8 @@ class TabRegistryTest {
}
@Test
- void moreTabsHasTwentySixEntries() {
- assertEquals(26, registry.moreTabs().size());
+ void moreTabsHasTwentySevenEntries() {
+ assertEquals(27, registry.moreTabs().size());
}
@Test
@@ -109,17 +108,22 @@ class TabRegistryTest {
// MORE_SHORTCUTS array carried before the MoreTab refactor. A label
edit that repoints a key must fail here.
List<Character> shortcuts =
registry.moreTabs().stream().map(TabRegistry.MoreTab::shortcut).toList();
assertEquals(
- List.of('W', 'C', 'N', 'F', 'U', 'Z', 'E', 'I', 'X', 'O', 'J',
'K', 'Q', 'R', 'A', 'H', 'M', 'Y', 'P', 'S',
- 'T', 'B', 'L', 'G', 'V', 'D'),
+ List.of('W', 'C', 'N', 'H', 'F', 'U', 'Z', 'E', 'I', 'X', 'O',
'J', 'K', 'Q', 'R', 'A', 'H', 'M', 'Y',
+ 'P', 'S', 'T', 'B', 'L', 'G', 'V', 'D'),
shortcuts, "More tab shortcut letters must match the
historical sequence");
}
@Test
- void moreTabShortcutsAreUnique() {
- // morePopupShortcut() returns the first matching tab, so a duplicated
letter would silently shadow a later tab.
+ void moreTabShortcutsHaveAtMostTwoDuplicates() {
+ // Duplicate mnemonics are allowed; the popup cycles through them on
repeated key press.
List<Character> shortcuts =
registry.moreTabs().stream().map(TabRegistry.MoreTab::shortcut).toList();
- assertEquals(shortcuts.size(), Set.copyOf(shortcuts).size(),
- "More tab shortcut letters must be unique: " + shortcuts);
+ java.util.Map<Character, Long> counts = shortcuts.stream()
+ .collect(java.util.stream.Collectors.groupingBy(c -> c,
java.util.stream.Collectors.counting()));
+ for (var entry : counts.entrySet()) {
+ assertTrue(entry.getValue() <= 2,
+ "More tab shortcut '" + entry.getKey() + "' appears " +
entry.getValue()
+ + " times (max 2): " +
shortcuts);
+ }
}
@Test
@@ -196,12 +200,12 @@ class TabRegistryTest {
@Test
void tabConstantsAreSequential() {
assertEquals(0, TabRegistry.TAB_OVERVIEW, "TAB_OVERVIEW should be 0");
- assertEquals(1, TabRegistry.TAB_LOG, "TAB_LOG should be 1");
- assertEquals(2, TabRegistry.TAB_ACTIVITY, "TAB_ACTIVITY should be 2");
- assertEquals(3, TabRegistry.TAB_DIAGRAM, "TAB_DIAGRAM should be 3");
- assertEquals(4, TabRegistry.TAB_ROUTES, "TAB_ROUTES should be 4");
- assertEquals(5, TabRegistry.TAB_ENDPOINTS, "TAB_ENDPOINTS should be
5");
- assertEquals(6, TabRegistry.TAB_HTTP, "TAB_HTTP should be 6");
+ assertEquals(1, TabRegistry.TAB_SOURCE, "TAB_SOURCE should be 1");
+ assertEquals(2, TabRegistry.TAB_LOG, "TAB_LOG should be 2");
+ assertEquals(3, TabRegistry.TAB_ACTIVITY, "TAB_ACTIVITY should be 3");
+ assertEquals(4, TabRegistry.TAB_DIAGRAM, "TAB_DIAGRAM should be 4");
+ assertEquals(5, TabRegistry.TAB_ROUTES, "TAB_ROUTES should be 5");
+ assertEquals(6, TabRegistry.TAB_ENDPOINTS, "TAB_ENDPOINTS should be
6");
assertEquals(7, TabRegistry.TAB_HISTORY, "TAB_HISTORY should be 7");
assertEquals(8, TabRegistry.TAB_ERRORS, "TAB_ERRORS should be 8");
assertEquals(9, TabRegistry.TAB_MORE, "TAB_MORE should be 9");
@@ -223,7 +227,7 @@ class TabRegistryTest {
int[] tabs = {
TabRegistry.TAB_OVERVIEW, TabRegistry.TAB_LOG,
TabRegistry.TAB_ACTIVITY,
TabRegistry.TAB_DIAGRAM, TabRegistry.TAB_ROUTES,
TabRegistry.TAB_ENDPOINTS,
- TabRegistry.TAB_HTTP, TabRegistry.TAB_HISTORY,
TabRegistry.TAB_ERRORS,
+ TabRegistry.TAB_SOURCE, TabRegistry.TAB_HISTORY,
TabRegistry.TAB_ERRORS,
TabRegistry.TAB_MORE
};
for (int i = 0; i < tabs.length; i++) {
@@ -242,7 +246,7 @@ class TabRegistryTest {
assertTrue(TabRegistry.TAB_DIAGRAM >= 0, "TAB_DIAGRAM should be
non-negative");
assertTrue(TabRegistry.TAB_ROUTES >= 0, "TAB_ROUTES should be
non-negative");
assertTrue(TabRegistry.TAB_ENDPOINTS >= 0, "TAB_ENDPOINTS should be
non-negative");
- assertTrue(TabRegistry.TAB_HTTP >= 0, "TAB_HTTP should be
non-negative");
+ assertTrue(TabRegistry.TAB_SOURCE >= 0, "TAB_SOURCE should be
non-negative");
assertTrue(TabRegistry.TAB_HISTORY >= 0, "TAB_HISTORY should be
non-negative");
assertTrue(TabRegistry.TAB_ERRORS >= 0, "TAB_ERRORS should be
non-negative");
assertTrue(TabRegistry.TAB_MORE >= 0, "TAB_MORE should be
non-negative");
@@ -256,7 +260,7 @@ class TabRegistryTest {
assertTrue(TabRegistry.TAB_DIAGRAM < TabRegistry.NUM_TABS);
assertTrue(TabRegistry.TAB_ROUTES < TabRegistry.NUM_TABS);
assertTrue(TabRegistry.TAB_ENDPOINTS < TabRegistry.NUM_TABS);
- assertTrue(TabRegistry.TAB_HTTP < TabRegistry.NUM_TABS);
+ assertTrue(TabRegistry.TAB_SOURCE < TabRegistry.NUM_TABS);
assertTrue(TabRegistry.TAB_HISTORY < TabRegistry.NUM_TABS);
assertTrue(TabRegistry.TAB_ERRORS < TabRegistry.NUM_TABS);
assertTrue(TabRegistry.TAB_MORE < TabRegistry.NUM_TABS);
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIconsTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIconsTest.java
index 87564bfae514..6ed452a76918 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIconsTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIconsTest.java
@@ -40,12 +40,12 @@ class TuiIconsTest {
// Guards that PRIMARY_TAB_ICONS is indexed by the TAB_* constants:
reordering the list must break this,
// otherwise TabRegistry.icon(index) would attach the wrong emoji to
Go-to entries.
assertEquals(TuiIcons.TAB_OVERVIEW,
TuiIcons.PRIMARY_TAB_ICONS.get(TabRegistry.TAB_OVERVIEW));
+ assertEquals(TuiIcons.TAB_SOURCE,
TuiIcons.PRIMARY_TAB_ICONS.get(TabRegistry.TAB_SOURCE));
assertEquals(TuiIcons.TAB_LOG,
TuiIcons.PRIMARY_TAB_ICONS.get(TabRegistry.TAB_LOG));
assertEquals(TuiIcons.TAB_ACTIVITY,
TuiIcons.PRIMARY_TAB_ICONS.get(TabRegistry.TAB_ACTIVITY));
assertEquals(TuiIcons.TAB_DIAGRAM,
TuiIcons.PRIMARY_TAB_ICONS.get(TabRegistry.TAB_DIAGRAM));
assertEquals(TuiIcons.TAB_ROUTES,
TuiIcons.PRIMARY_TAB_ICONS.get(TabRegistry.TAB_ROUTES));
assertEquals(TuiIcons.TAB_ENDPOINTS,
TuiIcons.PRIMARY_TAB_ICONS.get(TabRegistry.TAB_ENDPOINTS));
- assertEquals(TuiIcons.TAB_HTTP,
TuiIcons.PRIMARY_TAB_ICONS.get(TabRegistry.TAB_HTTP));
assertEquals(TuiIcons.TAB_INSPECT,
TuiIcons.PRIMARY_TAB_ICONS.get(TabRegistry.TAB_HISTORY));
assertEquals(TuiIcons.TAB_ERRORS,
TuiIcons.PRIMARY_TAB_ICONS.get(TabRegistry.TAB_ERRORS));
assertEquals(TuiIcons.TAB_MORE,
TuiIcons.PRIMARY_TAB_ICONS.get(TabRegistry.TAB_MORE));