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 9f2b1f71d92c CAMEL-23831: Add folder browser dialog and persist last
folder in camel-tui CAMEL-23831: Fix chart axis labels to use round base-10
numbers in camel-tui CAMEL-23831: Reduce UI freeze when switching integrations
in camel-tui - Change default refresh interval from 100ms to 500ms - On
Overview tab refresh all integration PIDs every cycle (not just selected) -
Remove eager data loading from onIntegrationChanged in 6 tabs - Remove eager
diagram preloading on integration s [...]
9f2b1f71d92c is described below
commit 9f2b1f71d92c0b126b6d8361190d45ec548fea35
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Jul 3 12:53:41 2026 +0200
CAMEL-23831: Add folder browser dialog and persist last folder in camel-tui
CAMEL-23831: Fix chart axis labels to use round base-10 numbers in camel-tui
CAMEL-23831: Reduce UI freeze when switching integrations in camel-tui
- Change default refresh interval from 100ms to 500ms
- On Overview tab refresh all integration PIDs every cycle (not just
selected)
- Remove eager data loading from onIntegrationChanged in 6 tabs
- Remove eager diagram preloading on integration switch
CAMEL-23831: Fix History tab scroll and add Home/End navigation in camel-tui
CAMEL-23831: Restore scroll position when navigating back in folder browser
in camel-tui
Co-Authored-By: Claude <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../dsl/jbang/core/commands/tui/ActionsPopup.java | 72 ++++-
.../dsl/jbang/core/commands/tui/BeansTab.java | 1 -
.../dsl/jbang/core/commands/tui/BrowseTab.java | 1 -
.../dsl/jbang/core/commands/tui/CamelMonitor.java | 5 +-
.../dsl/jbang/core/commands/tui/ClasspathTab.java | 1 -
.../core/commands/tui/DataRefreshService.java | 5 +-
.../dsl/jbang/core/commands/tui/FilesBrowser.java | 24 +-
.../dsl/jbang/core/commands/tui/FolderBrowser.java | 312 +++++++++++++++++++++
.../jbang/core/commands/tui/HeapHistogramTab.java | 1 -
.../dsl/jbang/core/commands/tui/HistoryTab.java | 41 ++-
.../dsl/jbang/core/commands/tui/OverviewTab.java | 48 +++-
.../dsl/jbang/core/commands/tui/StartupTab.java | 1 -
.../dsl/jbang/core/commands/tui/TabRegistry.java | 4 -
.../dsl/jbang/core/commands/tui/ThreadsTab.java | 1 -
14 files changed, 475 insertions(+), 42 deletions(-)
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 7e5d53dafa20..a9ce44650612 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
@@ -60,9 +60,11 @@ import dev.tamboui.widgets.list.ScrollMode;
import dev.tamboui.widgets.paragraph.Paragraph;
import org.apache.camel.catalog.CamelCatalog;
import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
import org.apache.camel.dsl.jbang.core.common.ExampleHelper;
import org.apache.camel.dsl.jbang.core.common.LauncherHelper;
import org.apache.camel.dsl.jbang.core.common.PathUtils;
+import org.apache.camel.dsl.jbang.core.common.Printer;
import org.apache.camel.util.json.JsonArray;
import org.apache.camel.util.json.JsonObject;
import org.apache.camel.util.json.Jsoner;
@@ -158,11 +160,13 @@ class ActionsPopup {
private int infraImplIndex;
private TextInputState infraPortState;
+ private static final String PROP_LAST_FOLDER = "camel.tui.lastFolder";
private boolean showFolderInput;
private TextInputState folderInputState;
private final List<String> folderHistory = new ArrayList<>();
private int folderHistoryIndex = -1;
private String selectedFolder;
+ private final FolderBrowser folderBrowser = new FolderBrowser();
private final McpLogPopup mcpLogPopup = new McpLogPopup();
private final AiLogPopup aiLogPopup = new AiLogPopup();
@@ -327,7 +331,8 @@ class ActionsPopup {
}
boolean isVisible() {
- return showActionsMenu || showGotoPopup || showExampleBrowser ||
showFolderInput || runOptionsForm.isVisible()
+ return showActionsMenu || showGotoPopup || showExampleBrowser ||
showFolderInput || folderBrowser.isVisible()
+ || runOptionsForm.isVisible()
|| showDocPicker || showDocViewer
|| showInfraBrowser || showInfraPortDialog
|| mcpLogPopup.isVisible() || aiLogPopup.isVisible() ||
doctorPopup.isVisible()
@@ -378,8 +383,8 @@ class ActionsPopup {
labels.add("───");
// Group 1: User Actions
labels.add("Send Message");
- labels.add("Run an example...");
- labels.add("Run from folder...");
+ labels.add("Run an Example...");
+ labels.add("Run from Folder...");
labels.add("Run Dev/Infra Service...");
labels.add("Browse Files...");
labels.add("Stop All");
@@ -550,12 +555,23 @@ class ActionsPopup {
}
return true;
}
+ if (folderBrowser.isVisible()) {
+ folderBrowser.handleKeyEvent(ke);
+ if (!folderBrowser.isVisible() && !showFolderInput) {
+ showFolderInput = true;
+ }
+ return true;
+ }
if (showFolderInput) {
if (ke.isCancel()) {
showFolderInput = false;
showActionsMenu = true;
} else if (ke.isConfirm()) {
confirmFolderInput();
+ } else if (ke.isKey(KeyCode.TAB)) {
+ String current = folderInputState.text().trim();
+ showFolderInput = false;
+ folderBrowser.open(current);
} else if (ke.isUp()) {
navigateFolderHistory(-1);
} else if (ke.isDown()) {
@@ -805,6 +821,9 @@ class ActionsPopup {
if (showInfraPortDialog) {
renderInfraPortDialog(frame, area);
}
+ if (folderBrowser.isVisible()) {
+ folderBrowser.render(frame, area);
+ }
if (showFolderInput) {
renderFolderInput(frame, area);
}
@@ -880,10 +899,15 @@ class ActionsPopup {
runOptionsForm.renderFooter(spans);
return;
}
+ if (folderBrowser.isVisible()) {
+ folderBrowser.renderFooter(spans);
+ return;
+ }
if (showFolderInput) {
if (!folderHistory.isEmpty()) {
hint(spans, "↑↓", "history");
}
+ hint(spans, "Tab", "browse");
hint(spans, "Enter", "run...");
hintLast(spans, "Esc", "back");
return;
@@ -963,8 +987,8 @@ class ActionsPopup {
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 an Example..."));
+ items.add(ListItem.from(" 📂 Run from Folder..."));
items.add(ListItem.from(" 🔧 Run Dev/Infra Service..."));
items.add(hasSelection
? ListItem.from(" 📁 Browse Files...")
@@ -1437,8 +1461,16 @@ class ActionsPopup {
private void openFolderInput() {
showActionsMenu = false;
showFolderInput = true;
- folderInputState = new TextInputState("");
+ String lastFolder = loadLastFolder();
+ if (lastFolder == null) {
+ lastFolder = System.getProperty("user.dir");
+ }
+ folderInputState = new TextInputState(lastFolder != null ? lastFolder
: "");
folderHistoryIndex = -1;
+ folderBrowser.setOnSelect(path -> {
+ folderInputState = new TextInputState(path);
+ showFolderInput = true;
+ });
}
private void confirmFolderInput() {
@@ -1462,10 +1494,36 @@ class ActionsPopup {
}
selectedFolder = folder;
showFolderInput = false;
+ persistLastFolder(folder);
String displayName = dirPath.getFileName().toString();
runOptionsForm.open(displayName, displayName, false, true);
}
+ private static String loadLastFolder() {
+ String[] holder = { null };
+ try {
+ CommandLineHelper.loadProperties(props -> {
+ holder[0] = props.getProperty(PROP_LAST_FOLDER);
+ });
+ } catch (RuntimeException e) {
+ // ignore
+ }
+ return holder[0];
+ }
+
+ private static void persistLastFolder(String folder) {
+ try {
+ CommandLineHelper.createPropertyFile(false);
+ CommandLineHelper.loadProperties(props -> {
+ props.setProperty(PROP_LAST_FOLDER, folder);
+ CommandLineHelper.storeProperties(props,
+ new Printer.QuietPrinter(new
Printer.SystemOutPrinter()), false);
+ });
+ } catch (Exception e) {
+ // ignore
+ }
+ }
+
private void navigateFolderHistory(int direction) {
if (folderHistory.isEmpty()) {
return;
@@ -1520,7 +1578,7 @@ class ActionsPopup {
Block block = Block.builder()
.borderType(BorderType.ROUNDED).borders(Borders.ALL)
- .title(" Run from folder ")
+ .title(" Run from Folder ")
.build();
frame.renderWidget(block, popup);
Rect inner = block.inner(popup);
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 cad5f1964ee3..072850109e1a 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
@@ -84,7 +84,6 @@ class BeansTab extends AbstractTableTab {
showDetail = false;
detailScroll = 0;
lastPid = null;
- loadBeans();
}
@Override
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 aa1780fd4f47..480f707f5e9d 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
@@ -110,7 +110,6 @@ class BrowseTab extends AbstractTab {
view = VIEW_ENDPOINTS;
detailScroll = 0;
lastPid = null;
- loadEndpoints();
}
@Override
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 fa6ec335c4ea..6114ad04fbfd 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
@@ -75,7 +75,7 @@ import static
org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.hint;
public class CamelMonitor extends CamelCommand {
private static final Logger LOG =
System.getLogger(CamelMonitor.class.getName());
- private static final long DEFAULT_REFRESH_MS = 100;
+ private static final long DEFAULT_REFRESH_MS = 500;
// Compact tab bar (10 labels + 9 "|" dividers) needs 88 chars — that is
the true minimum
private static final int MIN_WIDTH = 88;
@@ -1789,9 +1789,6 @@ public class CamelMonitor extends CamelCommand {
if (ctx.selectedPid != null && !isInfraSelected()) {
IntegrationInfo selInfo = findSelectedIntegration();
if (selInfo != null) {
- if (selInfo.directory != null && !selInfo.directory.isEmpty())
{
- hint(spans, "f", "files");
- }
hint(spans, "p", selInfo.routeStarted > 0 ? "stop routes" :
"start routes");
}
}
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 df85defa5013..f418f2aca1b1 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
@@ -90,7 +90,6 @@ class ClasspathTab extends AbstractTab {
lastPid = null;
errorMessage = null;
dataLoaded = false;
- loadClasspath();
}
@Override
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 4d449c991268..7285ed4337fc 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
@@ -240,7 +240,8 @@ class DataRefreshService {
}
List<Long> refreshPids;
- if (!fullScan && ctx.selectedPid != null) {
+ boolean overviewTab = refreshCtx.selectedTab() ==
TabRegistry.TAB_OVERVIEW;
+ if (!fullScan && ctx.selectedPid != null && !overviewTab) {
try {
refreshPids = List.of(Long.parseLong(ctx.selectedPid));
} catch (NumberFormatException e) {
@@ -267,7 +268,7 @@ class DataRefreshService {
}
}
}
- if (!fullScan && ctx.selectedPid != null) {
+ if (!fullScan && ctx.selectedPid != null && !overviewTab) {
boolean checkLiveness = now - lastLivenessCheckTime >=
LIVENESS_CHECK_INTERVAL_MS;
if (checkLiveness) {
lastLivenessCheckTime = now;
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 26f1e5309ab9..47ecfa8d48d7 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
@@ -113,7 +113,7 @@ class FilesBrowser {
stream.limit(200)
.forEach(p -> {
String name = p.getFileName().toString();
- if (Files.isDirectory(p)) {
+ if (Files.isDirectory(p) && !name.startsWith(".")) {
dirs.add(new FileEntry("📁", name, -1,
p.toString(), true));
} else if (Files.isRegularFile(p)) {
String emoji = fileEmoji(p);
@@ -171,6 +171,26 @@ class FilesBrowser {
listState.selectNext(entries.size());
return true;
}
+ if (ke.isPageUp()) {
+ for (int i = 0; i < 20; i++) {
+ listState.selectPrevious();
+ }
+ return true;
+ }
+ if (ke.isPageDown()) {
+ for (int i = 0; i < 20; i++) {
+ listState.selectNext(entries.size());
+ }
+ return true;
+ }
+ if (ke.isHome()) {
+ listState.select(0);
+ return true;
+ }
+ if (ke.isEnd()) {
+ listState.select(entries.size() - 1);
+ return true;
+ }
if (ke.isDeleteBackward()) {
if (currentDir != null && !currentDir.equals(rootDir)) {
loadDirectory(currentDir.getParent());
@@ -249,7 +269,7 @@ class FilesBrowser {
.items(items)
.highlightStyle(Theme.selectionBg())
.highlightSymbol("")
- .scrollMode(ScrollMode.NONE)
+ .scrollMode(ScrollMode.AUTO_SCROLL)
.block(Block.builder()
.borderType(BorderType.ROUNDED).borders(Borders.ALL)
.title(Title.from(Line
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
new file mode 100644
index 000000000000..8322f48b5851
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FolderBrowser.java
@@ -0,0 +1,312 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Deque;
+import java.util.List;
+import java.util.function.Consumer;
+
+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;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.widgets.Clear;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Borders;
+import dev.tamboui.widgets.block.Title;
+import dev.tamboui.widgets.list.ListItem;
+import dev.tamboui.widgets.list.ListState;
+import dev.tamboui.widgets.list.ListWidget;
+import dev.tamboui.widgets.list.ScrollMode;
+
+class FolderBrowser {
+
+ record DirEntry(String name, String path) {
+ }
+
+ private boolean visible;
+ private Path currentDir;
+ private final ListState listState = new ListState();
+ private final Deque<Integer> offsetStack = new ArrayDeque<>();
+ private List<DirEntry> entries = Collections.emptyList();
+ private Consumer<String> onSelect;
+ private char lastJumpChar;
+ private int lastJumpIndex = -1;
+
+ boolean isVisible() {
+ return visible;
+ }
+
+ void setOnSelect(Consumer<String> onSelect) {
+ this.onSelect = onSelect;
+ }
+
+ void open(String startPath) {
+ Path start = null;
+ if (startPath != null && !startPath.isEmpty()) {
+ String resolved = startPath;
+ if (resolved.startsWith("~")) {
+ resolved = System.getProperty("user.home") +
resolved.substring(1);
+ }
+ Path p = Path.of(resolved);
+ if (Files.isDirectory(p)) {
+ start = p;
+ }
+ }
+ if (start == null) {
+ start = Path.of(System.getProperty("user.dir"));
+ }
+ if (loadDirectory(start)) {
+ visible = true;
+ offsetStack.clear();
+ }
+ }
+
+ private boolean loadDirectory(Path dir) {
+ return loadDirectory(dir, null);
+ }
+
+ private boolean loadDirectory(Path dir, String selectName) {
+ List<DirEntry> dirs = new ArrayList<>();
+ try (var stream = Files.list(dir)) {
+ stream.filter(Files::isDirectory)
+ .filter(p -> !p.getFileName().toString().startsWith("."))
+ .limit(200)
+ .forEach(p -> dirs.add(new
DirEntry(p.getFileName().toString(), p.toString())));
+ } catch (IOException e) {
+ return false;
+ }
+ dirs.sort(Comparator.comparing(DirEntry::name,
String.CASE_INSENSITIVE_ORDER));
+
+ List<DirEntry> found = new ArrayList<>();
+ Path parent = dir.getParent();
+ if (parent != null) {
+ found.add(new DirEntry("..", parent.toString()));
+ }
+ found.addAll(dirs);
+
+ if (found.isEmpty()) {
+ return false;
+ }
+ entries = found;
+ int sel = 0;
+ if (selectName != null) {
+ for (int i = 0; i < found.size(); i++) {
+ if (found.get(i).name().equals(selectName)) {
+ sel = i;
+ break;
+ }
+ }
+ }
+ listState.select(sel);
+ currentDir = dir;
+ lastJumpChar = 0;
+ lastJumpIndex = -1;
+ return true;
+ }
+
+ private void navigateBack() {
+ String childName = currentDir.getFileName().toString();
+ if (loadDirectory(currentDir.getParent(), childName)) {
+ int savedOffset = offsetStack.isEmpty() ? 0 : offsetStack.pop();
+ listState.setOffset(savedOffset);
+ }
+ }
+
+ boolean handleKeyEvent(KeyEvent ke) {
+ if (ke.isCancel()) {
+ visible = false;
+ return true;
+ }
+ if (ke.isUp()) {
+ listState.selectPrevious();
+ lastJumpChar = 0;
+ lastJumpIndex = -1;
+ return true;
+ }
+ if (ke.isDown()) {
+ listState.selectNext(entries.size());
+ lastJumpChar = 0;
+ lastJumpIndex = -1;
+ return true;
+ }
+ if (ke.isPageUp()) {
+ for (int i = 0; i < 20; i++) {
+ listState.selectPrevious();
+ }
+ lastJumpChar = 0;
+ lastJumpIndex = -1;
+ return true;
+ }
+ if (ke.isPageDown()) {
+ for (int i = 0; i < 20; i++) {
+ listState.selectNext(entries.size());
+ }
+ lastJumpChar = 0;
+ lastJumpIndex = -1;
+ return true;
+ }
+ if (ke.isHome()) {
+ listState.select(0);
+ lastJumpChar = 0;
+ lastJumpIndex = -1;
+ return true;
+ }
+ if (ke.isEnd()) {
+ listState.select(entries.size() - 1);
+ lastJumpChar = 0;
+ lastJumpIndex = -1;
+ return true;
+ }
+ if (ke.isDeleteBackward()) {
+ if (currentDir != null && currentDir.getParent() != null) {
+ navigateBack();
+ }
+ return true;
+ }
+ if (ke.isConfirm()) {
+ Integer sel = listState.selected();
+ if (sel != null && sel < entries.size()) {
+ DirEntry entry = entries.get(sel);
+ if ("..".equals(entry.name()) && currentDir != null) {
+ navigateBack();
+ } else {
+ offsetStack.push(listState.offset());
+ loadDirectory(Path.of(entry.path()));
+ }
+ }
+ return true;
+ }
+ if (ke.isKey(KeyCode.TAB)) {
+ visible = false;
+ if (onSelect != null && currentDir != null) {
+ onSelect.accept(currentDir.toString());
+ }
+ return true;
+ }
+ if (ke.code() == KeyCode.CHAR) {
+ boolean reverse = Character.isUpperCase(ke.string().charAt(0));
+ char c = Character.toLowerCase(ke.string().charAt(0));
+ int found = -1;
+ if (reverse) {
+ int startFrom = c == lastJumpChar && lastJumpIndex >= 0 ?
lastJumpIndex - 1 : entries.size() - 1;
+ for (int i = startFrom; i >= 0; i--) {
+ String name = entries.get(i).name();
+ if (!name.isEmpty() &&
Character.toLowerCase(name.charAt(0)) == c) {
+ found = i;
+ break;
+ }
+ }
+ if (found < 0) {
+ for (int i = entries.size() - 1; i > startFrom; i--) {
+ String name = entries.get(i).name();
+ if (!name.isEmpty() &&
Character.toLowerCase(name.charAt(0)) == c) {
+ found = i;
+ break;
+ }
+ }
+ }
+ } else {
+ int startFrom = c == lastJumpChar && lastJumpIndex >= 0 ?
lastJumpIndex + 1 : 0;
+ for (int i = startFrom; i < entries.size(); i++) {
+ String name = entries.get(i).name();
+ if (!name.isEmpty() &&
Character.toLowerCase(name.charAt(0)) == c) {
+ found = i;
+ break;
+ }
+ }
+ if (found < 0) {
+ for (int i = 0; i < startFrom && i < entries.size(); i++) {
+ String name = entries.get(i).name();
+ if (!name.isEmpty() &&
Character.toLowerCase(name.charAt(0)) == c) {
+ found = i;
+ break;
+ }
+ }
+ }
+ }
+ if (found >= 0) {
+ listState.select(found);
+ lastJumpChar = c;
+ lastJumpIndex = found;
+ }
+ return true;
+ }
+ return true;
+ }
+
+ void render(Frame frame, Rect area) {
+ if (entries.isEmpty()) {
+ visible = false;
+ return;
+ }
+
+ String dirLabel = currentDir != null ? currentDir.toString() : "";
+ String popupTitle = " 📂 " + dirLabel + " ";
+
+ int nameWidth = entries.stream().mapToInt(e ->
e.name().length()).max().orElse(10);
+ int itemWidth = 6 + nameWidth;
+ int titleWidth = popupTitle.length() + 4;
+ int popupW = Math.min(area.width() - 4, Math.max(50,
Math.max(itemWidth + 4, titleWidth)));
+ int popupH = Math.min(area.height() - 4, Math.max(12, entries.size() +
2));
+
+ int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
+ int y = area.top() + 2;
+ Rect popup = new Rect(x, y, Math.min(popupW, area.width()),
Math.min(popupH, area.height() - 2));
+
+ frame.renderWidget(Clear.INSTANCE, popup);
+
+ ListItem[] items = new ListItem[entries.size()];
+ for (int i = 0; i < entries.size(); i++) {
+ DirEntry entry = entries.get(i);
+ String label = " 📁 " + entry.name();
+ items[i] = ListItem.from(Line.from(Span.styled(label,
Style.EMPTY.fg(Color.CYAN))));
+ }
+
+ ListWidget list = ListWidget.builder()
+ .items(items)
+ .highlightStyle(Theme.selectionBg())
+ .highlightSymbol("")
+ .scrollMode(ScrollMode.AUTO_SCROLL)
+ .block(Block.builder()
+ .borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .title(Title.from(Line
+ .from(Span.styled(popupTitle,
Style.EMPTY.fg(Color.YELLOW).bold()))))
+ .build())
+ .build();
+ frame.renderStatefulWidget(list, popup, listState);
+ }
+
+ void renderFooter(List<Span> spans) {
+ TuiHelper.hint(spans, "↑↓", "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/HeapHistogramTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HeapHistogramTab.java
index 5a2004fd154b..f6b713975777 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
@@ -88,7 +88,6 @@ class HeapHistogramTab extends AbstractTableTab {
allEntries = Collections.emptyList();
classpathEntries = Collections.emptyList();
lastPid = null;
- loadHeapHistogram();
}
@Override
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 f9c960c90344..883325c325b7 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
@@ -129,6 +129,7 @@ class HistoryTab extends AbstractTab {
private final DragSplit detailSplit = new DragSplit();
volatile List<HistoryEntry> historyEntries = Collections.emptyList();
+ private volatile int historyVisibleCount;
private final TableState historyTableState = new TableState();
private boolean showHistoryProperties;
private boolean showHistoryVariables;
@@ -297,7 +298,7 @@ class HistoryTab extends AbstractTab {
} else {
if (showWaterfall) {
for (int i = 0; i < 10; i++) {
- historyTableState.selectNext(historyEntries.size());
+ historyTableState.selectNext(historyVisibleCount);
}
} else {
historyDetailScroll += 5;
@@ -305,6 +306,31 @@ class HistoryTab extends AbstractTab {
}
return true;
}
+ if (ke.isHome()) {
+ if (tracerActive && traceDetailView) {
+ traceStepTableState.selectFirst();
+ traceDetailScroll = 0;
+ } else if (tracerActive) {
+ traceTableState.selectFirst();
+ } else {
+ historyTableState.selectFirst();
+ historyDetailScroll = 0;
+ }
+ return true;
+ }
+ if (ke.isEnd()) {
+ if (tracerActive && traceDetailView) {
+ List<TraceEntry> steps =
getTraceStepsDepthFirst(traceSelectedExchangeId);
+ traceStepTableState.selectLast(steps.size());
+ traceDetailScroll = 0;
+ } else if (tracerActive) {
+ traceTableState.selectLast(traceSortedExchangeIds.size());
+ } else {
+ historyTableState.selectLast(historyVisibleCount);
+ historyDetailScroll = 0;
+ }
+ return true;
+ }
if (ke.isLeft()) {
if (tracerActive && traceDetailView && !traceWordWrap) {
traceDetailHScroll = Math.max(0, traceDetailHScroll - 4);
@@ -520,7 +546,7 @@ class HistoryTab extends AbstractTab {
}
}
if (!tracerActive) {
- if (handleTableClick(me, lastHistoryTableArea, historyTableState,
historyEntries.size())) {
+ if (handleTableClick(me, lastHistoryTableArea, historyTableState,
historyVisibleCount)) {
historyDetailScroll = 0;
return true;
}
@@ -570,7 +596,7 @@ class HistoryTab extends AbstractTab {
}
} else {
if (isInArea(me, lastHistoryTableArea) || showWaterfall) {
- historyTableState.selectNext(historyEntries.size());
+ historyTableState.selectNext(historyVisibleCount);
historyDetailScroll = 0;
} else {
historyDetailScroll += MOUSE_SCROLL_LINES;
@@ -612,7 +638,7 @@ class HistoryTab extends AbstractTab {
traceTableState.selectNext(exchangeIds.size());
}
} else {
- historyTableState.selectNext(historyEntries.size());
+ historyTableState.selectNext(historyVisibleCount);
historyDetailScroll = 0;
}
}
@@ -1210,6 +1236,8 @@ class HistoryTab extends AbstractTab {
.build();
lastTraceTableArea = area;
+ int traceVisibleRows = Math.max(0, area.height() - 3);
+ traceTableState.scrollToSelected(traceVisibleRows, rows);
frame.renderStatefulWidget(table, area, traceTableState);
renderTableScrollbar(frame, lastTraceTableArea, traceTableState,
tableScrollState,
traceSortedExchangeIds.size());
@@ -1239,6 +1267,8 @@ class HistoryTab extends AbstractTab {
= String.format(" Trace [%s] — %d steps ",
TuiHelper.truncate(traceSelectedExchangeId, 30), steps.size());
lastTraceStepArea = chunks.get(0);
detailSplit.setBorderPos(chunks.get(1).y());
+ int stepVisibleRows = Math.max(0, chunks.get(0).height() - 3);
+ traceStepTableState.scrollToSelected(stepVisibleRows, rows);
frame.renderStatefulWidget(
buildStepTable(rows, stepTitle, showDescription),
chunks.get(0), traceStepTableState);
renderTableScrollbar(frame, lastTraceStepArea, traceStepTableState,
traceStepScrollState,
@@ -1493,6 +1523,7 @@ class HistoryTab extends AbstractTab {
}
List<HistoryEntry> current = reorderHistoryDepthFirst(historyEntries);
+ historyVisibleCount = current.size();
historyTopPanelHeight = Math.max(3, Math.min(historyTopPanelHeight,
area.height() - 5));
List<Rect> chunks = Layout.vertical()
@@ -1514,6 +1545,8 @@ class HistoryTab extends AbstractTab {
Title historyTitle = buildHistoryTitle(current);
lastHistoryTableArea = chunks.get(0);
vSplit.setBorderPos(chunks.get(1).y());
+ int histVisibleRows = Math.max(0, chunks.get(0).height() - 3);
+ historyTableState.scrollToSelected(histVisibleRows, rows);
frame.renderStatefulWidget(
buildStepTable(rows, historyTitle, showDescription),
chunks.get(0), historyTableState);
renderTableScrollbar(frame, lastHistoryTableArea, historyTableState,
historyTableScrollState,
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 e6d8691d09d1..a33d0944f632 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
@@ -488,10 +488,11 @@ class OverviewTab extends AbstractTab {
}
}
- long maxTp = 0;
+ long rawMax = 0;
for (long v : mergedTotal) {
- maxTp = Math.max(maxTp, v);
+ rawMax = Math.max(rawMax, v);
}
+ long maxTp = roundUpNice(rawMax);
long curTp = mergedTotal[renderPoints - 1];
long curFailed = mergedFailed[renderPoints - 1];
long curOk = Math.max(0, curTp - curFailed);
@@ -561,17 +562,26 @@ class OverviewTab extends AbstractTab {
if (!vChunks.get(1).isEmpty()) {
int barInnerStartX = barChartArea.x();
int xAxisY = vChunks.get(1).y();
- int[][] markerIndices = {
- { 0, renderPoints },
- { renderPoints / 4, renderPoints - renderPoints / 4 },
- { renderPoints / 2, renderPoints / 2 },
- { 3 * renderPoints / 4, renderPoints / 4 },
- { renderPoints - 1, 0 }
- };
- for (int[] m : markerIndices) {
- int groupIdx = m[0];
- int secsAgo = m[1];
- String label = secsAgo == 0 ? "now" : "-" + secsAgo + "s";
+ int step;
+ if (renderPoints <= 20) {
+ step = 5;
+ } else if (renderPoints <= 80) {
+ step = 10;
+ } else {
+ step = 20;
+ }
+ // "now" label at the right edge
+ int nowX = barInnerStartX + (renderPoints - 1) * 2;
+ if (nowX + 3 <= barChartArea.right()) {
+ frame.buffer().setString(nowX, xAxisY, "now", dimStyle);
+ }
+ // round time markers from right to left
+ for (int s = step; s <= renderPoints; s += step) {
+ int groupIdx = renderPoints - 1 - s;
+ if (groupIdx < 0) {
+ break;
+ }
+ String label = "-" + s + "s";
int markerX = barInnerStartX + groupIdx * 2;
if (markerX + label.length() <= barChartArea.right()) {
frame.buffer().setString(markerX, xAxisY, label,
dimStyle);
@@ -882,6 +892,18 @@ class OverviewTab extends AbstractTab {
return sortStyle(column, sort);
}
+ private static long roundUpNice(long value) {
+ if (value <= 10) {
+ return 10;
+ }
+ long step = (long) Math.pow(10, Math.floor(Math.log10(value)));
+ long rounded = ((value + step - 1) / step) * step;
+ if (rounded % 2 != 0) {
+ rounded += step;
+ }
+ return rounded;
+ }
+
private static boolean hasReadmeInSourceDir(IntegrationInfo info) {
java.nio.file.Path srcDir = FilesBrowser.resolveSourceDirectory(info);
if (srcDir != null) {
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 386b080e6431..3f097462f4f1 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
@@ -139,7 +139,6 @@ class StartupTab extends AbstractTab {
scrollOffset = 0;
errorMessage = null;
dataLoaded = false;
- loadStartupData();
}
@Override
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 b5efb3570005..d8fd97204900 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
@@ -265,10 +265,6 @@ class TabRegistry {
dataService.otelSpans().set(List.of());
filesBrowser.reset();
-
- // Preload diagram data in background so it's ready when the user
switches tabs
- routesTab.preloadDiagram();
- diagramTab.preloadDiagram();
}
void navigateUp() {
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 c79e36215ac4..04aad8b40ad8 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
@@ -88,7 +88,6 @@ class ThreadsTab extends AbstractTableTab {
showTrace = false;
traceScroll = 0;
lastPid = null;
- loadThreads();
}
@Override