diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractTab.java index 81167cc8586c7..56b4c7f39aef6 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractTab.java @@ -108,7 +108,7 @@ protected static Row emptyRow(String message, int columnCount) { // ---- Sort helpers ---- protected static String sortLabel(String label, String column, String currentSort, boolean reversed) { - return currentSort.equals(column) ? label + (reversed ? "▲" : "▼") : label; + return currentSort.equals(column) ? label + (reversed ? TuiIcons.SORT_UP : TuiIcons.SORT_DOWN) : label; } protected static Style sortStyle(String column, String currentSort) { diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java index 438d899486a83..2e87ca17eccff 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java @@ -757,13 +757,13 @@ void renderFooter(List spans) { } if (gotoTabPopup.isVisible()) { hint(spans, "type", "filter"); - hint(spans, "↑↓", "navigate"); + hint(spans, TuiIcons.HINT_SCROLL, "navigate"); hint(spans, "Enter", "go to"); hintLast(spans, "Esc", "back"); return; } if (showActionsMenu) { - hint(spans, "↑↓", "navigate"); + hint(spans, TuiIcons.HINT_SCROLL, "navigate"); hint(spans, "Enter", "select"); hintLast(spans, "Esc", "cancel"); } @@ -789,52 +789,54 @@ private void renderActionsMenu(Frame frame, Rect area) { // CAMEL-23818: use plain 2-wide emoji here. TamboUI mismeasures base-glyph + VS16 // sequences as 1-wide (fixed upstream in tamboui/tamboui#388), which left stray chars. String keystrokeLabel = keystrokesEnabled.get() - ? " 🔤 Hide Keystrokes" - : " 🔤 Show Keystrokes"; + ? TuiIcons.menuItem(TuiIcons.KEYSTROKES, "Hide Keystrokes") + : TuiIcons.menuItem(TuiIcons.KEYSTROKES, "Show Keystrokes"); String stopLabel = stopAllPopup.hasBothGroups() - ? " 🛑 Stop All..." - : " 🛑 Stop All"; + ? TuiIcons.menuItem(TuiIcons.STOP, "Stop All...") + : TuiIcons.menuItem(TuiIcons.STOP, "Stop All"); String tapeLabel = tapeRecordingActive.get() - ? " 🛑 Stop Tape Recording (Ctrl+R)" - : " 🔴 Start Tape Recording (Ctrl+R)"; + ? TuiIcons.menuItem(TuiIcons.STOP, "Stop Tape Recording (Ctrl+R)") + : TuiIcons.menuItem(TuiIcons.RECORD, "Start Tape Recording (Ctrl+R)"); List items = new ArrayList<>(); // Group 0: Go to - items.add(ListItem.from(" 🔍 Go to...")); + items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.GO_TO, "Go to..."))); items.add(ListItem.from(divider).style(Style.EMPTY.dim())); // Group 1: User Actions boolean hasSelection = ctx != null && ctx.selectedPid != null && !ctx.isInfraSelected(); items.add(hasSelection - ? ListItem.from(" 📩 Send Message") - : ListItem.from(" 📩 Send Message").style(Style.EMPTY.dim())); - items.add(ListItem.from(" 🐪 Run an Example...")); - items.add(ListItem.from(" 📂 Run from Folder...")); - items.add(ListItem.from(" 🔧 Run Dev/Infra Service...")); + ? ListItem.from(TuiIcons.menuItem(TuiIcons.MESSAGE, "Send Message")) + : ListItem.from(TuiIcons.menuItem(TuiIcons.MESSAGE, "Send Message")).style(Style.EMPTY.dim())); + items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.CAMEL, "Run an Example..."))); + items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.FOLDER_OPEN, "Run from Folder..."))); + items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.INFRA, "Run Dev/Infra Service..."))); items.add(hasSelection - ? ListItem.from(" 📁 Browse Files...") - : ListItem.from(" 📁 Browse Files...").style(Style.EMPTY.dim())); + ? ListItem.from(TuiIcons.menuItem(TuiIcons.FOLDER, "Browse Files...")) + : ListItem.from(TuiIcons.menuItem(TuiIcons.FOLDER, "Browse Files...")).style(Style.EMPTY.dim())); items.add(ListItem.from(stopLabel)); items.add(ListItem.from(divider).style(Style.EMPTY.dim())); // Group 2: Diagnostics - items.add(ListItem.from(" 🩺 Run Doctor")); - items.add(ListItem.from(" 🔄 Reset Stats")); - items.add(ListItem.from(" 🧹 Reset Screen")); - String themeLabel = "dark".equals(Theme.mode()) ? " 🌞 Light Theme" : " 🌙 Dark Theme"; + items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.DOCTOR, "Run Doctor"))); + items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.RESET, "Reset Stats"))); + items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.CLEAN, "Reset Screen"))); + String themeLabel = "dark".equals(Theme.mode()) + ? TuiIcons.menuItem(TuiIcons.LIGHT_THEME, "Light Theme") + : TuiIcons.menuItem(TuiIcons.DARK_THEME, "Dark Theme"); items.add(ListItem.from(themeLabel)); items.add(ListItem.from(divider).style(Style.EMPTY.dim())); // Group 3: Recording & Presentation - items.add(ListItem.from(" 📸 Take Screenshot")); + items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.SCREENSHOT, "Take Screenshot"))); items.add(ListItem.from(tapeLabel)); - items.add(ListItem.from(" 📄 Tape Recording Guide")); - items.add(ListItem.from(" 💬 Caption...")); + items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.DOCUMENT, "Tape Recording Guide"))); + items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.CAPTION, "Caption..."))); items.add(ListItem.from(keystrokeLabel)); // Group 4: MCP if (mcpEnabled) { items.add(ListItem.from(divider).style(Style.EMPTY.dim())); - items.add(ListItem.from(" 🧠 Setup MCP")); - items.add(ListItem.from(" 💬 AI Log")); - items.add(ListItem.from(" 🤖 MCP Info")); - items.add(ListItem.from(" 📋 MCP Log")); + items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.MCP_BRAIN, "Setup MCP"))); + items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.CAPTION, "AI Log"))); + items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.MCP, "MCP Info"))); + items.add(ListItem.from(TuiIcons.menuItem(TuiIcons.MCP_LOG, "MCP Log"))); } // Group 5: Shell items.add(ListItem.from(divider).style(Style.EMPTY.dim())); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiLogPopup.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiLogPopup.java index 97e8d468d2e15..b8272d17d9e8d 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiLogPopup.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiLogPopup.java @@ -123,7 +123,7 @@ void render(Frame frame, Rect area) { } void renderFooter(List spans) { - hint(spans, "↑↓", "select"); + hint(spans, TuiIcons.HINT_SCROLL, "select"); hint(spans, "PgUp/Dn", "scroll detail"); hintLast(spans, "Esc", "back"); } @@ -174,11 +174,13 @@ private void renderDetail(Frame frame, Rect area) { if (detail != null && !detail.isBlank()) { if (entry.level() == AiPanel.LogLevel.TOOL || entry.level() == AiPanel.LogLevel.RESULT) { lines.add(Line.from(Span.styled( - entry.level() == AiPanel.LogLevel.TOOL ? "▶ Arguments" : "◀ Result", + entry.level() == AiPanel.LogLevel.TOOL + ? TuiIcons.ARROW_RIGHT + " Arguments" + : TuiIcons.ARROW_LEFT + " Result", Style.EMPTY.fg(entry.level() == AiPanel.LogLevel.TOOL ? Color.YELLOW : Color.GREEN).bold()))); addJsonLines(lines, detail); } else { - lines.add(Line.from(Span.styled("▶ Content", + lines.add(Line.from(Span.styled(TuiIcons.ARROW_RIGHT + " Content", Style.EMPTY.fg(Color.CYAN).bold()))); for (String line : detail.split("\n", -1)) { lines.add(Line.from(Span.styled(" " + line, Style.EMPTY.dim()))); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java index c40c8fad3eabd..d8b4e043d5fc4 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java @@ -526,7 +526,7 @@ private void renderConversation(Frame frame, Rect area) { if (thinking.get()) { long elapsed = (System.currentTimeMillis() - thinkingStartTime) / 1000; long dots = (System.currentTimeMillis() / 500) % 4; - md.append("*🤔 thinking"); + md.append("*" + TuiIcons.THINKING + " thinking"); if (elapsed > 0) { md.append(" (").append(elapsed).append("s)"); } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BeansTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BeansTab.java index 212153e73d717..669179da31b5d 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BeansTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BeansTab.java @@ -321,7 +321,7 @@ public void renderFooter(List spans) { } else { hint(spans, "/", "filter"); } - hint(spans, "↑↓", "navigate"); + hint(spans, TuiIcons.HINT_SCROLL, "navigate"); hintLast(spans, "PgUp/Dn", "scroll"); } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BrowseTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BrowseTab.java index de5bd8507ed30..929c2925554ef 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BrowseTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BrowseTab.java @@ -470,7 +470,7 @@ public void renderFooter(List spans) { hint(spans, "Esc", "back"); if (view == VIEW_DETAIL) { hint(spans, "p", "pretty" + (prettyPrint ? " [on]" : "")); - hintLast(spans, "↑↓", "scroll"); + hintLast(spans, TuiIcons.HINT_SCROLL, "scroll"); } else if (view == VIEW_MESSAGES) { hint(spans, "r", "refresh"); hintLast(spans, "Enter", "detail"); 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 e109d54a0683e..65a5db83a6f4d 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 @@ -82,8 +82,8 @@ public class CamelMonitor extends CamelCommand { // Compact tab bar (10 labels + 9 "|" dividers) needs 88 chars — that is the true minimum private static final int MIN_WIDTH = 88; private static final int MIN_HEIGHT = 24; - // Full tab bar (10 labels + 9 " | " dividers) needs 126 chars; use compact below that - private static final int TABS_FULL_MIN_WIDTH = 126; + // Below this width the tab bar uses tight "|" dividers instead of spaced " | " so all 10 labels still fit + private static final int TABS_FULL_MIN_WIDTH = 157; @CommandLine.Parameters(description = "Name or pid of running Camel integration", arity = "0..1") String name = "*"; @@ -304,7 +304,7 @@ public void selectMorePopupEntry(int index) { }); popupManager = new PopupManager( - ctx, this::getNonVanishingIntegrations, filesBrowser, + ctx, this::getNonVanishingIntegrations, tabRegistry::moreTabs, filesBrowser, new PopupManager.PopupCallbacks() { @Override public void selectMoreTab(int index) { @@ -1244,15 +1244,10 @@ private void renderTabs(Frame frame, Rect area) { if (infraSelected) { // Infra mode: only Overview and Log tabs - Line[] labels = compact - ? new Line[] { - Line.from("1 Overview"), - Line.from("2 Log"), - } - : new Line[] { - Line.from(" 1 Overview "), - Line.from(" 2 Log "), - }; + Line[] labels = { + Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_OVERVIEW, "1", "Overview")), + Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_LOG, "2", "Log")), + }; // Map real tab index to infra tab index for highlight int infraTabIdx = tabsState.selected() == TAB_LOG ? 1 : 0; @@ -1273,31 +1268,20 @@ private void renderTabs(Frame frame, Rect area) { return; } - Line[] labels = compact - ? new Line[] { - Line.from("1 Overview"), - Line.from("2 Log"), - Line.from("3 Diagram"), - Line.from(tabRegistry.routesTab().isTopMode() ? "4 Top " : "4 Route"), - Line.from("5 Endpoint"), - Line.from("6 HTTP"), - Line.from("7 Health"), - Line.from("8 Inspect"), - Line.from("9 Errors"), - Line.from("0 More▾"), - } - : new Line[] { - Line.from(" 1 Overview "), - Line.from(" 2 Log "), - Line.from(" 3 Diagram "), - Line.from(tabRegistry.routesTab().isTopMode() ? " 4 Top " : " 4 Route "), - Line.from(" 5 Endpoint "), - Line.from(" 6 HTTP "), - Line.from(" 7 Health "), - Line.from(" 8 Inspect "), - Line.from(" 9 Errors "), - Line.from(" 0 More▾ "), - }; + // Route and Top labels are the same display width so toggling Top mode does not shift the bar. + String routesLabel = tabRegistry.routesTab().isTopMode() ? " Top " : "Route"; + 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_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_INSPECT, "8", "Inspect")), + Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_ERRORS, "9", "Errors")), + Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_MORE, "0", TuiIcons.moreTabLabel())), + }; popupManager.setCurrentTabLabels(labels); lastTabLabels = labels; lastTabDivider = dividerStr; @@ -1735,12 +1719,12 @@ private void renderFooter(Frame frame, Rect area) { Style labelStyle; Style suffixStyle; if (client != null) { - suffix = active ? " ●" : " ○"; + suffix = active ? " " + TuiIcons.SELECTED : " " + TuiIcons.IDLE; mcpLabel += " (" + client + ")"; labelStyle = Theme.success(); suffixStyle = active ? Theme.mcpActive() : Theme.mcpIdle(); } else { - suffix = " ✗"; + suffix = " " + TuiIcons.CROSS; labelStyle = Theme.muted(); suffixStyle = Theme.mcpDown(); } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CircuitBreakerTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CircuitBreakerTab.java index 3973feeb12876..84b847027cb3a 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CircuitBreakerTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CircuitBreakerTab.java @@ -165,7 +165,7 @@ protected void renderContent(Frame frame, Rect area, IntegrationInfo info) { @Override public void renderFooter(List spans) { hint(spans, "Esc", "back"); - hint(spans, "↑↓", "navigate"); + hint(spans, TuiIcons.HINT_SCROLL, "navigate"); hint(spans, "s", "sort"); } @@ -208,7 +208,7 @@ private void renderStateDiagram(Frame frame, Rect area, CircuitBreakerInfo cb) { lines.add(Line.from( Span.raw(" "), Span.styled("│ CLOSED │", closedBox), - Span.raw("─────────────►"), + Span.raw("─────────────" + TuiIcons.POINTER), Span.styled("│ OPEN │", openBox), Span.raw("◄─┐"))); lines.add(Line.from( @@ -220,7 +220,7 @@ private void renderStateDiagram(Frame frame, Rect area, CircuitBreakerInfo cb) { lines.add(Line.from( Span.raw(" "), Span.styled("└──────", closedBox), - Span.raw("▲"), + Span.raw(TuiIcons.SORT_UP), Span.styled("───────┘", closedBox), Span.raw(" "), Span.styled("└───────", openBox), @@ -240,7 +240,7 @@ private void renderStateDiagram(Frame frame, Rect area, CircuitBreakerInfo cb) { lines.add(Line.from( Span.raw(" │ "), Span.styled("┌───────", halfOpenBox), - Span.raw("▼"), + Span.raw(TuiIcons.SORT_DOWN), Span.styled("──────┐", halfOpenBox), Span.raw(" │"))); lines.add(Line.from( diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ClasspathTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ClasspathTab.java index abc3d512f2512..90ad5700604d4 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ClasspathTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ClasspathTab.java @@ -263,7 +263,7 @@ public void renderFooter(List spans) { } else { hint(spans, "/", "filter"); } - hintLast(spans, "↑↓", "navigate"); + hintLast(spans, TuiIcons.HINT_SCROLL, "navigate"); } private void loadClasspath() { 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 b0339299c6199..e20495912148a 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 @@ -361,7 +361,7 @@ private Map getComponentOptions(String componentName) { @Override public void renderFooter(List spans) { super.renderFooter(spans); - hint(spans, "↑↓", "navigate"); + hint(spans, TuiIcons.HINT_SCROLL, "navigate"); hintLast(spans, "PgUp/Dn", "scroll"); } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTab.java index bb94296e810f3..a1185388bdab2 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTab.java @@ -91,7 +91,7 @@ protected void renderContent(Frame frame, Rect area, IntegrationInfo info) { : ("Started".equals(ci.state) || "Polling".equals(status) ? Style.EMPTY.fg(Color.GREEN) : Style.EMPTY.fg(Color.LIGHT_RED)); - String statusText = healthDown ? "⚠ " + status : status; + String statusText = healthDown ? TuiIcons.HEALTH_WARN + " " + status : status; String type = consumerType(ci); String schedule = consumerSchedule(ci); String sinceLast = consumerSinceLast(ci); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramSupport.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramSupport.java index 1a710f3c63922..fa384ed5fd649 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramSupport.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramSupport.java @@ -562,7 +562,7 @@ void preload(MonitorContext ctx, String pid) { void renderFooterHints(List spans) { hint(spans, "d/Esc", "close"); - hint(spans, "↑↓←→", "scroll"); + hint(spans, TuiIcons.HINT_NAV, "scroll"); hint(spans, "PgUp/PgDn", "page"); hint(spans, "Home/End", "top/end"); } 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 6a669d6b9f7bd..d1487bfb2e799 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 @@ -663,17 +663,17 @@ public void renderFooter(List spans) { if (!topologyMode && !diagram.getEipNodeBoxes().isEmpty()) { hint(spans, "Esc", "back"); hint(spans, "t", "topology"); - hint(spans, "↑↓←→", "navigate"); + hint(spans, TuiIcons.HINT_NAV, "navigate"); hint(spans, "PgUp/PgDn", "page"); hint(spans, "c", "source"); } else if (!topologyMode) { hint(spans, "Esc", "back"); hint(spans, "t", "topology"); - hint(spans, "↑↓←→", "scroll"); + hint(spans, TuiIcons.HINT_NAV, "scroll"); hint(spans, "PgUp/PgDn", "page"); } else if (!diagram.getNodeBoxes().isEmpty()) { hint(spans, "Esc", "close"); - hint(spans, "↑↓←→", "navigate"); + hint(spans, TuiIcons.HINT_NAV, "navigate"); hint(spans, "Enter", "drill-down"); hint(spans, "PgUp/PgDn", "page"); hint(spans, "c", "source"); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DoctorPopup.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DoctorPopup.java index 6a668ec00de60..ac1e25d774c2a 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DoctorPopup.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DoctorPopup.java @@ -105,7 +105,7 @@ void render(Frame frame, Rect area) { .text(Text.from(lines.toArray(Line[]::new))) .block(Block.builder() .borderType(BorderType.ROUNDED).borders(Borders.ALL) - .title(" 🩺 Doctor ") + .title(" " + TuiIcons.DOCTOR + " Doctor ") .build()) .build(); frame.renderWidget(para, popup); @@ -125,16 +125,16 @@ private void checkJava(List result) { String emoji; if (major >= 21) { status = null; - emoji = "✅"; + emoji = TuiIcons.OK; } else if (major >= 17) { status = "Consider upgrading to 21 or 25"; - emoji = "⚠️"; + emoji = TuiIcons.WARN; } else { status = "17+ required"; - emoji = "❌"; + emoji = TuiIcons.FAIL; } result.add(Line.from( - Span.raw(" ☕ "), + Span.raw(TuiIcons.indent(TuiIcons.JAVA)), Span.styled(String.format("%-14s", "Java"), Style.EMPTY.bold()), Span.raw(String.format("%-30s", version + " (" + vendor + ")")), Span.raw(" " + emoji))); @@ -148,16 +148,16 @@ private void checkCamelVersion(List result) { CamelCatalog catalog = new DefaultCamelCatalog(); String version = catalog.getCatalogVersion(); result.add(Line.from( - Span.raw(" 🐪 "), + Span.raw(TuiIcons.indent(TuiIcons.CAMEL)), Span.styled(String.format("%-14s", "Camel"), Style.EMPTY.bold()), Span.raw(String.format("%-30s", version)), - Span.raw(" ✅"))); + Span.raw(" " + TuiIcons.OK))); } catch (Exception e) { result.add(Line.from( - Span.raw(" 🐪 "), + Span.raw(TuiIcons.indent(TuiIcons.CAMEL)), Span.styled(String.format("%-14s", "Camel"), Style.EMPTY.bold()), Span.raw(String.format("%-30s", "Not detected")), - Span.raw(" ❌"))); + Span.raw(" " + TuiIcons.FAIL))); } } @@ -165,16 +165,16 @@ private void checkJBang(List result) { String version = VersionHelper.getJBangVersion(); if (version != null) { result.add(Line.from( - Span.raw(" 📦 "), + Span.raw(TuiIcons.indent(TuiIcons.BUNDLED)), Span.styled(String.format("%-14s", "JBang"), Style.EMPTY.bold()), Span.raw(String.format("%-30s", version)), - Span.raw(" ✅"))); + Span.raw(" " + TuiIcons.OK))); } else { result.add(Line.from( - Span.raw(" 📦 "), + Span.raw(TuiIcons.indent(TuiIcons.BUNDLED)), Span.styled(String.format("%-14s", "JBang"), Style.EMPTY.bold()), Span.raw(String.format("%-30s", "Not detected")), - Span.raw(" ⚠️"))); + Span.raw(" " + TuiIcons.WARN))); } } @@ -187,24 +187,24 @@ private void checkMavenRepository(List result) { List.of("org.apache.camel:camel-api:" + version), Set.of(), false, false); result.add(Line.from( - Span.raw(" 🔧 "), + Span.raw(TuiIcons.indent(TuiIcons.INFRA)), Span.styled(String.format("%-14s", "Maven"), Style.EMPTY.bold()), Span.raw(String.format("%-30s", "Artifact resolution")), - Span.raw(" ✅"))); + Span.raw(" " + TuiIcons.OK))); } catch (MavenResolutionException e) { result.add(Line.from( - Span.raw(" 🔧 "), + Span.raw(TuiIcons.indent(TuiIcons.INFRA)), Span.styled(String.format("%-14s", "Maven"), Style.EMPTY.bold()), Span.raw(String.format("%-30s", "Resolution failed")), - Span.raw(" ❌"))); + Span.raw(" " + TuiIcons.FAIL))); result.add(Line.from(Span.styled(" " + TuiHelper.truncate(e.getMessage(), 40), Style.EMPTY.dim()))); } catch (Exception e) { result.add(Line.from( - Span.raw(" 🔧 "), + Span.raw(TuiIcons.indent(TuiIcons.INFRA)), Span.styled(String.format("%-14s", "Maven"), Style.EMPTY.bold()), Span.raw(String.format("%-30s", "Error")), - Span.raw(" ❌"))); + Span.raw(" " + TuiIcons.FAIL))); result.add(Line.from(Span.styled(" " + TuiHelper.truncate(e.getMessage(), 40), Style.EMPTY.dim()))); } @@ -221,10 +221,10 @@ private void checkContainerRuntime(List result) { if (exit == 0) { String name = Character.toUpperCase(cmd.charAt(0)) + cmd.substring(1); result.add(Line.from( - Span.raw(" 🐳 "), + Span.raw(TuiIcons.indent(TuiIcons.DOCKER)), Span.styled(String.format("%-14s", "Container"), Style.EMPTY.bold()), Span.raw(String.format("%-30s", name + " running")), - Span.raw(" ✅"))); + Span.raw(" " + TuiIcons.OK))); return; } } catch (Exception e) { @@ -232,10 +232,10 @@ private void checkContainerRuntime(List result) { } } result.add(Line.from( - Span.raw(" 🐳 "), + Span.raw(TuiIcons.indent(TuiIcons.DOCKER)), Span.styled(String.format("%-14s", "Container"), Style.EMPTY.bold()), Span.raw(String.format("%-30s", "Not found (optional)")), - Span.raw(" ⚠️"))); + Span.raw(" " + TuiIcons.WARN))); } private void checkCommonPorts(List result) { @@ -250,16 +250,16 @@ private void checkCommonPorts(List result) { } if (!conflicts.isEmpty()) { result.add(Line.from( - Span.raw(" 🔌 "), + Span.raw(TuiIcons.indent(TuiIcons.ENDPOINT)), Span.styled(String.format("%-14s", "Ports"), Style.EMPTY.bold()), Span.raw(String.format("%-30s", "In use: " + conflicts)), - Span.raw(" ⚠️"))); + Span.raw(" " + TuiIcons.WARN))); } else { result.add(Line.from( - Span.raw(" 🔌 "), + Span.raw(TuiIcons.indent(TuiIcons.ENDPOINT)), Span.styled(String.format("%-14s", "Ports"), Style.EMPTY.bold()), Span.raw(String.format("%-30s", "8080, 8443, 9090 free")), - Span.raw(" ✅"))); + Span.raw(" " + TuiIcons.OK))); } } @@ -290,16 +290,16 @@ private void checkAiProvider(List result) { } if (provider != null) { result.add(Line.from( - Span.raw(" 🤖 "), + Span.raw(TuiIcons.indent(TuiIcons.MCP)), Span.styled(String.format("%-14s", "AI"), Style.EMPTY.bold()), Span.raw(String.format("%-30s", provider)), - Span.raw(" ✅"))); + Span.raw(" " + TuiIcons.OK))); } else { result.add(Line.from( - Span.raw(" 🤖 "), + Span.raw(TuiIcons.indent(TuiIcons.MCP)), Span.styled(String.format("%-14s", "AI"), Style.EMPTY.bold()), Span.raw(String.format("%-30s", "No API key configured")), - Span.raw(" ⚠️"))); + Span.raw(" " + TuiIcons.WARN))); result.add(Line.from(Span.styled(" Set ANTHROPIC_API_KEY or OPENAI_API_KEY", Style.EMPTY.dim()))); } @@ -310,25 +310,25 @@ private void checkMcpConnection(List result) { String client = mcpConnectedClient != null ? mcpConnectedClient.get() : null; if (client != null) { result.add(Line.from( - Span.raw(" 🔗 "), + Span.raw(TuiIcons.indent(TuiIcons.MCP)), Span.styled(String.format("%-14s", "MCP"), Style.EMPTY.bold()), Span.raw(String.format("%-30s", client + " (port " + mcpPort + ")")), - Span.raw(" ✅"))); + Span.raw(" " + TuiIcons.OK))); } else { result.add(Line.from( - Span.raw(" 🔗 "), + Span.raw(TuiIcons.indent(TuiIcons.MCP)), Span.styled(String.format("%-14s", "MCP"), Style.EMPTY.bold()), Span.raw(String.format("%-30s", "Listening on port " + mcpPort)), - Span.raw(" ⚠️"))); + Span.raw(" " + TuiIcons.WARN))); result.add(Line.from(Span.styled(" No AI client connected", Style.EMPTY.dim()))); } } else { result.add(Line.from( - Span.raw(" 🔗 "), + Span.raw(TuiIcons.indent(TuiIcons.MCP)), Span.styled(String.format("%-14s", "MCP"), Style.EMPTY.bold()), Span.raw(String.format("%-30s", "Not enabled")), - Span.raw(" ⚠️"))); + Span.raw(" " + TuiIcons.WARN))); result.add(Line.from(Span.styled(" Use --mcp to enable MCP server", Style.EMPTY.dim()))); } @@ -339,11 +339,11 @@ private void checkDiskSpace(List result) { long free = tmpDir.getFreeSpace(); long mb = free / (1024 * 1024); long gb = mb / 1024; - String emoji = mb > 500 ? "✅" : "⚠️"; + String emoji = mb > 500 ? TuiIcons.OK : TuiIcons.WARN; String unit = gb > 10 ? "GB" : "MB"; long value = gb > 0 ? gb : mb; result.add(Line.from( - Span.raw(" 💾 "), + Span.raw(TuiIcons.indent(TuiIcons.MEMORY)), Span.styled(String.format("%-14s", "Disk Space"), Style.EMPTY.bold()), Span.raw(String.format("%-30s", value + " " + unit + " free in temp dir")), Span.raw(" " + emoji))); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DrawOverlay.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DrawOverlay.java index aeb651c921920..215dd18b6eb4b 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DrawOverlay.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DrawOverlay.java @@ -145,13 +145,13 @@ private static List generateArrow(int x, int y, int length, int dx, in String shaft = dy != 0 ? "│" : "─"; String head; if (dx > 0) { - head = "▶"; + head = TuiIcons.ARROW_RIGHT; } else if (dx < 0) { - head = "◀"; + head = TuiIcons.ARROW_LEFT; } else if (dy > 0) { - head = "▼"; + head = TuiIcons.SORT_DOWN; } else { - head = "▲"; + head = TuiIcons.SORT_UP; } for (int i = 0; i < length - 1; i++) { cells.add(new DrawCell(x + i * dx, y + i * dy, shaft, s)); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java index 7e79218465902..eaa0bdd5692ca 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java @@ -173,9 +173,9 @@ protected void renderContent(Frame frame, Rect area, IntegrationInfo info) { default -> Style.EMPTY.fg(Color.YELLOW); }; String arrow = switch (dir) { - case "in" -> "→ "; - case "out" -> "← "; - default -> "↔ "; + case "in" -> TuiIcons.KEY_RIGHT + " "; + case "out" -> TuiIcons.KEY_LEFT + " "; + default -> TuiIcons.ARROW_BOTH + " "; }; List cells = new ArrayList<>(); @@ -300,7 +300,7 @@ protected void renderContent(Frame frame, Rect area, IntegrationInfo info) { @Override public void renderFooter(List spans) { hint(spans, "Esc", "back"); - hint(spans, "↑↓", "navigate"); + hint(spans, TuiIcons.HINT_SCROLL, "navigate"); hint(spans, "s", "sort"); String[] filterLabels = { "all", "remote", "remote+stub" }; hint(spans, "f", "filter [" + filterLabels[filter] + "]"); @@ -385,7 +385,7 @@ private void renderEndpointFlow( int sideLen = Math.max(4, (w - boxLen - 2) / 2); String arm = "─".repeat(Math.max(1, sideLen - 1)); - String arrowStr = arm + "►"; + String arrowStr = arm + TuiIcons.POINTER; String inStr = String.valueOf(inTotal); String outStr = String.valueOf(outTotal); @@ -501,7 +501,7 @@ private void renderSingleEndpointChart(Frame frame, Rect area, String selectedUr int boxLen = CharWidth.of(box); int sideLen = Math.max(4, (w - boxLen - 2) / 2); String arm = "─".repeat(Math.max(1, sideLen - 1)); - String arrowStr = arm + "►"; + String arrowStr = arm + TuiIcons.POINTER; String inStr = String.valueOf(inTotal); String outStr = String.valueOf(outTotal); int inPad = Math.max(0, sideLen - inStr.length()); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java index 46bad5ff1efab..870af7955c5d4 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java @@ -408,7 +408,7 @@ public void renderFooter(List spans) { }; if (diagram.isHistoryTopologyMode()) { hint(spans, "d", "close"); - hint(spans, "↑↓←→", "navigate"); + hint(spans, TuiIcons.HINT_NAV, "navigate"); hint(spans, "Enter", "drill-down"); hint(spans, "i", infoLabel); hint(spans, "n", "description" + (diagram.isShowDescription() ? " [on]" : "")); @@ -417,8 +417,8 @@ public void renderFooter(List spans) { } else { hint(spans, "d", "close"); hint(spans, "Esc", "back"); - hint(spans, "↑↓", "step through path"); - hint(spans, "←→", "h-scroll"); + hint(spans, TuiIcons.HINT_SCROLL, "step through path"); + hint(spans, TuiIcons.HINT_H, "h-scroll"); hint(spans, "t", "topology"); hint(spans, "i", infoLabel); hint(spans, "n", "description" + (diagram.isShowDescription() ? " [on]" : "")); @@ -431,10 +431,10 @@ public void renderFooter(List spans) { return; } hint(spans, "Esc", "back"); - hint(spans, "↑↓", "navigate"); + hint(spans, TuiIcons.HINT_SCROLL, "navigate"); hint(spans, "PgUp/Dn", "scroll detail"); if (!wordWrap) { - hint(spans, "←→", "h-scroll"); + hint(spans, TuiIcons.HINT_H, "h-scroll"); } hint(spans, "Home/End", "top/end"); hint(spans, "s", "sort"); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ExampleBrowserPopup.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ExampleBrowserPopup.java index 01d2b5b8dcf9d..dcbae7e6cdf2c 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ExampleBrowserPopup.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ExampleBrowserPopup.java @@ -404,8 +404,8 @@ private List buildListItems(int width) { boolean citrus = ExampleHelper.hasCitrusTests(ex); boolean infra = !ExampleHelper.getInfraServices(ex).isEmpty(); - String icons = (bundled ? "📦" : "🌐") + (docker ? "🐳" : " ") - + (infra ? "🔧" : " ") + (citrus ? "🍋" : " "); + String icons = (bundled ? TuiIcons.BUNDLED : TuiIcons.ONLINE) + (docker ? TuiIcons.DOCKER : " ") + + (infra ? TuiIcons.INFRA : " ") + (citrus ? TuiIcons.CITRUS : " "); int nameCol = Math.min(30, width / 3); String padded = String.format("%-" + nameCol + "s", TuiHelper.truncate(name, nameCol)); String prefix = " " + icons + " " + padded + " "; @@ -426,7 +426,9 @@ private List buildListItems(int width) { } } items.add(ListItem.from("")); - items.add(ListItem.from(" 📦 = bundled 🌐 = online 🐳 = Docker 🔧 = infra services 🍋 = Citrus tests") + items.add(ListItem.from(" " + TuiIcons.BUNDLED + " = bundled " + TuiIcons.ONLINE + " = online " + + TuiIcons.DOCKER + " = Docker " + TuiIcons.INFRA + " = infra services " + TuiIcons.CITRUS + + " = Citrus tests") .style(Style.EMPTY.dim())); return items; } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FilesBrowser.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FilesBrowser.java index 73ef0ef882f58..4b442b1c84bd6 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FilesBrowser.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FilesBrowser.java @@ -98,7 +98,7 @@ private boolean loadDirectory(Path dir) { .forEach(p -> { String name = p.getFileName().toString(); if (Files.isDirectory(p) && !name.startsWith(".")) { - dirs.add(new FileEntry("📁", name, -1, p.toString(), true)); + dirs.add(new FileEntry(TuiIcons.FOLDER, name, -1, p.toString(), true)); } else if (Files.isRegularFile(p)) { String emoji = TuiHelper.fileEmoji(p); long size = 0; @@ -118,7 +118,7 @@ private boolean loadDirectory(Path dir) { List found = new ArrayList<>(); if (!dir.equals(rootDir)) { - found.add(new FileEntry("📁", "..", -1, dir.getParent().toString(), true)); + found.add(new FileEntry(TuiIcons.FOLDER, "..", -1, dir.getParent().toString(), true)); } found.addAll(dirs); found.addAll(files); @@ -267,7 +267,7 @@ void renderFooter(List spans) { if (sourceViewer.isVisible()) { sourceViewer.renderFooter(spans); } else { - TuiHelper.hint(spans, "↑↓", "navigate"); + TuiHelper.hint(spans, TuiIcons.HINT_SCROLL, "navigate"); TuiHelper.hint(spans, "Enter", "open"); TuiHelper.hint(spans, "Esc", "close"); } @@ -328,10 +328,10 @@ static String formatFileSize(long bytes) { static String fileType(Path path) { String emoji = TuiHelper.fileEmoji(path); return switch (emoji) { - case "🐪" -> "camel"; - case "☕" -> "java"; - case "📄" -> "config"; - case "📖" -> "readme"; + case TuiIcons.CAMEL -> "camel"; + case TuiIcons.JAVA -> "java"; + case TuiIcons.DOCUMENT -> "config"; + case TuiIcons.README -> "readme"; default -> "other"; }; } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FolderBrowser.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FolderBrowser.java index e83230343912f..d3d279f2ba8c3 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FolderBrowser.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FolderBrowser.java @@ -106,7 +106,7 @@ private boolean loadDirectory(Path dir, String selectName) { .forEach(p -> { String name = p.getFileName().toString(); if (Files.isDirectory(p)) { - dirs.add(new DirEntry("📁", name, p.toString(), true)); + dirs.add(new DirEntry(TuiIcons.FOLDER, name, p.toString(), true)); } else { files.add(new DirEntry(TuiHelper.fileEmoji(p), name, p.toString(), false)); } @@ -120,7 +120,7 @@ private boolean loadDirectory(Path dir, String selectName) { List found = new ArrayList<>(); Path parent = dir.getParent(); if (parent != null) { - found.add(new DirEntry("📁", "..", parent.toString(), true)); + found.add(new DirEntry(TuiIcons.FOLDER, "..", parent.toString(), true)); } found.addAll(dirs); found.addAll(files); @@ -297,7 +297,7 @@ void render(Frame frame, Rect area) { } String dirLabel = currentDir != null ? currentDir.toString() : ""; - String popupTitle = " 📂 " + dirLabel + " "; + String popupTitle = " " + TuiIcons.FOLDER_OPEN + " " + dirLabel + " "; int nameWidth = entries.stream().mapToInt(e -> e.name().length()).max().orElse(10); int itemWidth = 6 + nameWidth; @@ -338,7 +338,7 @@ void renderFooter(List spans) { sourceViewer.renderFooter(spans); return; } - TuiHelper.hint(spans, "↑↓", "navigate"); + TuiHelper.hint(spans, TuiIcons.HINT_SCROLL, "navigate"); TuiHelper.hint(spans, "Enter", "open"); TuiHelper.hint(spans, "Tab", "select"); TuiHelper.hintLast(spans, "Esc", "close"); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthTab.java index 705d20a2ef5b2..5e6d7949b64b9 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthTab.java @@ -98,13 +98,13 @@ protected void renderContent(Frame frame, Rect area, IntegrationInfo info) { String icon; if ("UP".equals(hc.state)) { stateStyle = Style.EMPTY.fg(Color.GREEN); - icon = "✔ "; + icon = TuiIcons.HEALTH_UP + " "; } else if ("DOWN".equals(hc.state)) { stateStyle = Style.EMPTY.fg(Color.LIGHT_RED); - icon = "✖ "; + icon = TuiIcons.HEALTH_DOWN + " "; } else { stateStyle = Style.EMPTY.fg(Color.YELLOW); - icon = "⚠ "; + icon = TuiIcons.HEALTH_WARN + " "; } String kind = ""; diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HelpOverlay.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HelpOverlay.java index d1ff39391854a..fa96d14819e81 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HelpOverlay.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HelpOverlay.java @@ -96,7 +96,7 @@ void render(Frame frame, Rect area) { .title(" Help ") .titleBottom(Title.from(Line.from( Span.styled(" F1/? ", Theme.hintKey()), Span.raw(" close "), - Span.styled(" ↑↓ ", Theme.hintKey()), Span.raw(" scroll ")))) + Span.styled(" " + TuiIcons.HINT_SCROLL + " ", Theme.hintKey()), Span.raw(" scroll ")))) .build(); MarkdownView view = MarkdownView.builder() @@ -108,7 +108,7 @@ void render(Frame frame, Rect area) { } void renderFooter(List spans) { - hint(spans, "↑↓", "scroll"); + hint(spans, TuiIcons.HINT_SCROLL, "scroll"); hintLast(spans, "Esc", "close"); } } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java index 9dd9db56290da..f7167163cf6fc 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java @@ -746,7 +746,7 @@ public void renderFooter(List spans) { }; if (diagram.isHistoryTopologyMode()) { hint(spans, "d", "close"); - hint(spans, "↑↓←→", "navigate"); + hint(spans, TuiIcons.HINT_NAV, "navigate"); hint(spans, "Enter", "drill-down"); hint(spans, "i", infoLabel); hint(spans, "n", "description" + (showDescription ? " [on]" : "")); @@ -755,8 +755,8 @@ public void renderFooter(List spans) { } else { hint(spans, "d", "close"); hint(spans, "Esc", "back"); - hint(spans, "↑↓", "step through path"); - hint(spans, "←→", "h-scroll"); + hint(spans, TuiIcons.HINT_SCROLL, "step through path"); + hint(spans, TuiIcons.HINT_H, "h-scroll"); hint(spans, "t", "topology"); hint(spans, "i", infoLabel); hint(spans, "n", "description" + (showDescription ? " [on]" : "")); @@ -771,10 +771,10 @@ public void renderFooter(List spans) { boolean tracerActive = !traces.get().isEmpty(); if (tracerActive && traceDetailView) { hint(spans, "Esc", "back"); - hint(spans, "↑↓", "navigate"); + hint(spans, TuiIcons.HINT_SCROLL, "navigate"); hint(spans, "PgUp/PgDn", "scroll"); if (!showWaterfall && !traceWordWrap) { - hint(spans, "←→", "h-scroll"); + hint(spans, TuiIcons.HINT_H, "h-scroll"); } hint(spans, "n", "description" + (showDescription ? " [on]" : "")); hint(spans, "g", "waterfall" + (showWaterfall ? " [on]" : "")); @@ -785,7 +785,7 @@ public void renderFooter(List spans) { hintLast(spans, "w", "wrap" + (traceWordWrap ? " [on]" : " [off]")); } else if (tracerActive) { hint(spans, "Esc", "back"); - hint(spans, "↑↓", "navigate"); + hint(spans, TuiIcons.HINT_SCROLL, "navigate"); hint(spans, "s", "sort"); hint(spans, "n", "description" + (showDescription ? " [on]" : "")); hint(spans, "d", "diagram"); @@ -793,10 +793,10 @@ public void renderFooter(List spans) { hintLast(spans, "F5", "refresh"); } else { hint(spans, "Esc", "back"); - hint(spans, "↑↓", "navigate"); + hint(spans, TuiIcons.HINT_SCROLL, "navigate"); hint(spans, "PgUp/PgDn", "scroll"); if (!showWaterfall && !historyWordWrap) { - hint(spans, "←→", "h-scroll"); + hint(spans, TuiIcons.HINT_H, "h-scroll"); } hint(spans, "n", "description" + (showDescription ? " [on]" : "")); hint(spans, "g", "waterfall" + (showWaterfall ? " [on]" : "")); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java index 799a836eae5b9..69e2a35e6f21d 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java @@ -295,13 +295,13 @@ public void renderFooter(List spans) { hint(spans, "+", "header"); hint(spans, "p", "pretty" + (probePrettyPrint ? " [on]" : "")); if (!probeHistory.isEmpty()) { - hintLast(spans, "↑↓", "history"); + hintLast(spans, TuiIcons.HINT_SCROLL, "history"); } return; } if (showSpec) { hint(spans, "c/Esc", "close"); - hint(spans, "↑↓", "scroll"); + hint(spans, TuiIcons.HINT_SCROLL, "scroll"); hintLast(spans, "PgUp/PgDn", "page"); return; } @@ -929,8 +929,8 @@ private void renderProbeRequest(Frame frame, Rect area) { FormHelper.renderLabel(frame, innerX, row, labelW, "Method:", probeField == PROBE_METHOD); Rect methodArea = new Rect(innerX + labelW, row, fieldW, 1); Style methodSt = methodStyle(method); - String leftArr = probeField == PROBE_METHOD ? "◀ " : " "; - String rightArr = probeField == PROBE_METHOD ? " ▶" : ""; + String leftArr = probeField == PROBE_METHOD ? TuiIcons.ARROW_LEFT + " " : " "; + String rightArr = probeField == PROBE_METHOD ? " " + TuiIcons.ARROW_RIGHT : ""; frame.renderWidget(Paragraph.from(Line.from( Span.styled(leftArr, methodSt), Span.styled(method, methodSt.bold()), @@ -1120,7 +1120,7 @@ private void renderProbeHistory(Frame frame, Rect area) { for (int i = start; i < end; i++) { ProbeHistoryEntry entry = probeHistory.get(i); boolean selected = probeField == PROBE_HISTORY && i == probeHistoryIndex; - String pointer = selected ? "► " : " "; + String pointer = selected ? TuiIcons.POINTER + " " : " "; String methodStr = String.format("%-8s", entry.method); String statusStr = entry.error ? "ERR" : entry.statusText; String elapsedStr = entry.elapsed > 0 ? entry.elapsed + "ms" : ""; diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/InfraBrowserPopup.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/InfraBrowserPopup.java index af8e75fc15d18..4ac21e8b4d859 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/InfraBrowserPopup.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/InfraBrowserPopup.java @@ -288,7 +288,7 @@ private void renderBrowser(Frame frame, Rect area) { List items = new ArrayList<>(); for (InfraServiceEntry entry : catalog) { String padded = String.format("%-" + nameCol + "s", TuiHelper.truncate(entry.alias(), nameCol)); - String prefix = " 🔧 " + padded + " "; + String prefix = TuiIcons.indent(TuiIcons.INFRA) + padded + " "; if (entry.running()) { items.add(ListItem.from(prefix + "(running)").style(Style.EMPTY.dim())); } else { diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTab.java index f4338d3c326ff..520d84b0ab3aa 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTab.java @@ -407,7 +407,7 @@ public void renderFooter(List spans) { return; } if (showLogLevelPopup) { - hint(spans, "↑↓", "navigate"); + hint(spans, TuiIcons.HINT_SCROLL, "navigate"); hint(spans, "Enter", "set level"); hintLast(spans, "Esc", "cancel"); return; @@ -418,7 +418,7 @@ public void renderFooter(List spans) { } else { hint(spans, "Esc", "back"); } - hint(spans, "↑↓", "scroll"); + hint(spans, TuiIcons.HINT_SCROLL, "scroll"); search.renderSearchHints(spans); hint(spans, "w", "wrap" + (wordWrap ? " [on]" : " [off]")); if (!ctx.isInfraSelected()) { 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 d15d8ee4cd921..14b1f5c68a522 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 @@ -87,12 +87,6 @@ interface MonitorBridge { "HTTP", "Health", "Inspect", "Errors", "More" }; - static final String[] MORE_TAB_NAMES = { - "Beans", "Browse", "Circuit Breaker", "Classpath", "Configuration", - "Consumers", "CVE Audit", "DataSource", "Heap Histogram", "Inflight", "Memory", "Memory Leak", "Metrics", - "SQL Query", "SQL Trace", "Spans", "Process", "Startup", "Threads" - }; - private final MonitorContext ctx; private final AtomicReference> data; private final TabsState tabsState; @@ -233,10 +227,11 @@ String navigateToTab(String tabName) { } } // Check More submenu tabs - for (int i = 0; i < MORE_TAB_NAMES.length; i++) { - if (MORE_TAB_NAMES[i].equalsIgnoreCase(tabName)) { + List moreTabs = tabRegistry.moreTabs(); + for (int i = 0; i < moreTabs.size(); i++) { + if (moreTabs.get(i).name().equalsIgnoreCase(tabName)) { bridge.selectMoreTab(i); - return MORE_TAB_NAMES[i]; + return moreTabs.get(i).name(); } } return null; @@ -260,7 +255,9 @@ String selectIntegration(String nameOrPid) { List getTabNames() { List names = new ArrayList<>(); names.addAll(List.of(TAB_NAMES)); - names.addAll(List.of(MORE_TAB_NAMES)); + for (TabRegistry.MoreTab mt : tabRegistry.moreTabs()) { + names.add(mt.name()); + } return names; } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpLogPopup.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpLogPopup.java index a2e26e4b1f6c1..90e80c6ed204e 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpLogPopup.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpLogPopup.java @@ -127,7 +127,7 @@ void render(Frame frame, Rect area) { } void renderFooter(List spans) { - hint(spans, "↑↓", "select"); + hint(spans, TuiIcons.HINT_SCROLL, "select"); hint(spans, "PgUp/Dn", "scroll detail"); hintLast(spans, "Esc", "back"); } @@ -182,12 +182,12 @@ private void renderDetail(Frame frame, Rect area) { TuiMcpServer.LogEntry entry = entries.get(selected); List lines = new ArrayList<>(); if (entry.requestBody() != null) { - lines.add(Line.from(Span.styled("▶ Request", Style.EMPTY.fg(Color.YELLOW).bold()))); + lines.add(Line.from(Span.styled(TuiIcons.ARROW_RIGHT + " Request", Style.EMPTY.fg(Color.YELLOW).bold()))); addJsonLines(lines, entry.requestBody()); lines.add(Line.from(Span.raw(""))); } if (entry.responseBody() != null) { - lines.add(Line.from(Span.styled("◀ Response", Style.EMPTY.fg(Color.GREEN).bold()))); + lines.add(Line.from(Span.styled(TuiIcons.ARROW_LEFT + " Response", Style.EMPTY.fg(Color.GREEN).bold()))); addJsonLines(lines, entry.responseBody()); } if (entry.requestBody() == null && entry.responseBody() == null) { diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryLeakTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryLeakTab.java index 45daa1511fd85..f2d6be86c8f4a 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryLeakTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryLeakTab.java @@ -551,7 +551,7 @@ private void renderComparisonTable(Frame frame, Rect area) { growth = "~" + growth; } Span trendSpan = trendSpan(e.trend); - String warn = e.lowConfidence ? " ⚠" : ""; + String warn = e.lowConfidence ? " " + TuiIcons.HEALTH_WARN : ""; rows.add(Row.from( rightCell(String.valueOf(i + 1), 4), @@ -642,7 +642,7 @@ private void renderComparisonDetail(Frame frame, Rect area) { } if (entry.lowConfidence) { lines.add(Line.from( - Span.styled(" ⚠ Low confidence: ", Style.EMPTY.fg(Color.YELLOW)), + Span.styled(" " + TuiIcons.HEALTH_WARN + " Low confidence: ", Style.EMPTY.fg(Color.YELLOW)), Span.styled("sample counts are too low or diverge significantly", Style.EMPTY.dim()))); lines.add(Line.from( Span.styled(" between runs. The growth percentage may not be reliable.", Style.EMPTY.dim()))); @@ -707,10 +707,10 @@ private static Span trendSpan(String trend) { return Span.styled("-", Style.EMPTY.dim()); } return switch (trend) { - case "growing" -> Span.styled("↑ leak!", Style.EMPTY.fg(Color.RED).bold()); - case "suspicious" -> Span.styled("↑ leak?", Style.EMPTY.fg(Color.YELLOW).bold()); - case "stable" -> Span.styled("→ stable", Style.EMPTY.fg(Color.GREEN)); - case "shrinking" -> Span.styled("↓", Style.EMPTY.dim()); + case "growing" -> Span.styled(TuiIcons.ARROW_UP + " leak!", Style.EMPTY.fg(Color.RED).bold()); + case "suspicious" -> Span.styled(TuiIcons.ARROW_UP + " leak?", Style.EMPTY.fg(Color.YELLOW).bold()); + case "stable" -> Span.styled(TuiIcons.ARROW_STABLE + " stable", Style.EMPTY.fg(Color.GREEN)); + case "shrinking" -> Span.styled(TuiIcons.ARROW_DOWN, Style.EMPTY.dim()); case "new" -> Span.styled("new", Style.EMPTY.fg(Color.YELLOW)); case "gone" -> Span.styled("gone", Style.EMPTY.dim()); default -> Span.styled(trend, Style.EMPTY.dim()); @@ -937,9 +937,9 @@ the class grew 30% faster than expected from the duration - **new** (yellow) — Only appeared in Run 2 - **gone** (dim) — Only appeared in Run 1 - ### Low Confidence ⚠ + ### Low Confidence %s - A **⚠** warning appears when sample counts are too low + A **%s** warning appears when sample counts are too low (fewer than 5 in either run) or diverge significantly from the expected duration ratio. The growth percentage is shown with a **~** prefix (e.g. ~+53%) to indicate the value may @@ -969,7 +969,7 @@ the class grew 30% faster than expected from the duration | m | Cycle minimum size filter | | PgUp/PgDn | Scroll detail panel | | Esc | Back | - """; + """.formatted(TuiIcons.HEALTH_WARN, TuiIcons.HEALTH_WARN); } // ---- Action methods ---- diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryTab.java index 1cad072dc3c88..c48c27537eed5 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryTab.java @@ -418,15 +418,15 @@ private static Span computeTrendSpan(LinkedList hist, long heapCeiling) { if (change > 0.05) { return Span.styled( - String.format(" ↑ growing by %d%% (%s) over last %s", pct, formatBytes(diff), period), + String.format(" %s growing by %d%% (%s) over last %s", TuiIcons.ARROW_UP, pct, formatBytes(diff), period), Style.EMPTY.fg(Color.LIGHT_RED).bold()); } else if (change < -0.05) { return Span.styled( - String.format(" ↓ shrinking by %d%% (%s) over last %s", + String.format(" %s shrinking by %d%% (%s) over last %s", TuiIcons.ARROW_DOWN, Math.abs(pct), formatBytes(Math.abs(diff)), period), Style.EMPTY.fg(Color.GREEN)); } else { - return Span.styled(" → stable over last " + period, Style.EMPTY.fg(Color.GREEN)); + return Span.styled(" " + TuiIcons.ARROW_STABLE + " stable over last " + period, Style.EMPTY.fg(Color.GREEN)); } } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsTab.java index 4652d62f29a89..81a054bd47c82 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsTab.java @@ -622,7 +622,7 @@ private void applyRawResult(String url, List lines, String contentType) @Override public void renderFooter(List spans) { if (showRaw) { - hint(spans, "↑↓", "scroll"); + hint(spans, TuiIcons.HINT_SCROLL, "scroll"); hint(spans, "PgUp/Dn", "page"); hint(spans, "F5", "refresh"); hintLast(spans, "Esc", "close"); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java index 09cb36256cdde..dab3cce6e4a88 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java @@ -283,13 +283,13 @@ public void render(Frame frame, Rect area) { int gray = (int) (100 * fade); Style dimStyle = Style.EMPTY.fg(Color.indexed(232 + Math.min(gray / 4, 23))); - String vanishName = "🐪 " + (info.name != null ? info.name : ""); + String vanishName = TuiIcons.labeled(TuiIcons.CAMEL, info.name != null ? info.name : ""); rows.add(Row.from( Cell.from(Span.styled(info.pid, dimStyle)), Cell.from(Span.styled(vanishName, dimStyle)), Cell.from(Span.styled("", dimStyle)), Cell.from(Span.styled("", dimStyle)), - Cell.from(Span.styled("✖ Stopped", Style.EMPTY.fg(Color.LIGHT_RED).dim())), + Cell.from(Span.styled(TuiIcons.STOPPED + " Stopped", Style.EMPTY.fg(Color.LIGHT_RED).dim())), Cell.from(Span.styled(info.ago != null ? info.ago : "", dimStyle)), Cell.from(Span.styled("", dimStyle)), Cell.from(Span.styled("", dimStyle)), @@ -320,11 +320,7 @@ public void render(Frame frame, Rect area) { if (!hasDoc) { hasDoc = hasReadmeInSourceDir(info); } - String platformIcon = switch (info.platform != null ? info.platform : "") { - case "Spring Boot" -> "🍃"; - case "Quarkus" -> "🚀"; - default -> "🐪"; - }; + String platformIcon = TuiIcons.runtimeIcon(info.platform != null ? info.platform : ""); String nameText = platformIcon + " " + (info.name != null ? info.name : ""); List nameSpans = new ArrayList<>(); nameSpans.add(Span.styled(nameText, Style.EMPTY.fg(Color.CYAN))); @@ -332,7 +328,7 @@ public void render(Frame frame, Rect area) { nameSpans.add(Span.styled(" [dev]", Style.EMPTY.fg(Color.YELLOW).dim())); } if (hasDoc) { - nameSpans.add(Span.styled(" 📖", Style.EMPTY)); + nameSpans.add(Span.styled(" " + TuiIcons.README, Style.EMPTY)); } Line nameLine = Line.from(nameSpans); String throughputDisplay = info.throughput; @@ -394,13 +390,13 @@ public void render(Frame frame, Rect area) { float fade = 1.0f - Math.min(1.0f, (float) elapsed / VANISH_DURATION_MS); int gray = (int) (100 * fade); Style dimStyle = Style.EMPTY.fg(Color.indexed(232 + Math.min(gray / 4, 23))); - String vanishAlias = "🔧 " + info.alias; + String vanishAlias = TuiIcons.INFRA + " " + info.alias; rows.add(Row.from( Cell.from(Span.styled(info.pid, dimStyle)), Cell.from(Span.styled(vanishAlias, dimStyle)), Cell.from(Span.styled("", dimStyle)), Cell.from(Span.styled("", dimStyle)), - Cell.from(Span.styled("✖ Stopped", Style.EMPTY.fg(Color.LIGHT_RED).dim())), + Cell.from(Span.styled(TuiIcons.STOPPED + " Stopped", Style.EMPTY.fg(Color.LIGHT_RED).dim())), Cell.from(Span.styled("", dimStyle)), Cell.from(Span.styled("", dimStyle)), Cell.from(Span.styled("", dimStyle)), @@ -410,7 +406,7 @@ public void render(Frame frame, Rect area) { Cell.from(Span.styled("", dimStyle))).style(rowBg)); } else { String statusText = info.alive ? "Running" : "Stopped"; - String infraAlias = "🔧 " + info.alias; + String infraAlias = TuiIcons.INFRA + " " + info.alias; String version = info.serviceVersion != null ? info.serviceVersion : ""; rows.add(Row.from( Cell.from(info.pid), @@ -629,12 +625,7 @@ private void renderInfoPanel(Frame frame, Rect area) { int jvmDetailCount = 0; if (sel != null) { if (sel.platform != null) { - String platEmoji = switch (sel.platform) { - case "Spring Boot" -> "🍃 "; - case "Quarkus" -> "🚀 "; - case "JBang", "Camel" -> "🐪 "; - default -> ""; - }; + String platEmoji = TuiIcons.platformIcon(sel.platform); String plat = sel.platformVersion != null ? platEmoji + sel.platform + " v" + sel.platformVersion : platEmoji + sel.platform; @@ -651,7 +642,7 @@ private void renderInfoPanel(Frame frame, Rect area) { List profileSpans = new ArrayList<>(); profileSpans.add(Span.styled("Profile: ", dim)); String profile = sel.profile != null ? sel.profile : "prod"; - String profileEmoji = "dev".equals(profile) ? "🛠️ " : "prod".equals(profile) ? "📦 " : ""; + String profileEmoji = TuiIcons.profilePrefix(profile); profileSpans.add(Span.raw(profileEmoji + profile)); if (sel.reloaded > 0) { profileSpans.add(Span.raw(" ")); @@ -781,7 +772,7 @@ public void renderFooter(List spans) { if (ctx.selectedPid != null) { hint(spans, "Esc", "unselect"); } - hint(spans, "↑↓", "navigate"); + hint(spans, TuiIcons.HINT_SCROLL, "navigate"); if (!ctx.isInfraSelected()) { hint(spans, "s", "sort"); hint(spans, "a", "chart " + switch (chartMode) { @@ -1055,11 +1046,11 @@ private void renderEmptyState(Frame frame, Rect area) { } lines.add(Line.from(Span.styled(" No Active Camel Integrations Found", Theme.title()))); lines.add(Line.from(Span.raw(""))); - lines.add(Line.from(Span.styled(" 💡 How to monitor integrations:", Style.EMPTY.bold()))); + lines.add(Line.from(Span.styled(TuiIcons.indent(TuiIcons.TIP) + "How to monitor integrations:", Style.EMPTY.bold()))); lines.add(Line.from(Span.raw(" Run a route or integration in another terminal window:"))); lines.add(Line.from(Span.styled(" > camel run my-route.yaml", Theme.success()))); lines.add(Line.from(Span.raw(""))); - lines.add(Line.from(Span.styled(" 🐪 Or run a bundled example:", Style.EMPTY.bold()))); + lines.add(Line.from(Span.styled(TuiIcons.indent(TuiIcons.CAMEL) + "Or run a bundled example:", Style.EMPTY.bold()))); lines.add(Line.from(List.of( Span.raw(" Press "), Span.styled(" F2 ", Theme.hintKey()), @@ -1067,7 +1058,8 @@ private void renderEmptyState(Frame frame, Rect area) { Span.styled("Run Example", Style.EMPTY.bold()), Span.raw(".")))); lines.add(Line.from(Span.raw(""))); - lines.add(Line.from(Span.styled(" 💻 Or use the embedded JLine shell panel:", Style.EMPTY.bold()))); + lines.add(Line.from(Span.styled(TuiIcons.indent(TuiIcons.COMPUTER) + "Or use the embedded JLine shell panel:", + Style.EMPTY.bold()))); lines.add(Line.from(List.of( Span.raw(" Press "), Span.styled(" F6 ", Theme.hintKey()), @@ -1075,7 +1067,7 @@ private void renderEmptyState(Frame frame, Rect area) { lines.add(Line.from(Span.styled(" camel> run examples/demo.java", Theme.success()))); lines.add(Line.from(Span.raw(""))); lines.add(Line.from(List.of( - Span.styled(" ❔ For shortcut keys and documentation, press ", Theme.muted()), + Span.styled(TuiIcons.indent(TuiIcons.HELP) + "For shortcut keys and documentation, press ", Theme.muted()), Span.styled(" ? ", Theme.hintKey()), Span.styled(" or ", Theme.muted()), Span.styled(" F1 ", Theme.hintKey()), 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 29fa974963567..d18cf7ab33e35 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 @@ -63,6 +63,7 @@ interface PopupCallbacks { private final MonitorContext ctx; private final Supplier> nonVanishingIntegrationsSupplier; + private final Supplier> moreTabsSupplier; private final PopupCallbacks callbacks; private final FilesBrowser filesBrowser; @@ -82,16 +83,21 @@ interface PopupCallbacks { // Last rendered popup rects for mouse hit-testing private Rect lastMorePopupRect; private Rect lastSwitchPopupRect; - private static final int MORE_POPUP_ITEM_COUNT = 19; PopupManager(MonitorContext ctx, Supplier> nonVanishingIntegrationsSupplier, + Supplier> moreTabsSupplier, FilesBrowser filesBrowser, PopupCallbacks callbacks) { this.ctx = ctx; this.nonVanishingIntegrationsSupplier = nonVanishingIntegrationsSupplier; + this.moreTabsSupplier = moreTabsSupplier; this.filesBrowser = filesBrowser; this.callbacks = callbacks; } + private int moreTabCount() { + return moreTabsSupplier.get().size(); + } + // ---- State queries ---- boolean isAnyPopupVisible() { @@ -184,7 +190,7 @@ private boolean handleMorePopupKeys(KeyEvent ke) { return true; } if (ke.isDown()) { - morePopupState.selectNext(MORE_POPUP_ITEM_COUNT); + morePopupState.selectNext(moreTabCount()); return true; } if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) { @@ -195,7 +201,7 @@ private boolean handleMorePopupKeys(KeyEvent ke) { } if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) { for (int i = 0; i < 5; i++) { - morePopupState.selectNext(MORE_POPUP_ITEM_COUNT); + morePopupState.selectNext(moreTabCount()); } return true; } @@ -204,7 +210,7 @@ private boolean handleMorePopupKeys(KeyEvent ke) { return true; } if (ke.isEnd() || ke.isKey(KeyCode.END)) { - morePopupState.selectLast(MORE_POPUP_ITEM_COUNT); + morePopupState.selectLast(moreTabCount()); return true; } int shortcutSel = morePopupShortcut(ke); @@ -311,7 +317,7 @@ private boolean handleMorePopupMouse(MouseEvent me) { } // Inside the popup: items start at y+1 (after border top row) and each is 1 row int itemIndex = me.y() - lastMorePopupRect.y() - 1; // -1 for top border - if (itemIndex < 0 || itemIndex >= MORE_POPUP_ITEM_COUNT) { + if (itemIndex < 0 || itemIndex >= moreTabCount()) { return true; // click on border area } if (me.kind() == MouseEventKind.SCROLL_UP) { @@ -319,7 +325,7 @@ private boolean handleMorePopupMouse(MouseEvent me) { return true; } if (me.kind() == MouseEventKind.SCROLL_DOWN) { - morePopupState.selectNext(MORE_POPUP_ITEM_COUNT); + morePopupState.selectNext(moreTabCount()); return true; } if (me.isClick()) { @@ -382,9 +388,9 @@ private boolean handleSwitchPopupMouse(MouseEvent me, int selectedTab, int tabLo // ---- Rendering ---- void renderMorePopup(Frame frame, Rect area) { - int popupW = 22; + int popupW = 28; int popupH = 21; - // Position just below the "0 More▾" tab label + // Position just below the More tab label int dividerW = CharWidth.of(" | "); int tabBarX = 0; Line[] tabLabels = currentTabLabels; @@ -405,27 +411,7 @@ void renderMorePopup(Frame frame, Rect area) { frame.renderWidget(Clear.INSTANCE, popup); Style keyStyle = Style.EMPTY.fg(Color.YELLOW).bold(); - ListItem[] items = { - ListItem.from(Line.from(Span.raw(" "), Span.styled("B", keyStyle), Span.raw("eans"))), - ListItem.from(Line.from(Span.raw(" Bro"), Span.styled("w", keyStyle), Span.raw("se"))), - ListItem.from(Line.from(Span.raw(" "), Span.styled("C", keyStyle), Span.raw("ircuit Breaker"))), - ListItem.from(Line.from(Span.raw(" Cl"), Span.styled("a", keyStyle), Span.raw("sspath"))), - ListItem.from(Line.from(Span.raw(" Confi"), Span.styled("g", keyStyle), Span.raw("uration"))), - ListItem.from(Line.from(Span.raw(" Co"), Span.styled("n", keyStyle), Span.raw("sumers"))), - ListItem.from(Line.from(Span.raw(" C"), Span.styled("V", keyStyle), Span.raw("E Audit"))), - ListItem.from(Line.from(Span.raw(" "), Span.styled("D", keyStyle), Span.raw("ataSource"))), - ListItem.from(Line.from(Span.raw(" "), Span.styled("H", keyStyle), Span.raw("eap Histogram"))), - ListItem.from(Line.from(Span.raw(" "), Span.styled("I", keyStyle), Span.raw("nflight"))), - ListItem.from(Line.from(Span.raw(" "), Span.styled("M", keyStyle), Span.raw("emory"))), - ListItem.from(Line.from(Span.raw(" Memory Lea"), Span.styled("k", keyStyle), Span.raw(""))), - ListItem.from(Line.from(Span.raw(" M"), Span.styled("e", keyStyle), Span.raw("trics"))), - ListItem.from(Line.from(Span.raw(" S"), Span.styled("Q", keyStyle), Span.raw("L Query"))), - ListItem.from(Line.from(Span.raw(" SQL T"), Span.styled("r", keyStyle), Span.raw("ace"))), - ListItem.from(Line.from(Span.raw(" "), Span.styled("O", keyStyle), Span.raw("Tel Spans"))), - ListItem.from(Line.from(Span.raw(" "), Span.styled("P", keyStyle), Span.raw("rocess"))), - ListItem.from(Line.from(Span.raw(" "), Span.styled("S", keyStyle), Span.raw("tartup"))), - ListItem.from(Line.from(Span.raw(" "), Span.styled("T", keyStyle), Span.raw("hreads"))), - }; + ListItem[] items = morePopupItems(keyStyle); ListWidget list = ListWidget.builder() .items(items) .highlightStyle(Theme.selectionBg()) @@ -433,12 +419,34 @@ void renderMorePopup(Frame frame, Rect area) { .scrollMode(ScrollMode.NONE) .block(Block.builder() .borderType(BorderType.ROUNDED).borders(Borders.ALL) - .title(Title.from(Line.from(Span.styled(" More Tabs ", Style.EMPTY.fg(Color.YELLOW).bold())))) + .title(Title.from(Line.from(Span.styled( + " " + TuiIcons.TAB_MORE + " More Tabs ", + Style.EMPTY.fg(Color.YELLOW).bold())))) .build()) .build(); frame.renderStatefulWidget(list, popup, morePopupState); } + private ListItem[] morePopupItems(Style keyStyle) { + List tabs = moreTabsSupplier.get(); + ListItem[] items = new ListItem[tabs.size()]; + for (int i = 0; i < tabs.size(); i++) { + TabRegistry.MoreTab tab = tabs.get(i); + String name = tab.displayName(); + String prefix = TuiIcons.indent(tab.icon()); + int keyPos = tab.mnemonicIndex(); + if (keyPos >= 0 && keyPos < name.length()) { + items[i] = ListItem.from(Line.from( + Span.raw(prefix + name.substring(0, keyPos)), + Span.styled(String.valueOf(name.charAt(keyPos)), keyStyle), + Span.raw(name.substring(keyPos + 1)))); + } else { + items[i] = ListItem.from(prefix + name); + } + } + return items; + } + void renderSwitchPopup(Frame frame, Rect area) { List integrations = nonVanishingIntegrationsSupplier.get(); if (integrations.isEmpty()) { @@ -467,7 +475,8 @@ void renderSwitchPopup(Frame frame, Rect area) { IntegrationInfo info = integrations.get(i); String name = info.name != null ? info.name : "?"; boolean current = info.pid.equals(ctx.selectedPid); - String label = String.format(" 🐪 %s (pid:%s)%s", name, info.pid, current ? " ●" : ""); + String label = String.format(" %s %s (pid:%s)%s", TuiIcons.CAMEL, name, info.pid, + current ? " " + TuiIcons.SELECTED : ""); if (current) { items[i] = ListItem.from(Line.from(Span.styled(label, Style.EMPTY.fg(Color.CYAN)))); } else { @@ -522,65 +531,18 @@ void renderKillConfirm(Frame frame, Rect area) { inner); } - // ---- Static utilities ---- - - static int morePopupShortcut(KeyEvent ke) { - if (ke.isChar('b')) { - return 0; - } - if (ke.isChar('w')) { - return 1; - } - if (ke.isChar('c')) { - return 2; - } - if (ke.isChar('a')) { - return 3; - } - if (ke.isChar('g')) { - return 4; - } - if (ke.isChar('n')) { - return 5; - } - if (ke.isChar('v')) { - return 6; - } - if (ke.isChar('d')) { - return 7; - } - if (ke.isChar('h')) { - return 8; - } - if (ke.isChar('i')) { - return 9; - } - if (ke.isChar('m')) { - return 10; - } - if (ke.isChar('k')) { - return 11; - } - if (ke.isChar('e')) { - return 12; - } - if (ke.isChar('q')) { - return 13; - } - if (ke.isChar('r')) { - return 14; - } - if (ke.isChar('o')) { - return 15; - } - if (ke.isChar('p')) { - return 16; - } - if (ke.isChar('s')) { - return 17; - } - if (ke.isChar('t')) { - return 18; + int morePopupShortcut(KeyEvent ke) { + List tabs = moreTabsSupplier.get(); + 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; + } } return -1; } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessTab.java index 836c2e06ea36d..a7fa9520550ba 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessTab.java @@ -199,7 +199,7 @@ public void render(Frame frame, Rect area) { @Override public void renderFooter(List spans) { - hint(spans, "↑↓", "scroll"); + hint(spans, TuiIcons.HINT_SCROLL, "scroll"); hint(spans, "w", "wrap [" + (wrap ? "on" : "off") + "]"); hintLast(spans, "Esc", "back"); } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RecordingManager.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RecordingManager.java index 9aea7a863a2c8..e6c2ad205ffde 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RecordingManager.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RecordingManager.java @@ -230,16 +230,16 @@ static String keyLabel(KeyEvent ke) { return ke.hasShift() ? "⇧Tab" : "Tab"; } if (ke.isKey(KeyCode.UP)) { - return "↑"; + return TuiIcons.ARROW_UP; } if (ke.isKey(KeyCode.DOWN)) { - return "↓"; + return TuiIcons.ARROW_DOWN; } if (ke.isKey(KeyCode.LEFT)) { - return "←"; + return TuiIcons.KEY_LEFT; } if (ke.isKey(KeyCode.RIGHT)) { - return "→"; + return TuiIcons.KEY_RIGHT; } if (ke.isKey(KeyCode.PAGE_UP)) { return "PgUp"; 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 a98246cdf8dc3..4e8606e844fb1 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 @@ -711,17 +711,17 @@ public void renderFooter(List spans) { if (!topologyMode && !diagram.getEipNodeBoxes().isEmpty()) { hint(spans, "Esc", "back"); hint(spans, "t", "topology"); - hint(spans, "↑↓←→", "navigate"); + hint(spans, TuiIcons.HINT_NAV, "navigate"); hint(spans, "PgUp/PgDn", "page"); hint(spans, "c", "source"); } else if (!topologyMode) { hint(spans, "Esc", "back"); hint(spans, "t", "topology"); - hint(spans, "↑↓←→", "scroll"); + hint(spans, TuiIcons.HINT_NAV, "scroll"); hint(spans, "PgUp/PgDn", "page"); } else if (!diagram.getNodeBoxes().isEmpty()) { hint(spans, "Esc", "close"); - hint(spans, "↑↓←→", "navigate"); + hint(spans, TuiIcons.HINT_NAV, "navigate"); hint(spans, "Enter", "drill-down"); hint(spans, "PgUp/PgDn", "page"); hint(spans, "c", "source"); @@ -735,7 +735,7 @@ public void renderFooter(List spans) { hint(spans, "n", "description" + (diagram.isShowDescription() ? " [on]" : " [off]")); } else { hint(spans, "Esc", "back"); - hint(spans, "↑↓", "navigate"); + hint(spans, TuiIcons.HINT_SCROLL, "navigate"); hint(spans, "Enter", "topology"); hint(spans, "d", "diagram"); hint(spans, "s", "sort"); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java index 5968b19e7121c..ec4fde3fa82d6 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java @@ -66,9 +66,16 @@ class RunOptionsForm { private static final String[] MAX_MODES = { "Max seconds:", "Max messages:", "Max idle secs:" }; private static final String[] MAX_FLAGS = { "--max-seconds=", "--max-messages=", "--max-idle-seconds=" }; - private static final String[] RUNTIME_LABELS = { "🐪 Camel Main", "🍃 Spring Boot", "🚀 Quarkus" }; + private static final String[] RUNTIME_LABELS = { + TuiIcons.labeled(TuiIcons.CAMEL, "Camel Main"), + TuiIcons.labeled(TuiIcons.SPRING_BOOT, "Spring Boot"), + TuiIcons.labeled(TuiIcons.QUARKUS, "Quarkus") + }; private static final String[] RUNTIME_VALUES = { "camel-main", "spring-boot", "quarkus" }; - private static final String[] PROFILE_LABELS = { "🛠️ dev", "📦 prod" }; + private static final String[] PROFILE_LABELS = { + TuiIcons.labeled(TuiIcons.DEV_PROFILE, "dev"), + TuiIcons.labeled(TuiIcons.PROD_PROFILE, "prod") + }; private static final String[] PROFILE_VALUES = { "dev", "prod" }; // Text fields @@ -200,13 +207,13 @@ void renderFooter(List spans) { hint(spans, "Space", "toggle"); } if (hasProperties()) { - hint(spans, "→", "properties"); + hint(spans, TuiIcons.KEY_RIGHT, "properties"); } hint(spans, "Enter", "launch"); hintLast(spans, "Esc", "back"); } else { - hint(spans, "←", "options"); - hint(spans, "↑↓", "navigate"); + hint(spans, TuiIcons.KEY_LEFT, "options"); + hint(spans, TuiIcons.HINT_SCROLL, "navigate"); hint(spans, "+", "add"); hint(spans, "Enter", "launch"); hintLast(spans, "Esc", "back"); @@ -592,7 +599,7 @@ private void renderOptionsPage(Frame frame, Rect area) { rowY++; Rect errorArea = new Rect(innerX, rowY, innerW, 1); frame.renderWidget(Paragraph.from(Line.from( - Span.styled("⚠ " + errorMessage, Style.EMPTY.bold()))), errorArea); + Span.styled(TuiIcons.HEALTH_WARN + " " + errorMessage, Style.EMPTY.bold()))), errorArea); } } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SendMessagePopup.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SendMessagePopup.java index 2137a88cf85be..8c24764080893 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SendMessagePopup.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SendMessagePopup.java @@ -712,8 +712,8 @@ private void renderRequest(Frame frame, Rect area) { FormHelper.renderLabel(frame, innerX, row, labelW, "Route:", selectedField == FIELD_ROUTE); RouteInfo ri = routes.get(selectedRouteIndex); String routeDisplay = ri.routeId + " (" + truncateUri(ri.from, fieldW - ri.routeId.length() - 6) + ")"; - String arrow = selectedField == FIELD_ROUTE ? "◀ " : " "; - String arrowR = selectedField == FIELD_ROUTE ? " ▶" : " "; + String arrow = selectedField == FIELD_ROUTE ? TuiIcons.ARROW_LEFT + " " : " "; + String arrowR = selectedField == FIELD_ROUTE ? " " + TuiIcons.ARROW_RIGHT : " "; Style routeStyle = selectedField == FIELD_ROUTE ? Style.EMPTY.bold() : Style.EMPTY; Rect routeArea = new Rect(innerX + labelW, row, fieldW, 1); frame.renderWidget(Paragraph.from(Line.from( @@ -913,7 +913,7 @@ private void renderHistory(Frame frame, Rect area) { for (int i = start; i < end; i++) { SendHistoryEntry entry = history.get(i); boolean selected = selectedField == FIELD_HISTORY && i == historyIndex; - String pointer = selected ? "► " : " "; + String pointer = selected ? TuiIcons.POINTER + " " : " "; String routeStr = String.format("%-16s", entry.routeId != null ? entry.routeId : ""); String modeStr = entry.inOut ? "InOut " : "InOnly"; String statusStr = entry.error ? "ERR" : entry.status; @@ -952,7 +952,7 @@ void renderFooter(List spans) { hint(spans, "+", "header"); hint(spans, "p", "pretty" + (prettyPrint ? " [on]" : "")); if (!history.isEmpty()) { - hintLast(spans, "↑↓", "history"); + hintLast(spans, TuiIcons.HINT_SCROLL, "history"); } else { hintLast(spans, "PgUp/Dn", "scroll"); } 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 8dc3a4d23316f..3e3cb1e35ec4e 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 @@ -307,7 +307,7 @@ void renderFooter(List spans) { } else { TuiHelper.hint(spans, "Esc/c", "close"); } - TuiHelper.hint(spans, "↑↓", "navigate"); + TuiHelper.hint(spans, TuiIcons.HINT_SCROLL, "navigate"); if (currentRouteId != null) { TuiHelper.hint(spans, "Y", "yaml"); TuiHelper.hint(spans, "J", "java"); @@ -316,7 +316,7 @@ void renderFooter(List spans) { search.renderSearchHints(spans); TuiHelper.hint(spans, "w", "wrap" + (wordWrap ? " [on]" : " [off]")); if (!wordWrap) { - TuiHelper.hint(spans, "←→", "horizontal"); + TuiHelper.hint(spans, TuiIcons.HINT_H, "horizontal"); } TuiHelper.hint(spans, "PgUp/PgDn", "page"); if (onLineSelected != null) { diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SpansTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SpansTab.java index 943452f11957f..f97069bd5dbfc 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SpansTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SpansTab.java @@ -647,7 +647,7 @@ public void renderFooter(List spans) { hint(spans, "F5", "refresh"); hint(spans, "c", camelOnly ? "camel-only [on]" : "camel-only [off]"); hint(spans, "p", showProcessors ? "processors [on]" : "processors [off]"); - hint(spans, "↑↓", "navigate"); + hint(spans, TuiIcons.HINT_SCROLL, "navigate"); hintLast(spans, "PgUp/Dn", "page"); } else if (filterInputActive) { spans.add(Span.styled(" /", Style.EMPTY.fg(Color.YELLOW).bold())); @@ -664,7 +664,7 @@ public void renderFooter(List spans) { } else { hint(spans, "/", "filter"); } - hintLast(spans, "↑↓", "navigate"); + hintLast(spans, TuiIcons.HINT_SCROLL, "navigate"); } } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlQueryTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlQueryTab.java index 428edaf08a77e..99d4b8cf920de 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlQueryTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlQueryTab.java @@ -542,7 +542,7 @@ private Cell[] buildHeaderCells() { for (int i = 0; i < columnNames.length; i++) { String label = columnNames[i]; if (columnIsPk != null && columnIsPk[i]) { - label = label + " 🔑"; + label = label + " " + TuiIcons.KEY; } cells[i] = Cell.from(Span.styled(label, Style.EMPTY.fg(Color.YELLOW))); } @@ -827,14 +827,14 @@ public void renderFooter(List spans) { hint(spans, "C-e", "history"); } if (dsNames.size() > 1) { - hint(spans, "C-←→", "datasource"); + hint(spans, TuiIcons.HINT_CTRL_H, "datasource"); } if (resultRows != null && !resultRows.isEmpty()) { hint(spans, "Tab", "results"); } } else { hint(spans, "Tab", "input"); - hint(spans, "↑↓", "navigate"); + hint(spans, TuiIcons.HINT_SCROLL, "navigate"); if (isEditable()) { hint(spans, "Enter", "edit"); } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlTraceTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlTraceTab.java index ffd152b908ef9..812c82352a921 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlTraceTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlTraceTab.java @@ -387,7 +387,7 @@ private static Style categoryStyle(String category) { @Override public void renderFooter(List spans) { hint(spans, "Esc", "back"); - hint(spans, "↑↓", "navigate"); + hint(spans, TuiIcons.HINT_SCROLL, "navigate"); hint(spans, "Home/End", "top/end"); hint(spans, "PgUp/Dn", "scroll detail"); hint(spans, "e", "edit SQL"); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTab.java index a24c300fb3111..108e49173f575 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTab.java @@ -255,7 +255,7 @@ private Style colorForDuration(long duration) { @Override public void renderFooter(List spans) { hint(spans, "Esc", "back"); - hint(spans, "↑↓", "scroll"); + hint(spans, TuiIcons.HINT_SCROLL, "scroll"); hintLast(spans, "PgUp/Dn", "page"); } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StopAllPopup.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StopAllPopup.java index 4598d2cb77e0b..6379b7b3f66f1 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StopAllPopup.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StopAllPopup.java @@ -158,7 +158,7 @@ void render(Frame frame, Rect area) { .text(Text.from(Line.from(""), intLine, infraLine)) .block(Block.builder() .borderType(BorderType.ROUNDED).borders(Borders.ALL) - .title(" 🛑 Stop All ") + .title(" " + TuiIcons.STOP + " Stop All ") .build()) .build(); frame.renderWidget(para, popup); 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 fa8538ecf3edf..84dc86fed2c13 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 @@ -94,6 +94,7 @@ interface TabCallbacks { private SqlTraceTab sqlTraceTab; private MonitorTab activeMoreTab; + private List moreTabs; TabRegistry(TabsState tabsState) { this.tabsState = tabsState; @@ -136,9 +137,32 @@ void initTabs(MonitorContext ctx, DataRefreshService dataService, Runnable reset resetIntegrationTabState); sqlTraceTab.setEditSqlAction(sql -> { - selectMoreTab(13); // switch to SQL Query tab + selectMoreTab(moreTabIndex("SQL Query")); sqlQueryTab.setInputValue("sql", sql); }); + + // Single source of truth for the More submenu: icon, programmatic name, mnemonic label and tab instance. + // Order defines the More popup index used by selectMoreTab(int). + moreTabs = List.of( + new MoreTab(TuiIcons.TAB_BEANS, "Beans", "&Beans", beansTab), + new MoreTab(TuiIcons.TAB_BROWSE, "Browse", "Bro&wse", browseTab), + new MoreTab(TuiIcons.TAB_CIRCUIT_BREAKER, "Circuit Breaker", "&Circuit Breaker", circuitBreakerTab), + new MoreTab(TuiIcons.TAB_CLASSPATH, "Classpath", "Cl&asspath", classpathTab), + new MoreTab(TuiIcons.TAB_CONFIGURATION, "Configuration", "Confi&guration", configurationTab), + new MoreTab(TuiIcons.TAB_CONSUMERS, "Consumers", "Co&nsumers", consumersTab), + new MoreTab(TuiIcons.TAB_CVE_AUDIT, "CVE Audit", "C&VE Audit", cveAuditTab), + new MoreTab(TuiIcons.TAB_DATASOURCE, "DataSource", "&DataSource", dataSourceTab), + new MoreTab(TuiIcons.TAB_HEAP, "Heap Histogram", "&Heap Histogram", heapHistogramTab), + new MoreTab(TuiIcons.TAB_INFLIGHT, "Inflight", "&Inflight", inflightTab), + new MoreTab(TuiIcons.TAB_MEMORY, "Memory", "&Memory", memoryTab), + new MoreTab(TuiIcons.TAB_MEMORY_LEAK, "Memory Leak", "Memory Lea&k", memoryLeakTab), + new MoreTab(TuiIcons.TAB_METRICS, "Metrics", "M&etrics", metricsTab), + new MoreTab(TuiIcons.TAB_SQL_QUERY, "SQL Query", "S&QL Query", sqlQueryTab), + new MoreTab(TuiIcons.TAB_SQL_TRACE, "SQL Trace", "SQL T&race", sqlTraceTab), + new MoreTab(TuiIcons.TAB_SPANS, "Spans", "&OTel Spans", spansTab), + new MoreTab(TuiIcons.TAB_PROCESS, "Process", "&Process", processTab), + new MoreTab(TuiIcons.TAB_STARTUP, "Startup", "&Startup", startupTab), + new MoreTab(TuiIcons.TAB_THREADS, "Threads", "&Threads", threadsTab)); } // ---- Tab access ---- @@ -215,28 +239,7 @@ boolean handleTabKey(int tab, MonitorContext ctx, DataRefreshService dataService void selectMoreTab(int index) { callbacks.selectMorePopupEntry(index); - activeMoreTab = switch (index) { - case 0 -> beansTab; - case 1 -> browseTab; - case 2 -> circuitBreakerTab; - case 3 -> classpathTab; - case 4 -> configurationTab; - case 5 -> consumersTab; - case 6 -> cveAuditTab; - case 7 -> dataSourceTab; - case 8 -> heapHistogramTab; - case 9 -> inflightTab; - case 10 -> memoryTab; - case 11 -> memoryLeakTab; - case 12 -> metricsTab; - case 13 -> sqlQueryTab; - case 14 -> sqlTraceTab; - case 15 -> spansTab; - case 16 -> processTab; - case 17 -> startupTab; - case 18 -> threadsTab; - default -> null; - }; + activeMoreTab = index >= 0 && index < moreTabs.size() ? moreTabs.get(index).tab() : null; if (activeMoreTab != null) { overviewTab.selectCurrentIntegration(); tabsState.select(TAB_MORE); @@ -335,40 +338,73 @@ CveAuditTab cveAuditTab() { // ---- Tab entries for Go-to and MCP ---- - record TabEntry(String name, String description, String shortcut, int tabIndex, int moreIndex) { + record TabEntry(String icon, String name, String description, String shortcut, int tabIndex, int moreIndex) { } - private static final String[] MORE_SHORTCUTS = { - "B", "W", "C", "A", "G", "N", "V", "D", "H", "I", "M", "K", "E", "Q", "R", "O", "P", "S", "T" - }; + /** + * A "More" submenu tab. Bundles its icon, programmatic {@code name} (used for tab lookup and the Go to… popup), + * popup {@code label} carrying a {@value TuiIcons#MNEMONIC_MARKER} shortcut marker, and the tab instance. The + * shortcut letter and its highlight offset are derived from {@code label} via + * {@link TuiIcons#mnemonicIndex(String)}, so there is no separate index or shortcut list to keep aligned. + */ + record MoreTab(String icon, String name, String label, MonitorTab tab) { + + MoreTab { + int i = TuiIcons.mnemonicIndex(label); + if (i < 0 || i >= TuiIcons.stripMnemonic(label).length()) { + throw new IllegalArgumentException( + "label must contain a '" + TuiIcons.MNEMONIC_MARKER + "' marker before a letter: " + label); + } + } + + String displayName() { + return TuiIcons.stripMnemonic(label); + } + + int mnemonicIndex() { + return TuiIcons.mnemonicIndex(label); + } + + char shortcut() { + return Character.toUpperCase(displayName().charAt(mnemonicIndex())); + } + } + + List moreTabs() { + return moreTabs; + } + + /** Position of the More tab with the given programmatic {@link MoreTab#name() name}, or -1 when absent. */ + int moreTabIndex(String name) { + for (int i = 0; i < moreTabs.size(); i++) { + if (moreTabs.get(i).name().equals(name)) { + return i; + } + } + return -1; + } List allTabEntries() { List entries = new ArrayList<>(); - entries.add(new TabEntry("Overview", overviewTab.description(), "1", TAB_OVERVIEW, -1)); - entries.add(new TabEntry("Log", logTab.description(), "2", TAB_LOG, -1)); - entries.add(new TabEntry("Diagram", diagramTab.description(), "3", TAB_DIAGRAM, -1)); - entries.add(new TabEntry("Routes", routesTab.description(), "4", TAB_ROUTES, -1)); - entries.add(new TabEntry("Endpoints", endpointsTab.description(), "5", TAB_ENDPOINTS, -1)); - entries.add(new TabEntry("HTTP", httpTab.description(), "6", TAB_HTTP, -1)); - entries.add(new TabEntry("Health", healthTab.description(), "7", TAB_HEALTH, -1)); - entries.add(new TabEntry("Inspect", historyTab.description(), "8", TAB_HISTORY, -1)); - entries.add(new TabEntry("Errors", errorsTab.description(), "9", TAB_ERRORS, -1)); - // More sub-tabs - MonitorTab[] moreTabs = { - beansTab, browseTab, circuitBreakerTab, classpathTab, configurationTab, - consumersTab, cveAuditTab, dataSourceTab, heapHistogramTab, inflightTab, memoryTab, - memoryLeakTab, metricsTab, sqlQueryTab, sqlTraceTab, spansTab, - processTab, startupTab, threadsTab - }; - String[] moreNames = { - "Beans", "Browse", "Circuit Breaker", "Classpath", "Configuration", - "Consumers", "CVE Audit", "DataSource", "Heap Histogram", "Inflight", "Memory", - "Memory Leak", "Metrics", "SQL Query", "SQL Trace", "Spans", - "Process", "Startup", "Threads" - }; - for (int i = 0; i < moreTabs.length; i++) { - entries.add(new TabEntry(moreNames[i], moreTabs[i].description(), MORE_SHORTCUTS[i], TAB_MORE, i)); + entries.add(new TabEntry(icon(TAB_OVERVIEW), "Overview", overviewTab.description(), "1", TAB_OVERVIEW, -1)); + entries.add(new TabEntry(icon(TAB_LOG), "Log", logTab.description(), "2", TAB_LOG, -1)); + entries.add(new TabEntry(icon(TAB_DIAGRAM), "Diagram", diagramTab.description(), "3", TAB_DIAGRAM, -1)); + entries.add(new TabEntry(icon(TAB_ROUTES), "Routes", routesTab.description(), "4", TAB_ROUTES, -1)); + entries.add(new TabEntry(icon(TAB_ENDPOINTS), "Endpoints", endpointsTab.description(), "5", TAB_ENDPOINTS, -1)); + entries.add(new TabEntry(icon(TAB_HTTP), "HTTP", httpTab.description(), "6", TAB_HTTP, -1)); + entries.add(new TabEntry(icon(TAB_HEALTH), "Health", healthTab.description(), "7", TAB_HEALTH, -1)); + entries.add(new TabEntry(icon(TAB_HISTORY), "Inspect", historyTab.description(), "8", TAB_HISTORY, -1)); + entries.add(new TabEntry(icon(TAB_ERRORS), "Errors", errorsTab.description(), "9", TAB_ERRORS, -1)); + for (int i = 0; i < moreTabs.size(); i++) { + MoreTab mt = moreTabs.get(i); + entries.add(new TabEntry( + mt.icon(), mt.name(), mt.tab().description(), String.valueOf(mt.shortcut()), + TAB_MORE, i)); } return entries; } + + private static String icon(int primaryTabIndex) { + return TuiIcons.PRIMARY_TAB_ICONS.get(primaryTabIndex); + } } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TapeRecorder.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TapeRecorder.java index 9fe3b138075a5..15da52713f771 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TapeRecorder.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TapeRecorder.java @@ -162,10 +162,10 @@ private void flushCharBatch(StringBuilder batch) { static String toTapeCommand(String key) { if (key.length() == 1) { return switch (key) { - case "↑" -> "Up"; - case "↓" -> "Down"; - case "←" -> "Left"; - case "→" -> "Right"; + case TuiIcons.ARROW_UP -> "Up"; + case TuiIcons.ARROW_DOWN -> "Down"; + case TuiIcons.KEY_LEFT -> "Left"; + case TuiIcons.KEY_RIGHT -> "Right"; case "⌫" -> "Backspace"; default -> null; }; diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ThreadsTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ThreadsTab.java index 9cbe8e0613330..8d97388763fa9 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ThreadsTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ThreadsTab.java @@ -269,7 +269,7 @@ public void renderFooter(List spans) { hint(spans, "Esc", "back"); hint(spans, "s", "sort"); hint(spans, "f", "filter [" + FILTER_LABELS[filter] + "]"); - hint(spans, "↑↓", "navigate"); + hint(spans, TuiIcons.HINT_SCROLL, "navigate"); hintLast(spans, "PgUp/Dn", "scroll"); } 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 508c39429760c..b8cd041090d9b 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 @@ -640,56 +640,56 @@ static String fileEmoji(Path path) { return detectPomEmoji(path); } if (lower.endsWith(".kamelet.yaml") || lower.endsWith(".kamelet.yml")) { - return "🐪"; + return TuiIcons.CAMEL; } if (lower.endsWith(".yaml") || lower.endsWith(".yml")) { - return isCamelYaml(path) ? "🐪" : "📋"; + return isCamelYaml(path) ? TuiIcons.CAMEL : TuiIcons.CLIPBOARD; } if (lower.endsWith(".xml")) { - return isCamelXml(path) ? "🐪" : "📋"; + return isCamelXml(path) ? TuiIcons.CAMEL : TuiIcons.CLIPBOARD; } if (lower.endsWith(".java")) { - return isCamelJava(path) ? "🐪" : "☕"; + return isCamelJava(path) ? TuiIcons.CAMEL : TuiIcons.JAVA; } if (lower.endsWith(".properties") || lower.endsWith(".cfg")) { - return "📄"; + return TuiIcons.DOCUMENT; } if (lower.endsWith(".json")) { - return "📋"; + return TuiIcons.CLIPBOARD; } if (lower.endsWith(".md") || lower.endsWith(".adoc") || lower.endsWith(".txt") || lower.startsWith("readme")) { - return "📖"; + return TuiIcons.README; } - return "📄"; + return TuiIcons.DOCUMENT; } static String fileEmojiByName(String name) { String lower = name.toLowerCase(Locale.ROOT); if (lower.endsWith(".camel.yaml") || lower.endsWith(".camel.yml") || lower.endsWith(".kamelet.yaml") || lower.endsWith(".kamelet.yml")) { - return "🐪"; + return TuiIcons.CAMEL; } if (lower.endsWith(".java")) { - return "☕"; + return TuiIcons.JAVA; } if (lower.endsWith(".yaml") || lower.endsWith(".yml")) { - return "📋"; + return TuiIcons.CLIPBOARD; } if (lower.endsWith(".xml")) { - return "📋"; + return TuiIcons.CLIPBOARD; } if (lower.endsWith(".properties") || lower.endsWith(".cfg")) { - return "📄"; + return TuiIcons.DOCUMENT; } if (lower.endsWith(".json")) { - return "📋"; + return TuiIcons.CLIPBOARD; } if (lower.endsWith(".md") || lower.endsWith(".adoc") || lower.endsWith(".txt") || lower.startsWith("readme")) { - return "📖"; + return TuiIcons.README; } - return "📄"; + return TuiIcons.DOCUMENT; } static String detectPomRuntime(Path pomFile) { @@ -718,20 +718,20 @@ private static String detectPomEmoji(Path path) { String content = Files.readString(path, StandardCharsets.UTF_8); if (content.contains("quarkus-maven-plugin") || content.contains("quarkus-bom") || content.contains("camel-quarkus")) { - return "🚀"; + return TuiIcons.QUARKUS; } if (content.contains("spring-boot-maven-plugin") || content.contains("spring-boot-starter") || content.contains("camel-spring-boot")) { - return "🍃"; + return TuiIcons.SPRING_BOOT; } if (content.contains("camel-core") || content.contains("camel-api") || content.contains("org.apache.camel")) { - return "🐪"; + return TuiIcons.CAMEL; } } catch (IOException e) { // ignore } - return "📋"; + return TuiIcons.CLIPBOARD; } private static boolean isCamelYaml(Path path) { 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 new file mode 100644 index 0000000000000..0b18bdc4dfe71 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java @@ -0,0 +1,221 @@ +/* + * 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.List; + +/** + * Single source of truth for emoji and symbolic icons used across the Camel TUI. + *

+ * Tab/menu icons use plain 2-column-wide emoji without VS16 variation selectors (see CAMEL-23818). Doctor and legacy + * status glyphs may still use mixed-width symbols until migrated. + */ +final class TuiIcons { + + // ---- Brand & runtime ---- + static final String CAMEL = "🐪"; + static final String SPRING_BOOT = "🍃"; + static final String QUARKUS = "🚀"; + static final String DEV_PROFILE = "🛠"; + static final String PROD_PROFILE = "📦"; + static final String INFRA = "🔧"; + + // ---- Status & health ---- + static final String OK = "✅"; + static final String WARN = "⚠"; + static final String FAIL = "❌"; + static final String HEALTH_UP = "✔"; + static final String HEALTH_DOWN = "✖"; + static final String HEALTH_WARN = "⚠"; + static final String STOPPED = "✖"; + static final String CROSS = "✗"; + + // ---- Files & folders ---- + static final String FOLDER = "📁"; + static final String FOLDER_OPEN = "📂"; + static final String CLIPBOARD = "📋"; + static final String DOCUMENT = "📄"; + static final String README = "📖"; + + // ---- Actions menu ---- + static final String GO_TO = "🔍"; + static final String MESSAGE = "📩"; + static final String KEYSTROKES = "🔤"; + static final String STOP = "🛑"; + static final String RECORD = "🔴"; + static final String DOCTOR = "🩺"; + static final String RESET = "🔄"; + static final String CLEAN = "🧹"; + static final String LIGHT_THEME = "🌞"; + static final String DARK_THEME = "🌙"; + static final String SCREENSHOT = "📸"; + static final String CAPTION = "💬"; + static final String MCP_BRAIN = "🧠"; + static final String MCP = "🤖"; + static final String MCP_LOG = "📋"; + + // ---- Example browser legend ---- + static final String BUNDLED = "📦"; + static final String ONLINE = "🌐"; + static final String DOCKER = "🐳"; + static final String CITRUS = "🍋"; + + // ---- Doctor checks ---- + static final String JAVA = "☕"; + static final String ENDPOINT = "🔌"; + static final String OTEL = "🔗"; + static final String MEMORY = "💾"; + + // ---- Misc UI ---- + static final String KEY = "🔑"; + static final String TIP = "💡"; + static final String COMPUTER = "💻"; + static final String HELP = "❔"; + static final String THINKING = "🤔"; + + // ---- UI symbols (non-emoji glyphs used across the TUI) ---- + static final String SELECTED = "●"; + static final String IDLE = "○"; + static final String MORE_CHEVRON = "▾"; + static final String ARROW_LEFT = "◀"; + static final String ARROW_RIGHT = "▶"; + static final String POINTER = "►"; + static final String KEY_LEFT = "←"; + static final String KEY_RIGHT = "→"; + static final String ARROW_UP = "↑"; + static final String ARROW_DOWN = "↓"; + static final String ARROW_STABLE = "→"; + static final String SORT_UP = "▲"; + static final String SORT_DOWN = "▼"; + + static final String ARROW_BOTH = "↔"; + static final String HINT_SCROLL = "↑↓"; + static final String HINT_NAV = "↑↓←→"; + static final String HINT_H = "←→"; + static final String HINT_CTRL_H = "C-←→"; + + // ---- Primary tab bar ---- + static final String TAB_OVERVIEW = CAMEL; + static final String TAB_LOG = DOCUMENT; + static final String TAB_DIAGRAM = "🗺"; + static final String TAB_ROUTES = "🛤"; + static final String TAB_ENDPOINTS = ENDPOINT; + static final String TAB_HTTP = ONLINE; + static final String TAB_HEALTH = DOCTOR; + static final String TAB_INSPECT = GO_TO; + static final String TAB_ERRORS = FAIL; + static final String TAB_MORE = FOLDER_OPEN; + + // ---- More submenu tabs ---- + static final String TAB_BEANS = JAVA; + static final String TAB_BROWSE = MESSAGE; + static final String TAB_CIRCUIT_BREAKER = "⚡"; + static final String TAB_CLASSPATH = BUNDLED; + static final String TAB_CONFIGURATION = DOCUMENT; + static final String TAB_CONSUMERS = "📥"; + static final String TAB_CVE_AUDIT = "🛡"; + static final String TAB_DATASOURCE = "🗄"; + static final String TAB_HEAP = MEMORY; + static final String TAB_INFLIGHT = RESET; + static final String TAB_MEMORY = MEMORY; + static final String TAB_MEMORY_LEAK = "💧"; + static final String TAB_METRICS = "📈"; + static final String TAB_SQL_QUERY = KEY; + static final String TAB_SQL_TRACE = "🔎"; + static final String TAB_SPANS = OTEL; + static final String TAB_PROCESS = CLIPBOARD; + static final String TAB_STARTUP = QUARKUS; + static final String TAB_THREADS = "🧵"; + + /** Icons for {@link TabRegistry#TAB_OVERVIEW}..{@link TabRegistry#TAB_MORE} (in order). */ + static final List PRIMARY_TAB_ICONS = List.of( + TAB_OVERVIEW, TAB_LOG, TAB_DIAGRAM, TAB_ROUTES, TAB_ENDPOINTS, + TAB_HTTP, TAB_HEALTH, TAB_INSPECT, TAB_ERRORS, TAB_MORE); + + /** Marker placed immediately before a label's keyboard-shortcut letter (Windows-style mnemonic). */ + static final char MNEMONIC_MARKER = '&'; + + private TuiIcons() { + } + + static String labeled(String icon, String text) { + return icon + " " + text; + } + + /** Prefix for popup list rows: {@code " 🐪 "}. */ + static String indent(String icon) { + return " " + icon + " "; + } + + /** Prefix for actions menu rows: {@code " 🔍 Go to..."}. */ + static String menuItem(String icon, String label) { + return " " + icon + " " + label; + } + + /** + * Primary tab bar label of the form {@code " "} with two spaces between the emoji and the key + * digit so the two do not visually collide. Rendered without outer padding (compact) for every tab. + */ + static String primaryTabHeader(String icon, String key, String name) { + return icon + " " + key + " " + name; + } + + /** Removes the {@value #MNEMONIC_MARKER} mnemonic marker from a label for display. */ + static String stripMnemonic(String label) { + int i = label.indexOf(MNEMONIC_MARKER); + return i < 0 ? label : label.substring(0, i) + label.substring(i + 1); + } + + /** + * Position of the shortcut letter within the {@link #stripMnemonic(String) stripped} label, which equals the index + * of the {@value #MNEMONIC_MARKER} marker in {@code label}, or {@code -1} when the label has no marker. + */ + static int mnemonicIndex(String label) { + return label.indexOf(MNEMONIC_MARKER); + } + + static String runtimeIcon(String runtime) { + return switch (runtime) { + case "Spring Boot" -> SPRING_BOOT; + case "Quarkus" -> QUARKUS; + default -> CAMEL; + }; + } + + static String platformIcon(String platform) { + return switch (platform) { + case "Spring Boot" -> SPRING_BOOT + " "; + case "Quarkus" -> QUARKUS + " "; + case "JBang", "Camel" -> CAMEL + " "; + default -> ""; + }; + } + + static String profilePrefix(String profile) { + if ("dev".equals(profile)) { + return DEV_PROFILE + " "; + } + if ("prod".equals(profile)) { + return PROD_PROFILE + " "; + } + return ""; + } + + static String moreTabLabel() { + return "More" + MORE_CHEVRON; + } +} diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitorTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitorTest.java index 515add2e67bea..6573b8d00bd8b 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitorTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitorTest.java @@ -179,7 +179,7 @@ void footerKeyEventParsesNamedAndSingleCharKeys() { void footerKeyEventRejectsAmbiguousAndInvalidTokens() { assertNull(CamelMonitor.footerKeyEvent("Up/Down"), "a two-key hint is not clickable"); assertNull(CamelMonitor.footerKeyEvent("PgUp/PgDn"), "a paging hint is not clickable"); - assertNull(CamelMonitor.footerKeyEvent("↑↓"), "arrow glyphs are not a single key"); + assertNull(CamelMonitor.footerKeyEvent(TuiIcons.HINT_SCROLL), "arrow glyphs are not a single key"); assertNull(CamelMonitor.footerKeyEvent("F13"), "there is no F13 key"); assertNull(CamelMonitor.footerKeyEvent(""), "an empty token is not clickable"); assertNull(CamelMonitor.footerKeyEvent(null), "a null token is not clickable"); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTabRenderTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTabRenderTest.java index f1921e7f59d52..1542df35f61aa 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTabRenderTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTabRenderTest.java @@ -107,7 +107,8 @@ void renderInDirectionColor() { Frame frame = Frame.forTesting(buffer); tab.render(frame, area); - boolean foundBrightGreen = TuiTestHelper.findCellWithColor(buffer, "→", Color.ansi(AnsiColor.BRIGHT_GREEN)); + boolean foundBrightGreen + = TuiTestHelper.findCellWithColor(buffer, TuiIcons.KEY_RIGHT, Color.ansi(AnsiColor.BRIGHT_GREEN)); assertTrue(foundBrightGreen, "In-direction arrow should be rendered in BRIGHT_GREEN"); } @@ -122,7 +123,7 @@ void renderOutDirectionInCyan() { Frame frame = Frame.forTesting(buffer); tab.render(frame, area); - boolean foundCyanArrow = TuiTestHelper.findCellWithColor(buffer, "←", Color.CYAN); + boolean foundCyanArrow = TuiTestHelper.findCellWithColor(buffer, TuiIcons.KEY_LEFT, Color.CYAN); assertTrue(foundCyanArrow, "Out-direction arrow should be rendered in CYAN"); } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthTabRenderTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthTabRenderTest.java index 00c007fce4f93..6b0a199a367a2 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthTabRenderTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthTabRenderTest.java @@ -111,7 +111,7 @@ void renderDownStatusCellUsesRedColor() { for (int y = 0; y < buffer.height(); y++) { for (int x = 0; x < buffer.width(); x++) { var cell = buffer.get(x, y); - if ("✖".equals(cell.symbol()) || "D".equals(cell.symbol())) { + if (TuiIcons.HEALTH_DOWN.equals(cell.symbol()) || "D".equals(cell.symbol())) { var fg = cell.style().fg().orElse(null); if (Color.LIGHT_RED.equals(fg)) { foundRedDown = true; @@ -148,7 +148,7 @@ void renderUpStatusCellUsesGreenColor() { for (int y = 0; y < buffer.height(); y++) { for (int x = 0; x < buffer.width(); x++) { var cell = buffer.get(x, y); - if ("✔".equals(cell.symbol())) { + if (TuiIcons.HEALTH_UP.equals(cell.symbol())) { var fg = cell.style().fg().orElse(null); if (Color.GREEN.equals(fg)) { foundGreenUp = true; diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContextRenderTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContextRenderTest.java index 1cd58e6297610..f9ee0f55eece6 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContextRenderTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContextRenderTest.java @@ -158,10 +158,10 @@ void centerCellCentersText() { @Test void sortLabelShowsIndicatorForActiveColumn() { String label = AbstractTab.sortLabel("NAME", "name", "name", false); - assertEquals("NAME▼", label, "Active sort column should have descending indicator"); + assertEquals("NAME" + TuiIcons.SORT_DOWN, label, "Active sort column should have descending indicator"); String reversed = AbstractTab.sortLabel("NAME", "name", "name", true); - assertEquals("NAME▲", reversed, "Reversed sort should have ascending indicator"); + assertEquals("NAME" + TuiIcons.SORT_UP, reversed, "Reversed sort should have ascending indicator"); } @Test diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManagerTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManagerTest.java index 7eb97cdde2b2a..a2ad9984448ab 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManagerTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManagerTest.java @@ -25,6 +25,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -69,7 +70,12 @@ public void stopSelectedProcess(boolean forceKill) { } }; - popupManager = new PopupManager(ctx, () -> List.of(info), new FilesBrowser(), callbacks); + popupManager = new PopupManager( + ctx, () -> List.of(info), + () -> List.of( + new TabRegistry.MoreTab(TuiIcons.TAB_BEANS, "Beans", "&Beans", null), + new TabRegistry.MoreTab(TuiIcons.TAB_BROWSE, "Browse", "Bro&wse", null)), + new FilesBrowser(), callbacks); } @Test @@ -119,10 +125,11 @@ void openSwitchPopupMakesVisible() { } @Test - void morePopupShortcutReturnsCorrectIndex() { - // 'a' should return 0 (first more tab = beans) - int index = PopupManager.morePopupShortcut(KeyEvent.ofChar('a', KeyModifiers.NONE)); - assertTrue(index >= 0 || index == -1, - "Shortcut should return a valid index or -1"); + void morePopupShortcutMatchesEitherCase() { + // 'w'/'W' both select Browse (index 1); Shift+letter must work too + assertEquals(1, popupManager.morePopupShortcut(KeyEvent.ofChar('w', KeyModifiers.NONE))); + assertEquals(1, popupManager.morePopupShortcut(KeyEvent.ofChar('W', KeyModifiers.NONE))); + assertEquals(0, popupManager.morePopupShortcut(KeyEvent.ofChar('B', KeyModifiers.NONE))); + assertEquals(-1, popupManager.morePopupShortcut(KeyEvent.ofChar('z', KeyModifiers.NONE))); } } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RecordingManagerTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RecordingManagerTest.java index 94f50c571c492..89ebcbe1276eb 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RecordingManagerTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RecordingManagerTest.java @@ -126,10 +126,10 @@ void keyLabelForTab() { @Test void keyLabelForArrowKeys() { - assertEquals("↑", RecordingManager.keyLabel(KeyEvent.ofKey(KeyCode.UP, KeyModifiers.NONE))); - assertEquals("↓", RecordingManager.keyLabel(KeyEvent.ofKey(KeyCode.DOWN, KeyModifiers.NONE))); - assertEquals("←", RecordingManager.keyLabel(KeyEvent.ofKey(KeyCode.LEFT, KeyModifiers.NONE))); - assertEquals("→", RecordingManager.keyLabel(KeyEvent.ofKey(KeyCode.RIGHT, KeyModifiers.NONE))); + assertEquals(TuiIcons.ARROW_UP, RecordingManager.keyLabel(KeyEvent.ofKey(KeyCode.UP, KeyModifiers.NONE))); + assertEquals(TuiIcons.ARROW_DOWN, RecordingManager.keyLabel(KeyEvent.ofKey(KeyCode.DOWN, KeyModifiers.NONE))); + assertEquals(TuiIcons.KEY_LEFT, RecordingManager.keyLabel(KeyEvent.ofKey(KeyCode.LEFT, KeyModifiers.NONE))); + assertEquals(TuiIcons.KEY_RIGHT, RecordingManager.keyLabel(KeyEvent.ofKey(KeyCode.RIGHT, KeyModifiers.NONE))); } @Test 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 new file mode 100644 index 0000000000000..7fa84d60afac5 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabBarRenderTest.java @@ -0,0 +1,91 @@ +/* + * 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 dev.tamboui.text.CharWidth; +import dev.tamboui.text.Line; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Smoke tests for the emoji-decorated primary tab bar (CAMEL-23720). Labels render compact with two spaces between the + * emoji and the key digit; the Route/Top toggle keeps a constant width so the bar does not shift. + */ +class TabBarRenderTest { + + private static final int TABS_FULL_MIN_WIDTH = 157; + + @Test + void primaryTabHeadersStartWithIconAndTwoSpaces() { + String[] names = { "Overview", "Log", "Diagram", "Route", "Endpoint", "HTTP", "Health", "Inspect", "Errors" }; + for (int i = 0; i < names.length; i++) { + String header = TuiIcons.primaryTabHeader(TuiIcons.PRIMARY_TAB_ICONS.get(i), String.valueOf(i + 1), names[i]); + assertTrue(header.startsWith(TuiIcons.PRIMARY_TAB_ICONS.get(i) + " " + (i + 1)), + "Tab header should be ' ': " + header); + } + } + + @Test + void moreTabHeaderIncludesIconAndChevron() { + String header = TuiIcons.primaryTabHeader(TuiIcons.TAB_MORE, "0", TuiIcons.moreTabLabel()); + assertTrue(header.startsWith(TuiIcons.TAB_MORE)); + assertTrue(header.contains(TuiIcons.MORE_CHEVRON)); + } + + @Test + void routeAndTopLabelsHaveEqualWidth() { + // toggling Top mode must not shift the tab bar + int route = CharWidth.of(TuiIcons.primaryTabHeader(TuiIcons.TAB_ROUTES, "4", "Route")); + int top = CharWidth.of(TuiIcons.primaryTabHeader(TuiIcons.TAB_ROUTES, "4", " Top ")); + assertEquals(route, top, "Route and Top tab cells must be the same width"); + } + + @Test + void fullTabBarFitsAtTabsFullMinWidth() { + Line[] labels = fullTabLabels("Route"); + int dividerW = CharWidth.of(" | "); + int total = 0; + for (int i = 0; i < labels.length; i++) { + total += labels[i].width(); + if (i < labels.length - 1) { + total += dividerW; + } + } + assertTrue(total <= TABS_FULL_MIN_WIDTH, + "Full tab bar width " + total + " exceeds TABS_FULL_MIN_WIDTH " + TABS_FULL_MIN_WIDTH); + // the emoji icons widened the bar past the old 126-column budget; guard against a silent revert + assertTrue(total > 126, + "Full tab bar width " + total + " should exceed the old 126 threshold once emoji icons are added"); + } + + 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_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 8d5c8be9c12db..8e5a0757db695 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 @@ -16,16 +16,182 @@ */ 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; +import dev.tamboui.widgets.tabs.TabsState; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; /** - * Tests for {@link TabRegistry} constants and tab index mapping. + * Tests for {@link TabRegistry} constants, tab index mapping, and the {@link TabRegistry.MoreTab} records that are the + * single source of truth for the More submenu (icon, name, mnemonic label, tab instance). */ class TabRegistryTest { + private TabRegistry registry; + + @BeforeEach + void setUp() { + AtomicReference> data = new AtomicReference<>(List.of()); + AtomicReference> infraData = new AtomicReference<>(List.of()); + MonitorContext ctx = new MonitorContext(data, infraData); + DataRefreshService dataService = new DataRefreshService( + "test", + new DataRefreshService.RefreshContext() { + @Override + public int selectedTab() { + return 0; + } + + @Override + public boolean isSwitchPopupVisible() { + return false; + } + + @Override + public String getPendingAutoSelect() { + return null; + } + + @Override + public void clearPendingAutoSelect() { + } + + @Override + public void onInfraAutoSelected(int tableIndex, String pid) { + } + + @Override + public boolean isInfraSelected() { + return false; + } + }, + Path::of, Path::of); + registry = new TabRegistry(new TabsState(TabRegistry.TAB_OVERVIEW)); + registry.initTabs(ctx, dataService, () -> { + }); + } + + @Test + void moreTabsHasNineteenEntries() { + assertEquals(19, registry.moreTabs().size()); + } + + @Test + void everyMoreTabLabelHasMnemonicMarker() { + for (TabRegistry.MoreTab mt : registry.moreTabs()) { + assertTrue(mt.mnemonicIndex() >= 0, + "More tab '" + mt.name() + "' label must carry a '" + TuiIcons.MNEMONIC_MARKER + "' marker: " + + mt.label()); + } + } + + @Test + void moreTabIconsAreTwoColumnsWideWithoutVariationSelector() { + for (TabRegistry.MoreTab mt : registry.moreTabs()) { + assertEquals(2, CharWidth.of(mt.icon()), "Icon should be 2 terminal columns wide: " + mt.icon()); + assertFalse(mt.icon().contains("\uFE0F"), "Icon should not contain VS16 variation selector: " + mt.icon()); + } + } + + @Test + void moreTabShortcutsMatchHistoricalSequence() { + // Independent oracle (not a re-derivation of shortcut()): these are the exact letters the hand-maintained + // MORE_SHORTCUTS array carried before the MoreTab refactor. A label edit that repoints a key must fail here. + List shortcuts = registry.moreTabs().stream().map(TabRegistry.MoreTab::shortcut).toList(); + assertEquals( + List.of('B', 'W', 'C', 'A', 'G', 'N', 'V', 'D', 'H', 'I', 'M', 'K', 'E', 'Q', 'R', 'O', 'P', 'S', 'T'), + 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. + List shortcuts = registry.moreTabs().stream().map(TabRegistry.MoreTab::shortcut).toList(); + assertEquals(shortcuts.size(), Set.copyOf(shortcuts).size(), + "More tab shortcut letters must be unique: " + shortcuts); + } + + @Test + void moreTabRejectsLabelWithoutUsableMnemonicMarker() { + // The record's compact constructor enforces the '&'-before-a-letter invariant that shortcut() relies on, + // so a mistyped literal fails loudly at construction instead of throwing StringIndexOutOfBounds later. + assertThrows(IllegalArgumentException.class, + () -> new TabRegistry.MoreTab(TuiIcons.TAB_BEANS, "Beans", "Beans", null)); + assertThrows(IllegalArgumentException.class, + () -> new TabRegistry.MoreTab(TuiIcons.TAB_BEANS, "Beans", "Beans&", null)); + } + + @Test + void moreTabIndexResolvesSqlQueryByNameForEditSqlJump() { + // Guards the selectMoreTab(moreTabIndex("SQL Query")) wiring that replaced a hardcoded index. + int idx = registry.moreTabIndex("SQL Query"); + assertEquals("SQL Query", registry.moreTabs().get(idx).name()); + assertEquals(-1, registry.moreTabIndex("Nonexistent"), "Unknown name resolves to -1"); + } + + @Test + void allTabEntriesExposeDigitsIconsAndMoreShortcuts() { + List entries = registry.allTabEntries(); + assertEquals(9 + registry.moreTabs().size(), entries.size()); + + // Primary tabs: digit shortcuts 1-9, moreIndex -1, icon indexed by tabIndex. + for (int i = 0; i < 9; i++) { + TabRegistry.TabEntry e = entries.get(i); + assertEquals(String.valueOf(i + 1), e.shortcut(), "Primary tab " + e.name() + " digit shortcut"); + assertEquals(-1, e.moreIndex(), "Primary tab " + e.name() + " has no More index"); + assertEquals(TuiIcons.PRIMARY_TAB_ICONS.get(e.tabIndex()), e.icon()); + } + + // More tabs: tabIndex TAB_MORE, ascending moreIndex, shortcut/name/icon carried from the owning MoreTab. + for (int i = 0; i < registry.moreTabs().size(); i++) { + TabRegistry.TabEntry e = entries.get(9 + i); + TabRegistry.MoreTab mt = registry.moreTabs().get(i); + assertEquals(TabRegistry.TAB_MORE, e.tabIndex()); + assertEquals(i, e.moreIndex()); + assertEquals(String.valueOf(mt.shortcut()), e.shortcut()); + assertEquals(mt.name(), e.name()); + assertEquals(mt.icon(), e.icon()); + } + } + + @Test + void browseAndConfigurationHighlightTheirShortcutLetter() { + // Regression guard: these two historically underlined the wrong letter when a hand-maintained index array + // drifted. The '&' marker now pins the highlight to the actual shortcut. + TabRegistry.MoreTab browse = moreTabNamed("Browse"); + assertEquals(3, browse.mnemonicIndex(), "Should underline the 'w' in Bro[w]se"); + assertEquals('W', browse.shortcut()); + + TabRegistry.MoreTab configuration = moreTabNamed("Configuration"); + assertEquals(5, configuration.mnemonicIndex(), "Should underline the 'g' in Confi[g]uration"); + assertEquals('G', configuration.shortcut()); + } + + @Test + void spansTabKeepsProgrammaticNameButOTelPopupLabel() { + TabRegistry.MoreTab spans = moreTabNamed("Spans"); + assertEquals("Spans", spans.name(), "MCP/Go-to lookup name must stay 'Spans'"); + assertEquals("OTel Spans", spans.displayName(), "Popup shows the OTel-qualified label"); + assertEquals('O', spans.shortcut()); + } + + private TabRegistry.MoreTab moreTabNamed(String name) { + return registry.moreTabs().stream() + .filter(mt -> mt.name().equals(name)) + .findFirst() + .orElseThrow(() -> new AssertionError("No More tab named " + name)); + } + @Test void tabConstantsAreSequential() { assertEquals(0, TabRegistry.TAB_OVERVIEW, "TAB_OVERVIEW should be 0"); 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 new file mode 100644 index 0000000000000..03bf2e1959fbf --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIconsTest.java @@ -0,0 +1,91 @@ +/* + * 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 dev.tamboui.text.CharWidth; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Validates {@link TuiIcons} primary tab emoji width (CAMEL-23818: avoid VS16 mismeasurement in TamboUI) and the + * mnemonic and runtime/platform icon helpers. More-submenu icons and labels are validated in {@link TabRegistryTest} + * where the {@link TabRegistry.MoreTab} records that own them are constructed. + */ +class TuiIconsTest { + + @Test + void primaryTabIconCountMatchesTabRegistry() { + assertEquals(TabRegistry.NUM_TABS, TuiIcons.PRIMARY_TAB_ICONS.size()); + } + + @Test + void primaryTabIconsAreOrderedByTabIndex() { + // 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_LOG, TuiIcons.PRIMARY_TAB_ICONS.get(TabRegistry.TAB_LOG)); + 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_HEALTH, TuiIcons.PRIMARY_TAB_ICONS.get(TabRegistry.TAB_HEALTH)); + 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)); + } + + @Test + void primaryTabEmojisAreTwoColumnsWide() { + for (String icon : TuiIcons.PRIMARY_TAB_ICONS) { + assertEquals(2, CharWidth.of(icon), "Icon should be 2 terminal columns wide: " + icon); + } + } + + @Test + void primaryTabEmojisHaveNoVariationSelector() { + for (String icon : TuiIcons.PRIMARY_TAB_ICONS) { + assertFalse(icon.contains("\uFE0F"), "Icon should not contain VS16 variation selector: " + icon); + } + } + + @Test + void stripMnemonicRemovesMarkerAndIndexPointsAtShortcutLetter() { + assertEquals("Browse", TuiIcons.stripMnemonic("Bro&wse")); + assertEquals(3, TuiIcons.mnemonicIndex("Bro&wse")); + assertEquals('w', TuiIcons.stripMnemonic("Bro&wse").charAt(TuiIcons.mnemonicIndex("Bro&wse"))); + assertEquals("Plain", TuiIcons.stripMnemonic("Plain")); + assertEquals(-1, TuiIcons.mnemonicIndex("Plain")); + } + + @Test + void runtimePlatformAndProfileIconsCoverKnownAndDefaultBranches() { + assertEquals(TuiIcons.SPRING_BOOT, TuiIcons.runtimeIcon("Spring Boot")); + assertEquals(TuiIcons.QUARKUS, TuiIcons.runtimeIcon("Quarkus")); + assertEquals(TuiIcons.CAMEL, TuiIcons.runtimeIcon("Something else")); + + assertEquals(TuiIcons.QUARKUS + " ", TuiIcons.platformIcon("Quarkus")); + assertEquals(TuiIcons.CAMEL + " ", TuiIcons.platformIcon("JBang")); + assertTrue(TuiIcons.platformIcon("unknown").isEmpty()); + + assertEquals(TuiIcons.DEV_PROFILE + " ", TuiIcons.profilePrefix("dev")); + assertEquals(TuiIcons.PROD_PROFILE + " ", TuiIcons.profilePrefix("prod")); + assertTrue(TuiIcons.profilePrefix("staging").isEmpty()); + } +}