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 fd1a81074fa4 Camel TUI: auto-skip empty middle folders when browsing
fd1a81074fa4 is described below
commit fd1a81074fa4fdaaf8b876a7ab8eea6d30c1d576
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Sep 2 10:32:51 2026 +0200
Camel TUI: auto-skip empty middle folders when browsing
In the folder/file browsers, navigating forward into a folder that has
no files and exactly one sub folder now auto-descends through it, and
".."/Backspace auto-ascends back past those empty middle folders in one
step. This is common in Java projects (e.g. src/main/java/com/foo/).
Applied consistently to the Open Project popup (FolderBrowser), the
Ctrl+F Browse Files popup (FilesBrowser) and the Source tab (SourceTab).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../dsl/jbang/core/commands/tui/FilesBrowser.java | 80 ++++++++++++++++++++--
.../dsl/jbang/core/commands/tui/FolderBrowser.java | 39 ++++++++++-
.../dsl/jbang/core/commands/tui/SourceTab.java | 74 ++++++++++++++++++--
3 files changed, 180 insertions(+), 13 deletions(-)
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 ddf3c62baa13..3a70c9a7042a 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
@@ -92,6 +92,10 @@ class FilesBrowser {
}
private boolean loadDirectory(Path dir) {
+ return loadDirectory(dir, null);
+ }
+
+ private boolean loadDirectory(Path dir, String selectName) {
List<FileEntry> dirs = new ArrayList<>();
List<FileEntry> files = new ArrayList<>();
try (var stream = Files.list(dir)) {
@@ -117,6 +121,12 @@ class FilesBrowser {
dirs.sort(Comparator.comparing(FileEntry::name,
String.CASE_INSENSITIVE_ORDER));
files.sort(Comparator.comparing(FileEntry::name,
String.CASE_INSENSITIVE_ORDER));
+ // auto-descend through empty middle folders (no files and exactly one
sub folder)
+ // when navigating forward (not when restoring position while
navigating back)
+ if (selectName == null && files.isEmpty() && dirs.size() == 1) {
+ return loadDirectory(Path.of(dirs.get(0).path()));
+ }
+
List<FileEntry> found = new ArrayList<>();
if (!dir.equals(rootDir)) {
found.add(new FileEntry(TuiIcons.FOLDER, "..", -1,
dir.getParent().toString(), true));
@@ -128,11 +138,63 @@ class FilesBrowser {
return false;
}
entries = found;
- listState.select(0);
+ 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;
return true;
}
+ private void navigateBack() {
+ if (currentDir == null || currentDir.equals(rootDir)) {
+ return;
+ }
+ Path child = currentDir;
+ Path parent = currentDir.getParent();
+ // skip back through empty middle folders (parent has no files and
only this one sub folder),
+ // but never above the root directory
+ while (parent != null && !parent.equals(rootDir) && parent.getParent()
!= null
+ && isEmptyMiddleFolder(parent)) {
+ child = parent;
+ parent = parent.getParent();
+ }
+ loadDirectory(parent, child.getFileName().toString());
+ }
+
+ private boolean isEmptyMiddleFolder(Path dir) {
+ int dirCount = 0;
+ try (var stream = Files.list(dir)) {
+ var it = stream.iterator();
+ while (it.hasNext()) {
+ Path p = it.next();
+ String name = p.getFileName().toString();
+ if (Files.isDirectory(p)) {
+ if (name.startsWith(".")) {
+ // hidden directories are not shown
+ continue;
+ }
+ dirCount++;
+ if (dirCount > 1) {
+ return false;
+ }
+ } else if (Files.isRegularFile(p)) {
+ // has at least one visible file
+ return false;
+ }
+ }
+ } catch (IOException e) {
+ return false;
+ }
+ return dirCount == 1;
+ }
+
boolean handleMouseEvent(MouseEvent me) {
if (sourceViewer.isVisible()) {
return sourceViewer.handleMouseEvent(me);
@@ -152,7 +214,11 @@ class FilesBrowser {
listState.select(clicked);
FileEntry entry = entries.get(clicked);
if (entry.directory()) {
- loadDirectory(Path.of(entry.path()));
+ if ("..".equals(entry.name())) {
+ navigateBack();
+ } else {
+ loadDirectory(Path.of(entry.path()));
+ }
} else {
sourceViewer.loadFile(Path.of(entry.path()));
}
@@ -206,9 +272,7 @@ class FilesBrowser {
return true;
}
if (ke.isDeleteBackward()) {
- if (currentDir != null && !currentDir.equals(rootDir)) {
- loadDirectory(currentDir.getParent());
- }
+ navigateBack();
return true;
}
if (ke.isConfirm()) {
@@ -216,7 +280,11 @@ class FilesBrowser {
if (sel != null && sel < entries.size()) {
FileEntry entry = entries.get(sel);
if (entry.directory()) {
- loadDirectory(Path.of(entry.path()));
+ if ("..".equals(entry.name())) {
+ navigateBack();
+ } else {
+ loadDirectory(Path.of(entry.path()));
+ }
} else {
sourceViewer.loadFile(Path.of(entry.path()));
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FolderBrowser.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FolderBrowser.java
index 7a535e0a6965..afa358b68989 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FolderBrowser.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FolderBrowser.java
@@ -123,6 +123,12 @@ class FolderBrowser {
dirs.sort(Comparator.comparing(DirEntry::name,
String.CASE_INSENSITIVE_ORDER));
files.sort(Comparator.comparing(DirEntry::name,
String.CASE_INSENSITIVE_ORDER));
+ // auto-descend through empty middle folders (no files and exactly one
sub folder)
+ // when navigating forward (not when restoring position while
navigating back)
+ if (selectName == null && files.isEmpty() && dirs.size() == 1) {
+ return loadDirectory(Path.of(dirs.get(0).path()));
+ }
+
List<DirEntry> found = new ArrayList<>();
Path parent = dir.getParent();
if (parent != null) {
@@ -152,13 +158,42 @@ class FolderBrowser {
}
private void navigateBack() {
- String childName = currentDir.getFileName().toString();
- if (loadDirectory(currentDir.getParent(), childName)) {
+ Path child = currentDir;
+ Path parent = currentDir.getParent();
+ // skip back through empty middle folders (parent has no files and
only this one sub folder)
+ while (parent != null && parent.getParent() != null &&
isEmptyMiddleFolder(parent)) {
+ child = parent;
+ parent = parent.getParent();
+ }
+ String childName = child.getFileName().toString();
+ if (loadDirectory(parent, childName)) {
int savedOffset = offsetStack.isEmpty() ? 0 : offsetStack.pop();
listState.setOffset(savedOffset);
}
}
+ private boolean isEmptyMiddleFolder(Path dir) {
+ int dirCount = 0;
+ try (var stream = Files.list(dir)) {
+ var it = stream.filter(p ->
!p.getFileName().toString().startsWith(".")).iterator();
+ while (it.hasNext()) {
+ Path p = it.next();
+ if (Files.isDirectory(p)) {
+ dirCount++;
+ if (dirCount > 1) {
+ return false;
+ }
+ } else {
+ // has at least one file
+ return false;
+ }
+ }
+ } catch (IOException e) {
+ return false;
+ }
+ return dirCount == 1;
+ }
+
boolean handleMouseEvent(MouseEvent me) {
if (sourceViewer.isVisible()) {
return sourceViewer.handleMouseEvent(me);
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
index 1f568084bbe5..9093fd8e583b 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
@@ -588,6 +588,10 @@ class SourceTab extends AbstractTab {
}
private boolean loadDirectory(Path dir) {
+ return loadDirectory(dir, null);
+ }
+
+ private boolean loadDirectory(Path dir, String selectName) {
List<FilesBrowser.FileEntry> dirs = new ArrayList<>();
List<FilesBrowser.FileEntry> files = new ArrayList<>();
try (var stream = Files.list(dir)) {
@@ -613,6 +617,12 @@ class SourceTab extends AbstractTab {
dirs.sort(Comparator.comparing(FilesBrowser.FileEntry::name,
String.CASE_INSENSITIVE_ORDER));
files.sort(Comparator.comparing(FilesBrowser.FileEntry::name,
String.CASE_INSENSITIVE_ORDER));
+ // auto-descend through empty middle folders (no files and exactly one
sub folder)
+ // when navigating forward (not when restoring position while
navigating back)
+ if (selectName == null && files.isEmpty() && dirs.size() == 1) {
+ return loadDirectory(Path.of(dirs.get(0).path()));
+ }
+
List<FilesBrowser.FileEntry> found = new ArrayList<>();
if (!dir.equals(rootDir)) {
found.add(new FilesBrowser.FileEntry(TuiIcons.FOLDER, "..", -1,
dir.getParent().toString(), true));
@@ -624,12 +634,64 @@ class SourceTab extends AbstractTab {
return false;
}
entries = found;
- listState.select(0);
+ 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;
buildRouteIndex();
return true;
}
+ private void navigateBack() {
+ if (currentDir == null || currentDir.equals(rootDir)) {
+ return;
+ }
+ Path child = currentDir;
+ Path parent = currentDir.getParent();
+ // skip back through empty middle folders (parent has no files and
only this one sub folder),
+ // but never above the root directory
+ while (parent != null && !parent.equals(rootDir) && parent.getParent()
!= null
+ && isEmptyMiddleFolder(parent)) {
+ child = parent;
+ parent = parent.getParent();
+ }
+ loadDirectory(parent, child.getFileName().toString());
+ }
+
+ private boolean isEmptyMiddleFolder(Path dir) {
+ int dirCount = 0;
+ try (var stream = Files.list(dir)) {
+ var it = stream.iterator();
+ while (it.hasNext()) {
+ Path p = it.next();
+ String name = p.getFileName().toString();
+ if (Files.isDirectory(p)) {
+ if (name.startsWith(".")) {
+ // hidden directories are not shown
+ continue;
+ }
+ dirCount++;
+ if (dirCount > 1) {
+ return false;
+ }
+ } else if (Files.isRegularFile(p)) {
+ // has at least one visible file
+ return false;
+ }
+ }
+ } catch (IOException e) {
+ return false;
+ }
+ return dirCount == 1;
+ }
+
private boolean handleFileListKey(KeyEvent ke) {
if (ke.isUp()) {
listState.selectPrevious();
@@ -660,9 +722,7 @@ class SourceTab extends AbstractTab {
return true;
}
if (ke.isDeleteBackward()) {
- if (currentDir != null && !currentDir.equals(rootDir)) {
- loadDirectory(currentDir.getParent());
- }
+ navigateBack();
return true;
}
if (ke.isConfirm()) {
@@ -687,7 +747,11 @@ class SourceTab extends AbstractTab {
if (sel != null && sel < entries.size()) {
FilesBrowser.FileEntry entry = entries.get(sel);
if (entry.directory()) {
- loadDirectory(Path.of(entry.path()));
+ if ("..".equals(entry.name())) {
+ navigateBack();
+ } else {
+ loadDirectory(Path.of(entry.path()));
+ }
} else {
Path filePath = Path.of(entry.path());
if (isCamelSourceFile(filePath)) {