This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch feature/CAMEL-24759-tamboui-05-cleanup in repository https://gitbox.apache.org/repos/asf/camel.git
commit 1bc4b8e6fa1eb4699b7894baea0a5f19b28dd47d Author: Claus Ibsen <[email protected]> AuthorDate: Sat Sep 19 10:23:37 2026 +0200 CAMEL-24759: camel-jbang - TUI: drop the workarounds that TamboUI 0.5.0 makes redundant TuiBackendHelper no longer wraps the explicit JLine backend for recording itself: TuiRunner.create does that for explicitly configured backends since tamboui/tamboui#418. The recording test now goes through createTuiRunner and asserts on the runner's backend instead. The throughput chart keeps its rate * 100 history intact and converts the y-axis label back to msg/s through DualSparkline.yAxisFormatter (tamboui/tamboui#396) rather than dividing the data before rendering, so sub-1 msg/s rates keep their bar height again. The payload size chart gets a compact byte formatter instead of the default 999+ cap. The plain-emoji icon rule from CAMEL-23818 stays: TamboUI 0.5.0 now measures base + VS16 sequences as 2 columns (tamboui/tamboui#388), but the xterm.js Unicode 11 width tables used by --web still treat VS16 as zero-width, so the comments now give that as the reason. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../dsl/jbang/core/commands/tui/FlowHelper.java | 30 ++++++++++-- .../jbang/core/commands/tui/TuiBackendHelper.java | 26 ++-------- .../dsl/jbang/core/commands/tui/TuiIcons.java | 8 +-- .../jbang/core/commands/tui/FlowHelperTest.java | 57 ++++++++++++++++++++++ .../tui/TuiBackendHelperRecordingTest.java | 41 +++++++++------- .../dsl/jbang/core/commands/tui/TuiIconsTest.java | 7 +-- 6 files changed, 120 insertions(+), 49 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FlowHelper.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FlowHelper.java index fb3a9c1b08cf..91064f51d6e4 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FlowHelper.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FlowHelper.java @@ -126,11 +126,6 @@ final class FlowHelper { } long curIn = inArr[renderPoints - 1]; long curOut = outArr[renderPoints - 1]; - // scale down from internal precision (rate * THROUGHPUT_SCALE) to actual msg/s for y-axis - for (int i = 0; i < renderPoints; i++) { - inArr[i] = Math.round((double) inArr[i] / MetricsCollector.THROUGHPUT_SCALE); - outArr[i] = Math.round((double) outArr[i] / MetricsCollector.THROUGHPUT_SCALE); - } List<Span> titleSpans = new ArrayList<>(); if (chartLabel != null) { @@ -156,6 +151,9 @@ final class FlowHelper { .topStyle(Theme.success()) .bottomStyle(Style.EMPTY.fg(Theme.accent())) .showYAxis(true) + // the data stays scaled by THROUGHPUT_SCALE so sub-1 msg/s rates keep their bar height; the axis + // label converts back to msg/s like the title does + .yAxisFormatter(MetricsCollector::formatThroughput) .xLabels("-" + renderPoints + "s", "-" + (renderPoints * 3 / 4) + "s", "-" + (renderPoints / 2) + "s", "-" + (renderPoints / 4) + "s", "now") .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL) @@ -198,6 +196,7 @@ final class FlowHelper { .topStyle(Theme.label()) .bottomStyle(Theme.notice()) .showYAxis(true) + .yAxisFormatter(FlowHelper::compactSize) .xLabels("-" + renderPoints + "s", "-" + (renderPoints * 3 / 4) + "s", "-" + (renderPoints / 2) + "s", "-" + (renderPoints / 4) + "s", "now") .block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL) @@ -221,6 +220,27 @@ final class FlowHelper { } } + /** + * Formats a byte count into at most four characters for the sparkline y-axis, where {@link #sizeToString(long)} + * would not fit: {@code 512}, {@code 1.5K}, {@code 12K}, {@code 1.2M}. + */ + static String compactSize(long size) { + if (size < 1000) { + return String.valueOf(Math.max(0, size)); + } + double kb = size / 1024.0; + if (kb < 10) { + return String.format(Locale.US, "%.1fK", kb); + } else if (kb < 999.5) { + return Math.round(kb) + "K"; + } + double mb = kb / 1024.0; + if (mb < 10) { + return String.format(Locale.US, "%.1fM", mb); + } + return Math.round(mb) + "M"; + } + private static long unbox(Long value) { return value != null ? value : 0L; } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiBackendHelper.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiBackendHelper.java index 2b5fa294e30f..3a5791ac6e19 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiBackendHelper.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiBackendHelper.java @@ -17,8 +17,6 @@ package org.apache.camel.dsl.jbang.core.commands.tui; import dev.tamboui.backend.jline3.JLineBackend; -import dev.tamboui.internal.record.RecordingBackend; -import dev.tamboui.internal.record.RecordingConfig; import dev.tamboui.terminal.Backend; import dev.tamboui.tui.TuiConfig; import dev.tamboui.tui.TuiRunner; @@ -40,26 +38,12 @@ final class TuiBackendHelper { return createTuiRunner(backend); } - static TuiRunner createTuiRunner(Backend backend) throws Exception { - return TuiRunner.create( - TuiConfig.builder().backend(applyRecording(backend)).mouseCapture(true).bracketedPaste(true).build()); - } - /** - * Wraps the backend for Asciinema recording when {@code --record} configured the {@code tamboui.record*} system - * properties. - * <p> - * TamboUI normally does this inside {@code BackendFactory.create()}, but {@code TuiRunner} only calls that factory - * when no explicit backend is configured. Because we must pass an explicit backend (see - * {@link #createTuiRunner()}), the wrapping has to be done here instead, otherwise {@code --record} exits cleanly - * without replaying the tape or writing a {@code .cast} file. + * Creates the runner for an explicitly built backend. {@code TuiRunner.create} wraps it for Asciinema recording + * itself when {@code --record} configured the {@code tamboui.record*} system properties. */ - static Backend applyRecording(Backend backend) { - // Guard on isEnabled() first: load() caches its result process-wide and installs a System.out capture - if (!RecordingConfig.isEnabled()) { - return backend; - } - RecordingConfig config = RecordingConfig.load(); - return config != null ? new RecordingBackend(backend, config) : backend; + static TuiRunner createTuiRunner(Backend backend) throws Exception { + return TuiRunner.create( + TuiConfig.builder().backend(backend).mouseCapture(true).bracketedPaste(true).build()); } } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java index 554f1e0f6dc2..c8e6b7bd725a 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java @@ -21,7 +21,9 @@ import java.util.List; /** * Single source of truth for emoji and symbolic icons used across the Camel TUI. * <p/> - * Tab/menu icons use plain 2-column-wide emoji without VS16 variation selectors (see CAMEL-23818). Doctor and legacy + * Tab/menu icons use plain 2-column-wide emoji without VS16 variation selectors (CAMEL-23818). TamboUI 0.5.0 measures a + * base glyph + VS16 sequence as 2 columns (tamboui/tamboui#388), but the xterm.js Unicode 11 width tables that the + * {@code --web} frontend uses treat VS16 as zero-width, so such a sequence still misaligns there. Doctor and legacy * status glyphs may still use mixed-width symbols until migrated. */ final class TuiIcons { @@ -60,8 +62,8 @@ final class TuiIcons { // memo (📝) reads as "edit"; the letters glyph reads as changing the name static final String RENAME = "🔤"; static final String DUPLICATE = "📑"; - // NOTE: the wastebasket emoji (🗑) is width-ambiguous and TamboUI does not align it correctly yet, so use the - // cross-mark instead until that is fixed upstream. + // the wastebasket (🗑) is a text-default glyph: TamboUI counts it as 2 columns while terminals draw it in 1 unless + // it carries VS16, which the --web frontend cannot measure (see the class javadoc), so use the cross-mark instead static final String DELETE = "❌"; // ---- Actions menu ---- diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/FlowHelperTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/FlowHelperTest.java new file mode 100644 index 000000000000..d7daaa72fafd --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/FlowHelperTest.java @@ -0,0 +1,57 @@ +/* + * 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.Map; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class FlowHelperTest { + + @Test + void compactSizeFitsTheFourColumnAxisLabel() { + Map<Long, String> expected = Map.of( + 0L, "0", + 512L, "512", + 999L, "999", + 1024L, "1.0K", + 1536L, "1.5K", + 12288L, "12K", + 1023488L, "1.0M", // 999.5 KB rounds up into the megabyte range rather than to "1000K" + 1048576L, "1.0M", + 1258291L, "1.2M", + 15728640L, "15M"); + + expected.forEach((size, label) -> { + assertThat(FlowHelper.compactSize(size)).as("size %d", size).isEqualTo(label); + assertThat(label.length()).isLessThanOrEqualTo(4); + }); + } + + @Test + void throughputAxisLabelConvertsScaledRatesBackToMessagesPerSecond() { + // the chart data stays scaled by THROUGHPUT_SCALE, so the axis label has to undo the scaling + assertThat(MetricsCollector.formatThroughput(0)).isEqualTo("0"); + assertThat(MetricsCollector.formatThroughput(20)).isEqualTo("0.20"); + assertThat(MetricsCollector.formatThroughput(150)).isEqualTo("1.5"); + assertThat(MetricsCollector.formatThroughput(700)).isEqualTo("7.0"); + assertThat(MetricsCollector.formatThroughput(1500)).isEqualTo("15"); + assertThat(MetricsCollector.formatThroughput(1234500)).isEqualTo("12K"); + } +} diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiBackendHelperRecordingTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiBackendHelperRecordingTest.java index fa2e495ae974..c95f2a274b38 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiBackendHelperRecordingTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiBackendHelperRecordingTest.java @@ -25,6 +25,7 @@ import dev.tamboui.internal.record.AnsiTerminalCapture; import dev.tamboui.layout.Position; import dev.tamboui.layout.Size; import dev.tamboui.terminal.Backend; +import dev.tamboui.tui.TuiRunner; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -35,12 +36,12 @@ import static org.assertj.core.api.Assertions.assertThat; /** * Verifies that {@code camel tui --record} actually engages TamboUI's recording backend. * <p> - * TamboUI only wraps a backend for recording inside {@code BackendFactory.create()}, and {@code TuiRunner} calls that - * factory <em>only</em> when no explicit backend is configured. Since camel-tui must supply an explicit - * {@link dev.tamboui.backend.jline3.JLineBackend} (auto-discovery would otherwise pick the Aesh backend that is on the - * classpath for {@code --web}), the recording wrapper is never applied unless camel-tui applies it itself. When that - * wrapping is missing, {@code --record} replays no tape and writes no {@code .cast} file, yet still exits cleanly, so - * only a test like this one catches the regression. + * camel-tui must supply an explicit {@link dev.tamboui.backend.jline3.JLineBackend} (auto-discovery would otherwise + * pick the Aesh backend that is on the classpath for {@code --web}). Before TamboUI 0.5.0 only backends created by + * TamboUI's own factory were wrapped for recording, so camel-tui wrapped its explicit backend itself; since 0.5.0 + * {@code TuiRunner.create} does that for explicit backends too. When the wrapping goes missing, {@code --record} + * replays no tape and writes no {@code .cast} file, yet still exits cleanly, so only a test like this one catches the + * regression. */ // RecordingConfig.load() also reads fps and duration, so every key camel-tui sets has to be managed here: a value // leaking out of this class would silently reconfigure recording in an unrelated test. junit-pioneer clears each key @@ -65,12 +66,12 @@ class TuiBackendHelperRecordingTest { } @Test - void withoutRecordOptionTheBackendIsHandedToTuiRunnerUntouched() { + void withoutRecordOptionTheBackendIsHandedToTuiRunnerUntouched() throws Exception { Backend original = new NoopBackend(); - Backend result = TuiBackendHelper.applyRecording(original); - - assertThat(result).isSameAs(original); + try (TuiRunner runner = TuiBackendHelper.createTuiRunner(original)) { + assertThat(runner.backend()).isSameAs(original); + } } @Test @@ -82,13 +83,14 @@ class TuiBackendHelperRecordingTest { System.setProperty("tamboui.record.width", "120"); System.setProperty("tamboui.record.height", "30"); - Backend result = TuiBackendHelper.applyRecording(new NoopBackend()); - - // A recording backend reports the configured cast dimensions rather than the real terminal - // size; asserting on those proves the configuration was applied, not merely that some - // wrapper was returned. - assertThat(result).isNotInstanceOf(NoopBackend.class); - assertThat(result.size()).isEqualTo(new Size(120, 30)); + try (TuiRunner runner = TuiBackendHelper.createTuiRunner(new NoopBackend())) { + // A recording backend reports the configured cast dimensions rather than the real terminal + // size; asserting on those proves the configuration was applied, not merely that some + // wrapper was returned. + Backend result = runner.backend(); + assertThat(result).isNotInstanceOf(NoopBackend.class); + assertThat(result.size()).isEqualTo(new Size(120, 30)); + } } /** @@ -161,6 +163,11 @@ class TuiBackendHelperRecordingTest { return -2; } + @Override + public void writeRaw(byte[] data) throws IOException { + // TuiRunner.create enables bracketed paste through this method + } + @Override public void close() throws IOException { } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIconsTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIconsTest.java index 6ed452a76918..0fa31d88d4bd 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIconsTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIconsTest.java @@ -24,9 +24,10 @@ 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. + * Validates {@link TuiIcons} primary tab emoji width (CAMEL-23818: no VS16 sequences, which the {@code --web} xterm.js + * width tables measure differently from 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 {
