This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 097a554ac39b CAMEL-24003: Add Select Tab setting and fix activity data
flicker
097a554ac39b is described below
commit 097a554ac39b57a8a0855af9250e0f15b187bea3
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Jul 13 14:52:45 2026 +0200
CAMEL-24003: Add Select Tab setting and fix activity data flicker
Add a "Select Tab" setting to the F2 Settings popup that controls which
tab the TUI navigates to after selecting an integration in Overview.
Default is Log tab (preserving current behavior). Users can change it to
any primary tab (Overview, Activity, Routes, etc.).
Fix activity data flicker caused by clear() + addAll() race on
CopyOnWriteArrayList. Replace with volatile List reference and atomic
swap via List.copyOf() so the render thread never sees an empty
intermediate state.
CAMEL-24003: Fix errors data flicker with same atomic swap pattern
Apply the same volatile List + atomic swap fix to the errors field
that was applied to activity data in the previous commit.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../dsl/jbang/core/commands/tui/CamelMonitor.java | 20 +++++-
.../core/commands/tui/DataRefreshService.java | 6 +-
.../jbang/core/commands/tui/IntegrationInfo.java | 5 +-
.../dsl/jbang/core/commands/tui/SettingsPopup.java | 71 +++++++++++++++++++---
.../dsl/jbang/core/commands/tui/TuiSettings.java | 12 ++++
.../jbang/core/commands/tui/SettingsPopupTest.java | 10 ++-
6 files changed, 103 insertions(+), 21 deletions(-)
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
index 6f9ddfa8ddde..fe49c4bcfd84 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
@@ -895,8 +895,10 @@ public class CamelMonitor extends CamelCommand {
if (ke.isConfirm() && tab == TAB_OVERVIEW) {
tabRegistry.overviewTab().selectCurrentIntegration();
if (ctx.selectedPid != null) {
- tabsState.select(TAB_LOG);
- refreshLogData();
+ int selectTab = resolveSelectTab();
+ if (selectTab != TAB_OVERVIEW) {
+ tabRegistry.handleTabKey(selectTab, ctx, dataService);
+ }
}
return true;
}
@@ -2071,6 +2073,20 @@ public class CamelMonitor extends CamelCommand {
return fKeyTotal;
}
+ private int resolveSelectTab() {
+ TuiSettings settings = TuiSettings.load();
+ String tabName = settings.getSelectTab();
+ if (tabName == null) {
+ return TAB_LOG;
+ }
+ for (TabRegistry.TabEntry entry : tabRegistry.allTabEntries()) {
+ if (entry.name().equals(tabName) && entry.tabIndex() != TAB_MORE) {
+ return entry.tabIndex();
+ }
+ }
+ return TAB_LOG;
+ }
+
// ---- Data Loading ----
private void refreshLogData() {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshService.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshService.java
index 5c30e084330e..b4396dbec667 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshService.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshService.java
@@ -611,8 +611,7 @@ class DataRefreshService {
JsonObject root = loadErrorFile(pid);
if (root != null) {
List<ErrorInfo> parsed = StatusParser.parseErrors(root);
- sel.errors.clear();
- sel.errors.addAll(parsed);
+ sel.errors = List.copyOf(parsed);
}
} catch (Exception e) {
// ignore
@@ -674,8 +673,7 @@ class DataRefreshService {
if (content != null && !content.isBlank()) {
JsonObject json = (JsonObject) Jsoner.deserialize(content);
List<ActivityEntry> parsed =
StatusParser.parseActivityEntries(json);
- sel.activity.clear();
- sel.activity.addAll(parsed);
+ sel.activity = List.copyOf(parsed);
}
} catch (Exception e) {
// ignore
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
index 88a97555ad93..b0201105a1c7 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
@@ -18,7 +18,6 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
import java.util.ArrayList;
import java.util.List;
-import java.util.concurrent.CopyOnWriteArrayList;
class IntegrationInfo {
String pid;
@@ -85,8 +84,8 @@ class IntegrationInfo {
final List<EndpointInfo> endpoints = new ArrayList<>();
final List<CircuitBreakerInfo> circuitBreakers = new ArrayList<>();
int errorCount;
- final List<ErrorInfo> errors = new CopyOnWriteArrayList<>();
- final List<ActivityEntry> activity = new CopyOnWriteArrayList<>();
+ volatile List<ErrorInfo> errors = new ArrayList<>();
+ volatile List<ActivityEntry> activity = List.of();
boolean inflightBrowseEnabled;
final List<InflightInfo> inflightExchanges = new ArrayList<>();
final List<MicrometerMeterInfo> meters = new ArrayList<>();
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopup.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopup.java
index 7b980c27b440..8a5e924a76c7 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopup.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopup.java
@@ -48,13 +48,14 @@ class SettingsPopup {
private static final int ROW_THEME = 0;
private static final int ROW_START_TAB = 1;
- private static final int ROW_LOG_PIN = 2;
- private static final int ROW_RATE_PER = 3;
- private static final int ROW_FOLDER = 4;
- private static final int ROW_AI_PROVIDER = 5;
- private static final int ROW_AI_MODEL = 6;
- private static final int ROW_AI_URL = 7;
- private static final int ROW_COUNT = 8;
+ private static final int ROW_SELECT_TAB = 2;
+ private static final int ROW_LOG_PIN = 3;
+ private static final int ROW_RATE_PER = 4;
+ private static final int ROW_FOLDER = 5;
+ private static final int ROW_AI_PROVIDER = 6;
+ private static final int ROW_AI_MODEL = 7;
+ private static final int ROW_AI_URL = 8;
+ private static final int ROW_COUNT = 9;
private static final String[] LOG_PIN_OPTIONS = { "off", "25", "50", "75"
};
private static final String[] RATE_PER_OPTIONS = { "seconds", "minutes" };
@@ -75,6 +76,7 @@ class SettingsPopup {
private TuiSettings settings;
private int themeIndex;
private int startTabIndex;
+ private int selectTabIndex;
private int logPinIndex;
private int ratePerIndex;
private int aiProviderIndex;
@@ -84,6 +86,7 @@ class SettingsPopup {
private List<String> tabNames = new ArrayList<>();
private List<TabRegistry.TabEntry> tabEntries;
+ private List<String> primaryTabNames = new ArrayList<>();
private Runnable clearScreen;
private MonitorContext monitorContext;
@@ -120,6 +123,21 @@ class SettingsPopup {
int idx = tabNames.indexOf(currentStartTab);
startTabIndex = idx >= 0 ? idx : 0;
+ primaryTabNames = new ArrayList<>();
+ if (tabEntries != null) {
+ for (TabRegistry.TabEntry entry : tabEntries) {
+ if (entry.tabIndex() != TabRegistry.TAB_MORE) {
+ primaryTabNames.add(entry.name());
+ }
+ }
+ }
+ String currentSelectTab = settings.getSelectTab() != null ?
settings.getSelectTab() : "Log";
+ int selIdx = primaryTabNames.indexOf(currentSelectTab);
+ selectTabIndex = selIdx >= 0 ? selIdx : primaryTabNames.indexOf("Log");
+ if (selectTabIndex < 0) {
+ selectTabIndex = 0;
+ }
+
String currentLogPin = settings.getLogPin() != null ?
settings.getLogPin() : "off";
logPinIndex = 0;
for (int i = 0; i < LOG_PIN_OPTIONS.length; i++) {
@@ -196,6 +214,16 @@ class SettingsPopup {
}
return true;
}
+ if (selectedRow == ROW_SELECT_TAB) {
+ if (!primaryTabNames.isEmpty()) {
+ if (ke.isChar(' ') || ke.isRight()) {
+ selectTabIndex = (selectTabIndex + 1) %
primaryTabNames.size();
+ } else if (ke.isLeft()) {
+ selectTabIndex = (selectTabIndex - 1 +
primaryTabNames.size()) % primaryTabNames.size();
+ }
+ }
+ return true;
+ }
if (selectedRow == ROW_LOG_PIN) {
if (ke.isChar(' ') || ke.isRight()) {
logPinIndex = (logPinIndex + 1) % LOG_PIN_OPTIONS.length;
@@ -241,6 +269,10 @@ class SettingsPopup {
if (!tabNames.isEmpty()) {
settings.setStartTab(tabNames.get(startTabIndex));
}
+ if (!primaryTabNames.isEmpty()) {
+ String selectTabValue = primaryTabNames.get(selectTabIndex);
+ settings.setSelectTab("Log".equals(selectTabValue) ? null :
selectTabValue);
+ }
String logPinValue = LOG_PIN_OPTIONS[logPinIndex];
settings.setLogPin("off".equals(logPinValue) ? null : logPinValue);
String ratePerValue = RATE_PER_OPTIONS[ratePerIndex];
@@ -293,6 +325,10 @@ class SettingsPopup {
renderValue(frame, innerX + labelW, rowY, fieldW, currentTabLabel(),
selectedRow == ROW_START_TAB);
rowY++;
+ renderLabel(frame, innerX, rowY, labelW, "Select Tab:", selectedRow ==
ROW_SELECT_TAB);
+ renderValue(frame, innerX + labelW, rowY, fieldW,
currentSelectTabLabel(), selectedRow == ROW_SELECT_TAB);
+ rowY++;
+
String logPinLabel = "off".equals(LOG_PIN_OPTIONS[logPinIndex])
? "off"
: LOG_PIN_OPTIONS[logPinIndex] + "%";
@@ -323,8 +359,8 @@ class SettingsPopup {
void renderFooter(List<Span> spans) {
hint(spans, TuiIcons.HINT_SCROLL, "navigate");
- if (selectedRow == ROW_THEME || selectedRow == ROW_START_TAB ||
selectedRow == ROW_LOG_PIN
- || selectedRow == ROW_RATE_PER || selectedRow ==
ROW_AI_PROVIDER) {
+ if (selectedRow == ROW_THEME || selectedRow == ROW_START_TAB ||
selectedRow == ROW_SELECT_TAB
+ || selectedRow == ROW_LOG_PIN || selectedRow == ROW_RATE_PER
|| selectedRow == ROW_AI_PROVIDER) {
hint(spans, "Space", "cycle");
}
hint(spans, "Enter", "save");
@@ -347,6 +383,19 @@ class SettingsPopup {
return entry.icon() + " " + entry.name();
}
+ private String currentSelectTabLabel() {
+ if (tabEntries == null || primaryTabNames.isEmpty()) {
+ return "Log";
+ }
+ String name = primaryTabNames.get(Math.min(selectTabIndex,
primaryTabNames.size() - 1));
+ for (TabRegistry.TabEntry entry : tabEntries) {
+ if (entry.name().equals(name)) {
+ return entry.icon() + " " + name;
+ }
+ }
+ return name;
+ }
+
private void handleTextInput(KeyEvent ke, TextInputState active) {
if (ke.isDeleteBackward()) {
active.deleteBackward();
@@ -428,6 +477,10 @@ class SettingsPopup {
return tabNames.isEmpty() ? null : tabNames.get(startTabIndex);
}
+ String selectedSelectTab() {
+ return primaryTabNames.isEmpty() ? null :
primaryTabNames.get(selectTabIndex);
+ }
+
String selectedLogPin() {
return LOG_PIN_OPTIONS[logPinIndex];
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiSettings.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiSettings.java
index b3a70784e4da..005ad20022eb 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiSettings.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiSettings.java
@@ -29,6 +29,7 @@ final class TuiSettings {
static final String PROP_THEME = "camel.tui.theme";
static final String PROP_START_TAB = "camel.tui.startTab";
+ static final String PROP_SELECT_TAB = "camel.tui.selectTab";
static final String PROP_LOG_PIN = "camel.tui.logPin";
static final String PROP_DEFAULT_FOLDER = "camel.tui.defaultFolder";
static final String PROP_RATE_PER = "camel.tui.ratePer";
@@ -38,6 +39,7 @@ final class TuiSettings {
private String themeId;
private String startTab;
+ private String selectTab;
private String logPin;
private String ratePer;
private String defaultFolder;
@@ -61,6 +63,14 @@ final class TuiSettings {
this.startTab = startTab;
}
+ String getSelectTab() {
+ return selectTab;
+ }
+
+ void setSelectTab(String selectTab) {
+ this.selectTab = selectTab;
+ }
+
String getLogPin() {
return logPin;
}
@@ -119,6 +129,7 @@ final class TuiSettings {
try {
settings.themeId = trimToNull(TuiUserConfig.read(PROP_THEME));
settings.startTab = trimToNull(TuiUserConfig.read(PROP_START_TAB));
+ settings.selectTab =
trimToNull(TuiUserConfig.read(PROP_SELECT_TAB));
settings.logPin = trimToNull(TuiUserConfig.read(PROP_LOG_PIN));
settings.ratePer = trimToNull(TuiUserConfig.read(PROP_RATE_PER));
settings.defaultFolder =
trimToNull(TuiUserConfig.read(PROP_DEFAULT_FOLDER));
@@ -140,6 +151,7 @@ final class TuiSettings {
try {
TuiUserConfig.write(PROP_THEME, themeId);
TuiUserConfig.write(PROP_START_TAB, startTab);
+ TuiUserConfig.write(PROP_SELECT_TAB, selectTab);
TuiUserConfig.write(PROP_LOG_PIN, logPin);
TuiUserConfig.write(PROP_RATE_PER, ratePer);
TuiUserConfig.write(PROP_DEFAULT_FOLDER, defaultFolder);
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopupTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopupTest.java
index 99f85c393edd..abfc218bc188 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopupTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopupTest.java
@@ -136,7 +136,8 @@ class SettingsPopupTest {
popup.handleKeyEvent(key(KeyCode.DOWN));
popup.handleKeyEvent(key(KeyCode.DOWN));
popup.handleKeyEvent(key(KeyCode.DOWN));
- assertEquals(4, popup.selectedRow());
+ popup.handleKeyEvent(key(KeyCode.DOWN));
+ assertEquals(5, popup.selectedRow());
popup.handleKeyEvent(KeyEvent.ofChar('/'));
popup.handleKeyEvent(KeyEvent.ofChar('a'));
assertEquals("/a", popup.folderText());
@@ -153,7 +154,8 @@ class SettingsPopupTest {
popup.handleKeyEvent(key(KeyCode.DOWN));
popup.handleKeyEvent(key(KeyCode.DOWN));
popup.handleKeyEvent(key(KeyCode.DOWN));
- assertEquals(4, popup.selectedRow());
+ popup.handleKeyEvent(key(KeyCode.DOWN));
+ assertEquals(5, popup.selectedRow());
popup.handleKeyEvent(KeyEvent.ofChar(0x01));
popup.handleKeyEvent(KeyEvent.ofChar(0x00));
popup.handleKeyEvent(KeyEvent.ofChar('x'));
@@ -173,7 +175,8 @@ class SettingsPopupTest {
popup.handleKeyEvent(key(KeyCode.DOWN));
popup.handleKeyEvent(key(KeyCode.DOWN));
popup.handleKeyEvent(key(KeyCode.DOWN));
- assertEquals(5, popup.selectedRow());
+ popup.handleKeyEvent(key(KeyCode.DOWN));
+ assertEquals(6, popup.selectedRow());
assertEquals("auto", popup.selectedAiProvider());
popup.handleKeyEvent(KeyEvent.ofChar(' '));
assertEquals("ollama", popup.selectedAiProvider());
@@ -215,6 +218,7 @@ class SettingsPopupTest {
popup.handleKeyEvent(key(KeyCode.DOWN));
popup.handleKeyEvent(KeyEvent.ofChar(' ')); // starting tab Overview
-> Health
assertEquals("Health", popup.selectedStartTab());
+ popup.handleKeyEvent(key(KeyCode.DOWN)); // select tab
popup.handleKeyEvent(key(KeyCode.DOWN)); // log pin
popup.handleKeyEvent(key(KeyCode.DOWN)); // rate per
popup.handleKeyEvent(key(KeyCode.DOWN)); // folder