This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch panel-focus-border in repository https://gitbox.apache.org/repos/asf/camel.git
commit f1f3d43a059cf01cbc8bfe584181f7d07bd546e1 Author: Claus Ibsen <[email protected]> AuthorDate: Mon Nov 9 10:53:03 2026 +0100 chore: camel-jbang - the panel the cursor is in has the focus border on About, Timeline, Routes, Diagram, Errors, Circuit Breaker, JFR and Heap Histogram Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../dsl/jbang/core/commands/tui/AboutView.java | 40 +++++++++++++++------- .../core/commands/tui/AbstractDiagramTab.java | 1 + .../jbang/core/commands/tui/CircuitBreakerTab.java | 14 +++++--- .../core/commands/tui/DiagramDetailSupport.java | 7 ++-- .../jbang/core/commands/tui/DiagramSupport.java | 25 +++++++------- .../dsl/jbang/core/commands/tui/DiagramTab.java | 1 + .../dsl/jbang/core/commands/tui/ErrorsTab.java | 11 +++--- .../jbang/core/commands/tui/HeapHistogramTab.java | 8 +++-- .../camel/dsl/jbang/core/commands/tui/JfrTab.java | 15 ++++---- .../dsl/jbang/core/commands/tui/RoutesTab.java | 12 +++---- .../dsl/jbang/core/commands/tui/TimelineTab.java | 15 ++++---- .../dsl/jbang/core/commands/tui/AboutViewTest.java | 12 +++++++ .../core/commands/tui/TimelineTabRenderTest.java | 16 +++++++++ .../dsl/jbang/core/commands/tui/TuiTestHelper.java | 25 ++++++++++++++ 14 files changed, 146 insertions(+), 56 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java index e1e6efb42d37..4aba7b02e35f 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java @@ -96,6 +96,8 @@ final class AboutView { private int stripColumns = STRIP_COLUMNS; // the line of In, Routes or Out the cursor is on (↑ ↓), in the order of the columns; -1 when none is private int cursor = -1; + // the column the cursor is in: 0 In, 1 Routes, 2 Out, or -1 when it is on nothing; its panel has the focus border + private int focus = -1; private volatile List<Target> targets = List.of(); private volatile List<String> targetLabels = List.of(); @@ -467,12 +469,13 @@ final class AboutView { .constraints(Constraint.length(headHeight), Constraint.length(columnHeight), Constraint.length(statusHeight), Constraint.length(happenedHeight), Constraint.fill()) .split(area); - frame.renderWidget(Paragraph.builder().text(Text.from(head)).block(block(name, Padding.NONE)).build(), rows.get(0)); - renderColumns(frame, rows.get(1), columns, Padding.NONE, 0); + frame.renderWidget(Paragraph.builder().text(Text.from(head)).block(block(name, Padding.NONE, false)).build(), + rows.get(0)); + renderColumns(frame, rows.get(1), columns, Padding.NONE, 0, focus); frame.renderWidget(Paragraph.builder().text(Text.from(status)).build(), rows.get(2)); if (happenedHeight > 2) { frame.renderWidget(Paragraph.builder().text(Text.from(strips)) - .block(block(happenedTitle(info), Padding.NONE)).build(), rows.get(3)); + .block(block(happenedTitle(info), Padding.NONE, false)).build(), rows.get(3)); } } @@ -489,13 +492,13 @@ final class AboutView { Constraint.length(1), Constraint.length(status.size()), Constraint.length(1), Constraint.length(happenedHeight), Constraint.length(1), Constraint.length(1), Constraint.fill()) .split(area); - frame.renderWidget(Paragraph.builder().text(Text.from(head)).block(block(name, Padding.horizontal(1))).build(), + frame.renderWidget(Paragraph.builder().text(Text.from(head)).block(block(name, Padding.horizontal(1), false)).build(), rows.get(0)); - renderColumns(frame, rows.get(2), columns, Padding.uniform(1), 1); + renderColumns(frame, rows.get(2), columns, Padding.uniform(1), 1, focus); frame.renderWidget(Paragraph.builder().text(Text.from(status)).build(), indent(rows.get(4))); if (happenedHeight > 2) { frame.renderWidget(Paragraph.builder().text(Text.from(strips)) - .block(block(happenedTitle(info), Padding.uniform(1))).build(), rows.get(6)); + .block(block(happenedTitle(info), Padding.uniform(1), false)).build(), rows.get(6)); } frame.renderWidget(Paragraph.builder().text(Text.from(next())).build(), indent(rows.get(8))); } @@ -505,7 +508,7 @@ final class AboutView { return new Rect(r.x() + 2, r.y(), Math.max(0, r.width() - 2), r.height()); } - private static void renderColumns(Frame frame, Rect area, Columns columns, Padding padding, int spacing) { + private void renderColumns(Frame frame, Rect area, Columns columns, Padding padding, int spacing, int focus) { // by hand: the layout leaves the last column short of the edge when spaced int w0 = area.width() * 28 / 100; int w1 = area.width() * 42 / 100; @@ -513,11 +516,13 @@ final class AboutView { List<Rect> cols = List.of(new Rect(area.x(), area.y(), w0, area.height()), new Rect(area.x() + w0 + spacing, area.y(), w1, area.height()), new Rect(area.x() + w0 + w1 + 2 * spacing, area.y(), w2, area.height())); - frame.renderWidget(Paragraph.builder().text(Text.from(columns.in())).block(block(" In ", padding)).build(), + frame.renderWidget(Paragraph.builder().text(Text.from(columns.in())).block(block(" In ", padding, focus == 0)).build(), cols.get(0)); - frame.renderWidget(Paragraph.builder().text(Text.from(columns.routes())).block(block(" Routes ", padding)).build(), + frame.renderWidget( + Paragraph.builder().text(Text.from(columns.routes())).block(block(" Routes ", padding, focus == 1)).build(), cols.get(1)); - frame.renderWidget(Paragraph.builder().text(Text.from(columns.out())).block(block(" Out ", padding)).build(), + frame.renderWidget( + Paragraph.builder().text(Text.from(columns.out())).block(block(" Out ", padding, focus == 2)).build(), cols.get(2)); } @@ -571,6 +576,15 @@ final class AboutView { if (cursor >= all.size()) { cursor = all.size() - 1; } + focus = -1; + for (int i = 0, before = 0; i < columns.size() && cursor >= 0; i++) { + int n = (int) columns.get(i).stream().filter(t -> t != null).count(); + if (cursor < before + n) { + focus = i; + break; + } + before += n; + } highlight(c.in(), c.inTargets(), c.routes(), c.routeTargets(), c.out(), c.outTargets()); } @@ -1220,7 +1234,9 @@ final class AboutView { return lines; } - private static Block block(String title, Padding padding) { - return Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title(title).padding(padding).build(); + /** A panel: the one the cursor is in has the focus border, as the panels of the other screens. */ + private Block block(String title, Padding padding, boolean focused) { + return Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).borderStyle(ctx.paneBorder(focused)) + .title(title).padding(padding).build(); } } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractDiagramTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractDiagramTab.java index 668d0eb88c54..ad941094a4b1 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractDiagramTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractDiagramTab.java @@ -74,6 +74,7 @@ abstract class AbstractDiagramTab extends AbstractTab { protected AbstractDiagramTab(MonitorContext ctx) { super(ctx); diagram.zoom = ctx.routeZoom; + diagram.paneBorder = ctx::paneBorder; } boolean isShowDiagram() { 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 00b88047dd7d..cf5f1c612a5f 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 @@ -157,7 +157,8 @@ class CircuitBreakerTab extends AbstractTableTab { Constraint.fill()) .highlightStyle(Theme.selectionBg()) .highlightSpacing(Table.HighlightSpacing.ALWAYS) - .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title(" Circuit Breaker ").build()) + .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).borderStyle(ctx.paneBorder(true)) + .title(" Circuit Breaker ").build()) .build(); lastTableArea = chunks.get(0); @@ -271,7 +272,8 @@ class CircuitBreakerTab extends AbstractTableTab { frame.renderWidget(Paragraph.builder() .text(Text.from(lines)) - .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title(title).build()) + .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).borderStyle(ctx.paneBorder(false)) + .title(title).build()) .build(), area); } @@ -303,7 +305,8 @@ class CircuitBreakerTab extends AbstractTableTab { Span.styled("░".repeat(empty), Style.EMPTY.dim())); frame.renderWidget(Paragraph.builder() .text(Text.from(barLine)) - .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title(" Failure Rate ").build()) + .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).borderStyle(ctx.paneBorder(false)) + .title(" Failure Rate ").build()) .build(), vSplit.get(0)); LinkedList<Long> successHist = cbSuccessHistory.get(key); @@ -342,7 +345,7 @@ class CircuitBreakerTab extends AbstractTableTab { .showYAxis(true) .xLabels("-" + renderPoints + "s", "-" + (renderPoints * 3 / 4) + "s", "-" + (renderPoints / 2) + "s", "-" + (renderPoints / 4) + "s", "now") - .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL) + .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).borderStyle(ctx.paneBorder(false)) .title(Title.from(chartTitle)).build()) .build(), vSplit.get(1)); @@ -369,7 +372,8 @@ class CircuitBreakerTab extends AbstractTableTab { Span.styled("fail:", dim), Span.raw(" " + lastFail)); frame.renderWidget(Paragraph.builder() .text(Text.from(Line.from(Span.raw("")), metricsLine1, metricsLine2)) - .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).build()) + .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).borderStyle(ctx.paneBorder(false)) + .build()) .build(), vSplit.get(2)); } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramDetailSupport.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramDetailSupport.java index 23cd8ebf8b1e..2fe5127a1e1a 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramDetailSupport.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramDetailSupport.java @@ -384,7 +384,7 @@ final class DiagramDetailSupport { Paragraph paragraph = Paragraph.builder() .text(Text.from(lines)) - .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL) + .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).borderStyle(ctx.paneBorder(false)) .title(" Info ").build()) .build(); frame.renderWidget(paragraph, area); @@ -556,7 +556,7 @@ final class DiagramDetailSupport { Paragraph paragraph = Paragraph.builder() .text(Text.from(lines)) - .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL) + .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).borderStyle(ctx.paneBorder(false)) .title(" Info ").build()) .build(); frame.renderWidget(paragraph, area); @@ -675,6 +675,7 @@ final class DiagramDetailSupport { MarkdownView.builder() .source("*Select an EIP node*") .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL) + .borderStyle(ctx.paneBorder(false)) .title(" EIP Detail ").build()) .styles(Theme.markdownStyles()) .syntaxTheme(Theme.syntaxTheme()) @@ -690,6 +691,7 @@ final class DiagramDetailSupport { MarkdownView.builder() .source("*No detail available*") .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL) + .borderStyle(ctx.paneBorder(false)) .title(" EIP Detail ").build()) .styles(Theme.markdownStyles()) .syntaxTheme(Theme.syntaxTheme()) @@ -741,6 +743,7 @@ final class DiagramDetailSupport { .source(md.toString()) .scroll(detailScroll) .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL) + .borderStyle(ctx.paneBorder(false)) .title(title).build()) .styles(Theme.markdownStyles()) .syntaxTheme(Theme.syntaxTheme()) 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 ba0b954ef85b..e77f407025ed 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 @@ -29,12 +29,14 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; import java.util.function.Supplier; import dev.tamboui.layout.Constraint; import dev.tamboui.layout.Layout; import dev.tamboui.layout.Rect; import dev.tamboui.style.Color; +import dev.tamboui.style.Style; import dev.tamboui.terminal.Frame; import dev.tamboui.text.Line; import dev.tamboui.text.Span; @@ -77,6 +79,9 @@ import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.hint; class DiagramSupport { + // the border of the diagram panel: a screen where the keys move in the diagram gives it the focus border + Function<Boolean, Style> paneBorder = focused -> Style.EMPTY; + /** The width of a topology box in columns, as the layout engine draws it by default. */ static final int DEFAULT_BOX_COLUMNS = new TopologyLayoutEngine().getNodeWidth() / 15; /** How wide a topology box may grow so its label and group tag fit on a line. */ @@ -1104,7 +1109,7 @@ class DiagramSupport { void renderNativeDiagram(Frame frame, Rect area, Line title, boolean metrics) { Block.Builder builder = Block.builder() - .borderType(BorderType.ROUNDED).borders(Borders.ALL) + .borderType(BorderType.ROUNDED).borders(Borders.ALL).borderStyle(paneBorder.apply(true)) .title(Title.from(withAiMark(title))); if (groupLegend != null) { builder.titleBottom(Title.from(groupLegend)); @@ -1541,10 +1546,8 @@ class DiagramSupport { void renderNativeRouteDiagram( Frame frame, Rect area, Line title, boolean metrics, String currentRouteId, RouteDiagramLayoutEngine.LayoutRoute routeLayout) { - Block block = Block.builder() - .borderType(BorderType.ROUNDED).borders(Borders.ALL) - .title(Title.from(title)) - .build(); + Block block = Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL) + .borderStyle(paneBorder.apply(true)).title(Title.from(title)).build(); frame.renderWidget(block, area); Rect inner = block.inner(area); @@ -1958,10 +1961,8 @@ class DiagramSupport { return; } - Block block = Block.builder() - .borderType(BorderType.ROUNDED).borders(Borders.ALL) - .title(Title.from(withAiMark(title))) - .build(); + Block block = Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL) + .title(Title.from(withAiMark(title))).build(); frame.renderWidget(block, area); Rect inner = block.inner(area); @@ -2041,10 +2042,8 @@ class DiagramSupport { return; } - Block block = Block.builder() - .borderType(BorderType.ROUNDED).borders(Borders.ALL) - .title(Title.from(title)) - .build(); + Block block = Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL) + .title(Title.from(title)).build(); frame.renderWidget(block, area); Rect inner = block.inner(area); 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 90f6877aa898..44200a466296 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 @@ -688,6 +688,7 @@ class DiagramTab extends AbstractDiagramTab { "Loading diagram...", Style.EMPTY.dim())))) .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL) + .borderStyle(ctx.paneBorder(true)) .title(" Diagram ").build()) .build(), area); 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 6c71f5d8c199..48aeaa8492bf 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 @@ -411,7 +411,7 @@ class ErrorsTab extends AbstractTableTab { Constraint.fill()) .highlightStyle(Theme.selectionBg()) .highlightSpacing(Table.HighlightSpacing.ALWAYS) - .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL) + .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).borderStyle(ctx.paneBorder(true)) .title(" Errors [" + sorted.size() + "] ").build()) .build(); @@ -681,7 +681,8 @@ class ErrorsTab extends AbstractTableTab { int[] scroll = { detailScroll }; int[] hScroll = { detailHScroll }; - HistoryTab.renderDetailPanel(frame, area, lines, wordWrap, hScroll, scroll, detailScrollState, " Detail "); + HistoryTab.renderDetailPanel(frame, area, lines, wordWrap, hScroll, scroll, detailScrollState, " Detail ", + ctx.paneBorder(false), null); detailScroll = scroll[0]; detailHScroll = hScroll[0]; } @@ -777,7 +778,8 @@ class ErrorsTab extends AbstractTableTab { frame.renderWidget( Paragraph.builder() .text(Text.from(Line.from(Span.styled("No error selected", Style.EMPTY.dim())))) - .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title(" Info ").build()) + .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL) + .borderStyle(ctx.paneBorder(false)).title(" Info ").build()) .build(), area); return; @@ -847,7 +849,8 @@ class ErrorsTab extends AbstractTableTab { Paragraph.Builder pb = Paragraph.builder() .text(Text.from(lines)) - .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title(" Info ").build()); + .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).borderStyle(ctx.paneBorder(false)) + .title(" Info ").build()); if (wordWrap) { pb.overflow(Overflow.WRAP_WORD); } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HeapHistogramTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HeapHistogramTab.java index 47c792d58f94..036fe978593b 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HeapHistogramTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HeapHistogramTab.java @@ -109,6 +109,7 @@ class HeapHistogramTab extends AbstractTableTab { .text(Text.from( Line.from(Span.styled(" Loading heap histogram...", Style.EMPTY.dim())))) .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL) + .borderStyle(ctx.paneBorder(true)) .title(" Heap Histogram ").build()) .build(), area); @@ -163,7 +164,8 @@ class HeapHistogramTab extends AbstractTableTab { Constraint.length(15)) .highlightStyle(Theme.selectionBg()) .highlightSpacing(Table.HighlightSpacing.ALWAYS) - .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title(title).build()) + .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).borderStyle(ctx.paneBorder(true)) + .title(title).build()) .build(); frame.renderStatefulWidget(table, area, tableState); @@ -176,6 +178,7 @@ class HeapHistogramTab extends AbstractTableTab { Paragraph.builder() .text(Text.from(Line.from(Span.styled(" Select a class to see details", Style.EMPTY.dim())))) .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL) + .borderStyle(ctx.paneBorder(false)) .title(" Detail ").build()) .build(), area); @@ -262,7 +265,8 @@ class HeapHistogramTab extends AbstractTableTab { frame.renderWidget( Paragraph.builder() .text(Text.from(lines)) - .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title(title).build()) + .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL) + .borderStyle(ctx.paneBorder(false)).title(title).build()) .build(), area); } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JfrTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JfrTab.java index d22fabb37db0..11a221df6dfb 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JfrTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JfrTab.java @@ -319,6 +319,7 @@ class JfrTab extends AbstractTab { frame.renderWidget( Paragraph.builder().text(Text.from(lines)) .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL) + .borderStyle(ctx.paneBorder(false)) .title(" JFR ").build()) .build(), area); @@ -333,6 +334,7 @@ class JfrTab extends AbstractTab { Paragraph.builder() .text(Text.from(Line.from(Span.styled(msg, LABEL)))) .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL) + .borderStyle(ctx.paneBorder(true)) .title(" " + activeView.label + " ").build()) .build(), area); @@ -397,7 +399,7 @@ class JfrTab extends AbstractTab { Constraint.length(10), Constraint.length(10), Constraint.length(10)) .highlightStyle(Theme.selectionBg()) .highlightSpacing(Table.HighlightSpacing.ALWAYS) - .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL) + .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).borderStyle(ctx.paneBorder(true)) .title(" Routes (" + data.size() + ") ").build()) .build(); lastTableArea = chunks.get(0); @@ -415,6 +417,7 @@ class JfrTab extends AbstractTab { Paragraph.builder() .text(Text.from(Line.from(Span.styled("No routes", Style.EMPTY.dim())))) .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL) + .borderStyle(ctx.paneBorder(false)) .title(" Processors ").build()) .build(), chunks.get(1)); @@ -467,7 +470,7 @@ class JfrTab extends AbstractTab { Constraint.length(10), Constraint.length(10), Constraint.length(10)) .highlightStyle(Theme.selectionBg()) .highlightSpacing(Table.HighlightSpacing.ALWAYS) - .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL) + .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).borderStyle(ctx.paneBorder(false)) .title(" Processors [" + routeId + "] (" + data.size() + ") ").build()) .build(); frame.renderStatefulWidget(table, area, processorTableState); @@ -521,7 +524,7 @@ class JfrTab extends AbstractTab { Constraint.length(10), Constraint.length(10), Constraint.length(10)) .highlightStyle(Theme.selectionBg()) .highlightSpacing(Table.HighlightSpacing.ALWAYS) - .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL) + .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).borderStyle(ctx.paneBorder(true)) .title(" Processors (" + data.size() + ") ").build()) .build(); lastTableArea = area; @@ -572,7 +575,7 @@ class JfrTab extends AbstractTab { Constraint.length(10), Constraint.length(10), Constraint.length(10)) .highlightStyle(Theme.selectionBg()) .highlightSpacing(Table.HighlightSpacing.ALWAYS) - .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL) + .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).borderStyle(ctx.paneBorder(true)) .title(" Endpoints (" + data.size() + ") ").build()) .build(); lastTableArea = area; @@ -608,7 +611,7 @@ class JfrTab extends AbstractTab { Constraint.length(28), Constraint.fill()) .highlightStyle(Theme.selectionBg()) .highlightSpacing(Table.HighlightSpacing.ALWAYS) - .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL) + .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).borderStyle(ctx.paneBorder(true)) .title(" Failures (" + failureData.size() + ") ").build()) .build(); lastTableArea = area; @@ -641,7 +644,7 @@ class JfrTab extends AbstractTab { Constraint.length(8), Constraint.length(8)) .highlightStyle(Theme.selectionBg()) .highlightSpacing(Table.HighlightSpacing.ALWAYS) - .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL) + .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).borderStyle(ctx.paneBorder(true)) .title(" Redeliveries (" + redeliveryData.size() + ") ").build()) .build(); lastTableArea = area; 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 b533dc6c4025..1c841f510c32 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 @@ -357,8 +357,8 @@ class RoutesTab extends AbstractDiagramTab { frame.renderWidget( Paragraph.builder() .text(Text.from(Line.from(Span.styled("No routes", Style.EMPTY.dim())))) - .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title(" Processors ") - .build()) + .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL) + .borderStyle(ctx.paneBorder(false)).title(" Processors ").build()) .build(), chunks.get(1)); } @@ -536,7 +536,7 @@ class RoutesTab extends AbstractDiagramTab { .highlightStyle(Theme.selectionBg()) .highlightSpacing(Table.HighlightSpacing.ALWAYS) .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL) - .title(" Routes ").build()) + .borderStyle(ctx.paneBorder(true)).title(" Routes ").build()) .build(); } @@ -682,7 +682,7 @@ class RoutesTab extends AbstractDiagramTab { .highlightStyle(Theme.selectionBg()) .highlightSpacing(Table.HighlightSpacing.ALWAYS) .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL) - .title(" Routes ").build()) + .borderStyle(ctx.paneBorder(true)).title(" Routes ").build()) .build(); } @@ -990,7 +990,7 @@ class RoutesTab extends AbstractDiagramTab { .header(Row.from(header)) .widths(widths.toArray(Constraint[]::new)) .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL) - .title(" Processors [" + route.routeId + "] ") + .borderStyle(ctx.paneBorder(false)).title(" Processors [" + route.routeId + "] ") .build()) .build(); } else { @@ -1125,7 +1125,7 @@ class RoutesTab extends AbstractDiagramTab { .header(Row.from(header)) .widths(widths.toArray(Constraint[]::new)) .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL) - .title(" Processors [" + route.routeId + "] ") + .borderStyle(ctx.paneBorder(false)).title(" Processors [" + route.routeId + "] ") .build()) .build(); } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTab.java index 3de12c0904b1..ac381233f9ac 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTab.java @@ -424,12 +424,13 @@ class TimelineTab extends AbstractTableTab { routeStrip = room.strip(); lastTableArea = new Rect(top.x(), routeTop, top.width(), Math.max(1, routeHeight)); Padding padding = room.roomy() ? Padding.uniform(1) : Padding.NONE; - frame.renderWidget(Paragraph.builder().text(Text.from(lines)).block(block(title(info), padding)).build(), top); + frame.renderWidget( + Paragraph.builder().text(Text.from(lines)).block(block(title(info), padding, !drill.isOpen())).build(), top); if (drill.isOpen()) { List<Timeline.Kept> kept = keptAtCursor(); frame.renderWidget(Paragraph.builder().text(Text.from(drill.lines(kept, detailHeight - 2 - 2 * gap))) - .block(block(drill.title(kept, ctx.timeline.done(info.pid, drill.from(), drill.to())), padding)) + .block(block(drill.title(kept, ctx.timeline.done(info.pid, drill.from(), drill.to())), padding, true)) .build(), rows.get(2)); return; @@ -705,7 +706,7 @@ class TimelineTab extends AbstractTableTab { Padding padding = room.roomy() ? Padding.uniform(1) : Padding.NONE; if (r == null) { frame.renderWidget(Paragraph.builder().text(Text.from(Line.from(Span.raw("")))) - .block(block(" Route ", padding)).build(), area); + .block(block(" Route ", padding, false)).build(), area); return; } List<long[]> values = List.of(cols(r.completed(), false), cols(r.recovered(), false), @@ -788,7 +789,7 @@ class TimelineTab extends AbstractTableTab { addTotal(totals, r.total(r.failed()), "failed", Theme.error()); lines.add(Line.from(totals)); String title = " " + r.route() + " ─ per second "; - frame.renderWidget(Paragraph.builder().text(Text.from(lines)).block(block(title, padding)).build(), area); + frame.renderWidget(Paragraph.builder().text(Text.from(lines)).block(block(title, padding, false)).build(), area); } private static void addTotal(List<Span> spans, long count, String words, Style style) { @@ -917,8 +918,10 @@ class TimelineTab extends AbstractTableTab { return s.length() >= width ? s : " ".repeat(width - s.length()) + s; } - private static Block block(String title, Padding padding) { - return Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title(title).padding(padding).build(); + /** A panel: the one the keys go to has the focus border (the routes, or the exchanges of a second when open). */ + private Block block(String title, Padding padding, boolean focused) { + return Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).borderStyle(ctx.paneBorder(focused)) + .title(title).padding(padding).build(); } @Override diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutViewTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutViewTest.java index e867bb9cc6b4..a2b942acd9f6 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutViewTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutViewTest.java @@ -28,6 +28,7 @@ import java.util.concurrent.atomic.AtomicReference; import dev.tamboui.buffer.Buffer; import dev.tamboui.layout.Rect; +import dev.tamboui.style.Color; import dev.tamboui.terminal.Frame; import dev.tamboui.tui.event.KeyCode; import dev.tamboui.tui.event.KeyEvent; @@ -281,6 +282,14 @@ class AboutViewTest { return ep; } + /** The colours of the borders of In and Routes. */ + private static List<Color> borders(AboutView about, IntegrationInfo info) { + Rect area = new Rect(0, 0, 160, 34); + Buffer buffer = Buffer.empty(area); + about.render(Frame.forTesting(buffer), area, info); + return List.of(TuiTestHelper.borderColor(buffer, " In "), TuiTestHelper.borderColor(buffer, " Routes ")); + } + @Test void aboutAnIntegrationWithWhatHasHappened() throws Exception { Files.writeString(dir.resolve("shop.camel.yaml"), """ @@ -356,9 +365,12 @@ class AboutViewTest { about.moveCursor(1); render(about, info); assertThat(about.selected()).isEqualTo(new AboutView.Target("orders", null)); + // the panel the cursor is in has the focus border + assertThat(borders(about, info)).containsExactly(Theme.accent(), Theme.muted().fg().orElseThrow()); about.moveCursor(2); render(about, info); assertThat(about.selected()).isEqualTo(new AboutView.Target("stock", null)); + assertThat(borders(about, info)).containsExactly(Theme.muted().fg().orElseThrow(), Theme.accent()); assertThat(about.selectionContext().items()).contains("in: timer:orders (period=500) → orders", "route: orders", "route: stock"); assertThat(about.selectionContext().selectedIndex()).isEqualTo(2); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTabRenderTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTabRenderTest.java index ee381eead6d1..7a3a3cbf862c 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTabRenderTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTabRenderTest.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; +import dev.tamboui.buffer.Buffer; import dev.tamboui.layout.Rect; import dev.tamboui.text.Span; import dev.tamboui.tui.event.KeyCode; @@ -384,6 +385,21 @@ class TimelineTabRenderTest { assertThat(tab.getTableDataAsJson().getInteger("selectedIndex")).isEqualTo(2); } + @Test + void thePanelTheKeysGoToHasTheFocusBorder() { + TimelineTab tab = new TimelineTab(ctx, now::get); + Buffer screen = TuiTestHelper.renderToBuffer(tab, 160, 40); + // ↑↓ select a route: the timeline has the focus + assertThat(TuiTestHelper.borderColor(screen, "Timeline ─ shop")).isEqualTo(Theme.accent()); + assertThat(TuiTestHelper.borderColor(screen, "orders ─ per second")).isNotEqualTo(Theme.accent()); + + // Enter: ↑↓ go over the exchanges of the second + tab.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER)); + screen = TuiTestHelper.renderToBuffer(tab, 160, 40); + assertThat(TuiTestHelper.borderColor(screen, "Timeline ─ shop")).isNotEqualTo(Theme.accent()); + assertThat(TuiTestHelper.borderColor(screen, "Exchanges at")).isEqualTo(Theme.accent()); + } + private static int indexOf(List<String> lines, String text) { for (int i = 0; i < lines.size(); i++) { if (lines.get(i).contains(text)) { diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiTestHelper.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiTestHelper.java index a02465bd76ee..cdfffd78fa4c 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiTestHelper.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiTestHelper.java @@ -74,4 +74,29 @@ final class TuiTestHelper { } return false; } + + /** Renders a tab into a virtual buffer, for the styles of its cells. */ + static Buffer renderToBuffer(MonitorTab tab, int width, int height) { + Rect area = new Rect(0, 0, width, height); + Buffer buffer = Buffer.empty(area); + tab.render(Frame.forTesting(buffer), area); + return buffer; + } + + /** The colour of the border of the panel with this title, from its top left corner, or null when none. */ + static Color borderColor(Buffer buffer, String title) { + for (int y = 0; y < buffer.height(); y++) { + StringBuilder row = new StringBuilder(); + for (int x = 0; x < buffer.width(); x++) { + String sym = buffer.get(x, y).symbol(); + row.append(sym.isEmpty() ? " " : sym.substring(0, 1)); + } + int at = row.indexOf(title); + if (at >= 0) { + int corner = row.lastIndexOf("╭", at); + return corner >= 0 ? buffer.get(corner, y).style().fg().orElse(null) : null; + } + } + return null; + } }
