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 610731580ffe camel-tui: Use TAB to switch focus between master/detail
panels
610731580ffe is described below
commit 610731580ffe95af6437b810407cffc32fab58e5
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Jul 28 13:30:10 2026 +0200
camel-tui: Use TAB to switch focus between master/detail panels
Add consistent TAB focus switching to all tabs with master/detail
panels. The focused panel gets an accent border and the unfocused
panel gets a muted border. Up/Down and PgUp/PgDn operate on
whichever panel has focus.
Also fix BrowseTab to correctly parse the dev console JSON format
where headers are a JsonArray of {key,value,type} objects and body
is a JsonObject with {type,value} fields.
camel-tui: Expose detailFocused panel state to MCP tools
AI agents can now tell which panel has focus in master/detail tabs
via the detailFocused field in tui_get_state, tui_get_screen,
tui_navigate, and tui_send_keys responses.
camel-tui: Clean up dead infra service files so stopped services disappear
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../dsl/jbang/core/commands/tui/BeansTab.java | 66 ++++++++---
.../dsl/jbang/core/commands/tui/BrowseTab.java | 126 ++++++++++++++------
.../jbang/core/commands/tui/ConfigurationTab.java | 62 +++++++---
.../core/commands/tui/DataRefreshService.java | 7 ++
.../dsl/jbang/core/commands/tui/EndpointsTab.java | 77 ++++++++----
.../dsl/jbang/core/commands/tui/HistoryTab.java | 132 +++++++++++++++++----
.../dsl/jbang/core/commands/tui/McpFacade.java | 5 +
.../dsl/jbang/core/commands/tui/MonitorTab.java | 4 +
.../dsl/jbang/core/commands/tui/ThreadsTab.java | 64 +++++++---
.../jbang/core/commands/tui/TuiToolRegistry.java | 12 +-
10 files changed, 429 insertions(+), 126 deletions(-)
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 6ff0dde6a8c4..200eedef6b7e 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
@@ -35,6 +35,7 @@ import dev.tamboui.tui.event.MouseEvent;
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.input.TextInputState;
import dev.tamboui.widgets.paragraph.Paragraph;
import dev.tamboui.widgets.table.Cell;
@@ -55,6 +56,7 @@ class BeansTab extends AbstractTableTab {
private String filterTerm;
private List<BeanData> allBeans = Collections.emptyList();
private int detailScroll;
+ private boolean detailFocused;
private String lastPid;
BeansTab(MonitorContext ctx) {
@@ -132,13 +134,19 @@ class BeansTab extends AbstractTableTab {
@Override
protected boolean handleTabKeyEvent(KeyEvent ke) {
- if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) {
- detailScroll = Math.max(0, detailScroll - 10);
+ if (ke.isKey(KeyCode.TAB)) {
+ detailFocused = !detailFocused;
return true;
}
- if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) {
- detailScroll += 10;
- return true;
+ if (detailFocused) {
+ if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) {
+ detailScroll = Math.max(0, detailScroll - 10);
+ return true;
+ }
+ if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) {
+ detailScroll += 10;
+ return true;
+ }
}
if (ke.isCharIgnoreCase('f')) {
filterIndex = (filterIndex + 1) % filterModes().length;
@@ -149,15 +157,23 @@ class BeansTab extends AbstractTableTab {
@Override
public void navigateUp() {
- tableState.selectPrevious();
- detailScroll = 0;
+ if (detailFocused) {
+ detailScroll = Math.max(0, detailScroll - 1);
+ } else {
+ tableState.selectPrevious();
+ detailScroll = 0;
+ }
}
@Override
public void navigateDown() {
- List<BeanData> visible = sortedBeans();
- tableState.selectNext(visible.size());
- detailScroll = 0;
+ if (detailFocused) {
+ detailScroll++;
+ } else {
+ List<BeanData> visible = sortedBeans();
+ tableState.selectNext(visible.size());
+ detailScroll = 0;
+ }
}
@Override
@@ -211,6 +227,9 @@ class BeansTab extends AbstractTableTab {
? String.format(" Beans [%d] scope:%s filter:\"%s\" ",
visible.size(), mode, filterTerm)
: String.format(" Beans [%d] scope:%s ", visible.size(), mode);
+ Style tableBorderStyle = detailFocused ? Theme.muted() :
Style.EMPTY.fg(Theme.accent());
+ Style tableTitleStyle = detailFocused ? Style.EMPTY.fg(Theme.accent())
: Theme.title();
+
Table table = Table.builder()
.rows(rows)
.header(Row.from(
@@ -219,9 +238,11 @@ class BeansTab extends AbstractTableTab {
.widths(
Constraint.percentage(55),
Constraint.fill())
- .highlightStyle(Theme.selectionBg())
+ .highlightStyle(detailFocused ? Theme.selectionBg().dim() :
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(tableBorderStyle)
+ .title(Title.from(Line.from(Span.styled(title,
tableTitleStyle)))).build())
.build();
lastTableArea = area;
@@ -230,13 +251,17 @@ class BeansTab extends AbstractTableTab {
}
private void renderDetail(Frame frame, Rect area, List<BeanData> visible) {
+ Style detailBorderStyle = detailFocused ?
Style.EMPTY.fg(Theme.accent()) : Theme.muted();
+ Style detailTitleStyle = detailFocused ? Theme.title() :
Style.EMPTY.fg(Theme.accent());
+
Integer sel = tableState.selected();
if (sel == null || sel < 0 || sel >= visible.size()) {
frame.renderWidget(
Paragraph.builder()
.text(Text.from(Line.from(Span.styled(" Select a
bean", Style.EMPTY.dim()))))
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
- .title(" Bean Detail ").build())
+ .borderStyle(detailBorderStyle)
+ .title(Title.from(Line.from(Span.styled("
Bean Detail ", detailTitleStyle)))).build())
.build(),
area);
return;
@@ -296,7 +321,9 @@ class BeansTab extends AbstractTableTab {
frame.renderWidget(
Paragraph.builder()
.text(Text.from(visibleContent))
-
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title(title).build())
+
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .borderStyle(detailBorderStyle)
+ .title(Title.from(Line.from(Span.styled(title,
detailTitleStyle)))).build())
.build(),
area);
}
@@ -318,6 +345,7 @@ class BeansTab extends AbstractTableTab {
} else {
hint(spans, "/", "filter");
}
+ hint(spans, "Tab", detailFocused ? "table" : "detail");
hint(spans, TuiIcons.HINT_SCROLL, "navigate");
hintLast(spans, "PgUp/Dn", "scroll");
}
@@ -333,6 +361,11 @@ class BeansTab extends AbstractTableTab {
return new SelectionContext("table", items, sel != null ? sel : -1,
items.size(), "Beans");
}
+ @Override
+ public Boolean isDetailFocused() {
+ return detailFocused;
+ }
+
private String[] filterModes() {
IntegrationInfo info = ctx.findSelectedIntegration();
String platform = info != null ? info.platform : null;
@@ -556,8 +589,9 @@ class BeansTab extends AbstractTableTab {
## Keys
- - `Up/Down` — select bean
- - `PgUp/PgDn` — scroll detail panel
+ - `Tab` — switch focus between table and detail panel
+ - `Up/Down` — navigate in focused panel
+ - `PgUp/PgDn` — page in focused panel
- `s` — cycle sort column
- `S` — reverse sort order
- `f` — cycle filter
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 52ba8db3713f..0e39410bbf67 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
@@ -41,6 +41,7 @@ import dev.tamboui.tui.event.MouseEvent;
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.paragraph.Paragraph;
import dev.tamboui.widgets.scrollbar.ScrollbarState;
import dev.tamboui.widgets.table.Cell;
@@ -74,6 +75,7 @@ class BrowseTab extends AbstractTab {
private int sortIndex;
private boolean sortReversed;
private int view = VIEW_ENDPOINTS;
+ private boolean detailFocused = true;
private int detailScroll;
private boolean prettyPrint;
@@ -112,20 +114,28 @@ class BrowseTab extends AbstractTab {
@Override
public boolean handleKeyEvent(KeyEvent ke) {
if (view == VIEW_DETAIL) {
- if (ke.isUp()) {
- detailScroll = Math.max(0, detailScroll - 1);
- return true;
- }
- if (ke.isDown()) {
- detailScroll++;
+ if (ke.isKey(KeyCode.TAB)) {
+ detailFocused = !detailFocused;
return true;
}
if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) {
- detailScroll = Math.max(0, detailScroll - 20);
+ if (detailFocused) {
+ detailScroll = Math.max(0, detailScroll - 20);
+ } else {
+ for (int i = 0; i < 20 && messageTableState.selected() !=
null && messageTableState.selected() > 0; i++) {
+ messageTableState.selectPrevious();
+ }
+ }
return true;
}
if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) {
- detailScroll += 20;
+ if (detailFocused) {
+ detailScroll += 20;
+ } else {
+ for (int i = 0; i < 20; i++) {
+ messageTableState.selectNext(messages.size());
+ }
+ }
return true;
}
if (ke.isChar('p')) {
@@ -138,6 +148,7 @@ class BrowseTab extends AbstractTab {
if (view == VIEW_MESSAGES) {
if (ke.isConfirm()) {
view = VIEW_DETAIL;
+ detailFocused = true;
detailScroll = 0;
return true;
}
@@ -236,7 +247,14 @@ class BrowseTab extends AbstractTab {
@Override
public void navigateUp() {
- if (view == VIEW_ENDPOINTS) {
+ if (view == VIEW_DETAIL) {
+ if (detailFocused) {
+ detailScroll = Math.max(0, detailScroll - 1);
+ } else {
+ messageTableState.selectPrevious();
+ detailScroll = 0;
+ }
+ } else if (view == VIEW_ENDPOINTS) {
endpointTableState.selectPrevious();
} else if (view == VIEW_MESSAGES) {
messageTableState.selectPrevious();
@@ -245,7 +263,14 @@ class BrowseTab extends AbstractTab {
@Override
public void navigateDown() {
- if (view == VIEW_ENDPOINTS) {
+ if (view == VIEW_DETAIL) {
+ if (detailFocused) {
+ detailScroll++;
+ } else {
+ messageTableState.selectNext(messages.size());
+ detailScroll = 0;
+ }
+ } else if (view == VIEW_ENDPOINTS) {
List<EndpointData> sorted = sortedEndpoints();
endpointTableState.selectNext(sorted.size());
} else if (view == VIEW_MESSAGES) {
@@ -324,12 +349,17 @@ class BrowseTab extends AbstractTab {
}
private void renderMessages(Frame frame, Rect area) {
+ renderMessages(frame, area, Style.EMPTY, Theme.selectionBg());
+ }
+
+ private void renderMessages(Frame frame, Rect area, Style borderStyle,
Style highlightStyle) {
if (loading.get() && messages.isEmpty()) {
String title = " " + (selectedEndpoint != null ?
selectedEndpoint.uri : "Messages") + " ";
frame.renderWidget(
Paragraph.builder()
.text(Text.from(Line.from(Span.styled(" Loading
messages...", Style.EMPTY.dim()))))
-
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title(title).build())
+
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+
.borderStyle(borderStyle).title(title).build())
.build(),
area);
return;
@@ -368,9 +398,10 @@ class BrowseTab extends AbstractTab {
Constraint.length(40),
Constraint.length(10),
Constraint.fill())
- .highlightStyle(Theme.selectionBg())
+ .highlightStyle(highlightStyle)
.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(borderStyle).title(title).build())
.build();
lastMessageTableArea = area;
@@ -380,12 +411,19 @@ class BrowseTab extends AbstractTab {
}
private void renderDetail(Frame frame, Rect area) {
+ Style tableBorderStyle = detailFocused ? Theme.muted() :
Style.EMPTY.fg(Theme.accent());
+ Style tableHighlight = detailFocused ? Theme.selectionBg().dim() :
Theme.selectionBg();
+ Style detailBorderStyle = detailFocused ?
Style.EMPTY.fg(Theme.accent()) : Theme.muted();
+ Style detailTitleStyle = detailFocused ? Theme.title() :
Style.EMPTY.fg(Theme.accent());
+
Integer sel = messageTableState.selected();
if (sel == null || sel < 0 || sel >= messages.size()) {
frame.renderWidget(
Paragraph.builder()
.text(Text.from(Line.from(Span.styled(" Select a
message", Style.EMPTY.dim()))))
-
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title("
Message ")
+
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .borderStyle(detailBorderStyle)
+ .title(Title.from(Line.from(Span.styled("
Message ", detailTitleStyle))))
.build())
.build(),
area);
@@ -394,11 +432,11 @@ class BrowseTab extends AbstractTab {
MessageData msg = messages.get(sel);
- // Split: message list (40%) + detail (fill)
+ // Split: message list (35%) + detail (fill)
List<Rect> chunks = Layout.vertical()
.constraints(Constraint.percentage(35), Constraint.fill())
.split(area);
- renderMessages(frame, chunks.get(0));
+ renderMessages(frame, chunks.get(0), tableBorderStyle, tableHighlight);
String title = " Message " + msg.position + " [" + (msg.exchangeId !=
null ? msg.exchangeId : "") + "] ";
@@ -457,7 +495,9 @@ class BrowseTab extends AbstractTab {
frame.renderWidget(
Paragraph.builder()
.text(Text.from(visible))
-
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title(title).build())
+
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .borderStyle(detailBorderStyle)
+ .title(Title.from(Line.from(Span.styled(title,
detailTitleStyle)))).build())
.build(),
chunks.get(1));
}
@@ -466,8 +506,9 @@ class BrowseTab extends AbstractTab {
public void renderFooter(List<Span> spans) {
hint(spans, "Esc", "back");
if (view == VIEW_DETAIL) {
- hint(spans, "p", "pretty" + (prettyPrint ? " [on]" : ""));
- hintLast(spans, TuiIcons.HINT_SCROLL, "scroll");
+ hint(spans, "Tab", detailFocused ? "messages" : "detail");
+ hint(spans, TuiIcons.HINT_SCROLL, "navigate");
+ hintLast(spans, "p", "pretty" + (prettyPrint ? " [on]" : ""));
} else if (view == VIEW_MESSAGES) {
hint(spans, "r", "refresh");
hintLast(spans, "Enter", "detail");
@@ -498,6 +539,14 @@ class BrowseTab extends AbstractTab {
return new SelectionContext("table", items, sel != null ? sel : -1,
items.size(), "Browse");
}
+ @Override
+ public Boolean isDetailFocused() {
+ if (view != VIEW_DETAIL) {
+ return null;
+ }
+ return detailFocused;
+ }
+
private List<EndpointData> sortedEndpoints() {
List<EndpointData> result = new ArrayList<>(allEndpoints);
result.sort((a, b) -> {
@@ -634,22 +683,30 @@ class BrowseTab extends AbstractTab {
}
md.exchangePattern = message.getString("exchangePattern");
- // Timestamp from headers
- JsonObject headers = message.getMap("headers");
- if (headers != null) {
+ // headers are a JsonArray of {key, value, type} objects
+ JsonArray headerArr = message.getCollection("headers");
+ if (headerArr != null) {
md.headers = new LinkedHashMap<>();
- for (String key : headers.keySet()) {
- Object val = headers.get(key);
- md.headers.put(key, val != null ? val.toString() :
"null");
- }
- Object tsObj = headers.get("CamelMessageTimestamp");
- if (tsObj instanceof Number) {
- md.timestamp = ((Number) tsObj).longValue();
+ for (Object item : headerArr) {
+ if (item instanceof JsonObject entry) {
+ String key = entry.getString("key");
+ Object val = entry.get("value");
+ if (key != null) {
+ md.headers.put(key, val != null ?
val.toString() : "null");
+ }
+ if ("CamelMessageTimestamp".equals(key) && val
instanceof Number n) {
+ md.timestamp = n.longValue();
+ }
+ }
}
}
- Object bodyObj = message.get("body");
- md.body = bodyObj != null ? bodyObj.toString() : null;
+ // body is a JsonObject with {type, value}
+ JsonObject bodyObj = message.getMap("body");
+ if (bodyObj != null) {
+ Object val = bodyObj.get("value");
+ md.body = val != null ? val.toString() : null;
+ }
result.add(md);
}
@@ -729,8 +786,11 @@ class BrowseTab extends AbstractTab {
## Keys
- `Up/Down` — navigate endpoints and messages
- - `Enter` — browse selected endpoint
- - `Esc` — back to endpoint list
+ - `PgUp/PgDn` — page through list or detail
+ - `Tab` — switch focus between messages and detail (in detail
view)
+ - `Enter` — browse selected endpoint / view message detail
+ - `p` — toggle pretty-print for message body
+ - `Esc` — back to previous view
""";
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConfigurationTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConfigurationTab.java
index 4e4e1066b76c..084a8c555848 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConfigurationTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConfigurationTab.java
@@ -35,6 +35,7 @@ import dev.tamboui.tui.event.MouseEvent;
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.paragraph.Paragraph;
import dev.tamboui.widgets.table.Cell;
import dev.tamboui.widgets.table.Row;
@@ -55,6 +56,7 @@ class ConfigurationTab extends AbstractTableTab {
private static final Style SECRET_STYLE = Theme.muted();
private int detailScroll;
+ private boolean detailFocused;
private CamelCatalog catalog;
private String catalogVersion;
@@ -77,29 +79,43 @@ class ConfigurationTab extends AbstractTableTab {
@Override
protected boolean handleTabKeyEvent(KeyEvent ke) {
- if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) {
- detailScroll = Math.max(0, detailScroll - 10);
+ if (ke.isKey(KeyCode.TAB)) {
+ detailFocused = !detailFocused;
return true;
}
- if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) {
- detailScroll += 10;
- return true;
+ if (detailFocused) {
+ if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) {
+ detailScroll = Math.max(0, detailScroll - 10);
+ return true;
+ }
+ if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) {
+ detailScroll += 10;
+ return true;
+ }
}
return false;
}
@Override
public void navigateUp() {
- tableState.selectPrevious();
- detailScroll = 0;
+ if (detailFocused) {
+ detailScroll = Math.max(0, detailScroll - 1);
+ } else {
+ tableState.selectPrevious();
+ detailScroll = 0;
+ }
}
@Override
public void navigateDown() {
- IntegrationInfo info = ctx.findSelectedIntegration();
- int count = info != null ? info.configProperties.size() : 0;
- tableState.selectNext(count);
- detailScroll = 0;
+ if (detailFocused) {
+ detailScroll++;
+ } else {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ int count = info != null ? info.configProperties.size() : 0;
+ tableState.selectNext(count);
+ detailScroll = 0;
+ }
}
@Override
@@ -171,6 +187,9 @@ class ConfigurationTab extends AbstractTableTab {
String title = String.format(" Configuration [%d] ", props.size());
+ Style tableBorderStyle = detailFocused ? Theme.muted() :
Style.EMPTY.fg(Theme.accent());
+ Style tableTitleStyle = detailFocused ? Style.EMPTY.fg(Theme.accent())
: Theme.title();
+
Table table = Table.builder()
.rows(rows)
.header(Row.from(
@@ -181,9 +200,11 @@ class ConfigurationTab extends AbstractTableTab {
Constraint.percentage(35),
Constraint.fill(),
Constraint.length(20))
- .highlightStyle(Theme.selectionBg())
+ .highlightStyle(detailFocused ? Theme.selectionBg().dim() :
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(tableBorderStyle)
+ .title(Title.from(Line.from(Span.styled(title,
tableTitleStyle)))).build())
.build();
lastTableArea = area;
@@ -192,13 +213,17 @@ class ConfigurationTab extends AbstractTableTab {
}
private void renderDetail(Frame frame, Rect area, List<ConfigProperty>
props) {
+ Style detailBorderStyle = detailFocused ?
Style.EMPTY.fg(Theme.accent()) : Theme.muted();
+ Style detailTitleStyle = detailFocused ? Theme.title() :
Style.EMPTY.fg(Theme.accent());
+
Integer sel = tableState.selected();
if (sel == null || sel < 0 || sel >= props.size()) {
frame.renderWidget(
Paragraph.builder()
.text(Text.from(Line.from(Span.styled(" Select a
property", Style.EMPTY.dim()))))
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
- .title(" Property Detail ").build())
+ .borderStyle(detailBorderStyle)
+ .title(Title.from(Line.from(Span.styled("
Property Detail ", detailTitleStyle)))).build())
.build(),
area);
return;
@@ -268,7 +293,8 @@ class ConfigurationTab extends AbstractTableTab {
Paragraph.builder()
.text(Text.from(visible))
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
- .title(title).build())
+ .borderStyle(detailBorderStyle)
+ .title(Title.from(Line.from(Span.styled(title,
detailTitleStyle)))).build())
.build(),
area);
}
@@ -401,6 +427,7 @@ class ConfigurationTab extends AbstractTableTab {
@Override
public void renderFooter(List<Span> spans) {
super.renderFooter(spans);
+ hint(spans, "Tab", detailFocused ? "table" : "detail");
hint(spans, TuiIcons.HINT_SCROLL, "navigate");
hintLast(spans, "PgUp/Dn", "scroll");
}
@@ -416,6 +443,11 @@ class ConfigurationTab extends AbstractTableTab {
return new SelectionContext("table", items, sel != null ? sel : -1,
items.size(), "Configuration");
}
+ @Override
+ public Boolean isDetailFocused() {
+ return detailFocused;
+ }
+
static int compareCamelFirst(ConfigProperty a, ConfigProperty b) {
boolean aCamel = a.key.startsWith("camel.");
boolean bCamel = b.key.startsWith("camel.");
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 b7332b32b0cd..24cf3b3a220b 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
@@ -417,6 +417,13 @@ class DataRefreshService {
continue;
}
boolean alive =
ProcessHandle.of(pid).map(ProcessHandle::isAlive).orElse(false);
+ if (!alive) {
+ Files.deleteIfExists(jsonFile);
+ Path logFile = jsonFile.resolveSibling(
+ fn.substring(0, fn.lastIndexOf('.')) +
".log");
+ Files.deleteIfExists(logFile);
+ continue;
+ }
InfraInfo info = new InfraInfo();
info.pid = pidStr;
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
index e68a0b6d292b..4fceee674992 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
@@ -33,6 +33,7 @@ import dev.tamboui.markdown.MarkdownView;
import dev.tamboui.style.Style;
import dev.tamboui.terminal.Frame;
import dev.tamboui.text.CharWidth;
+import dev.tamboui.text.Line;
import dev.tamboui.text.Span;
import dev.tamboui.tui.event.KeyCode;
import dev.tamboui.tui.event.KeyEvent;
@@ -41,6 +42,7 @@ import dev.tamboui.tui.event.MouseEventKind;
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.table.Cell;
import dev.tamboui.widgets.table.Row;
import dev.tamboui.widgets.table.Table;
@@ -80,6 +82,7 @@ class EndpointsTab extends AbstractTableTab {
private int chartMode = CHART_ALL;
private int panelMode = PANEL_CHART;
private int detailScroll;
+ private boolean detailFocused;
private Rect lastDetailArea;
private int chartPanelHeight = 16;
private final DragSplit vSplit = new DragSplit();
@@ -115,6 +118,10 @@ class EndpointsTab extends AbstractTableTab {
@Override
protected boolean handleTabKeyEvent(KeyEvent ke) {
+ if (ke.isKey(KeyCode.TAB) && panelMode == PANEL_DETAIL) {
+ detailFocused = !detailFocused;
+ return true;
+ }
if (ke.isCharIgnoreCase('f')) {
filter = (filter + 1) % 3;
return true;
@@ -126,9 +133,10 @@ class EndpointsTab extends AbstractTableTab {
if (ke.isCharIgnoreCase('d')) {
panelMode = panelMode == PANEL_CHART ? PANEL_DETAIL : PANEL_CHART;
detailScroll = 0;
+ detailFocused = false;
return true;
}
- if (panelMode == PANEL_DETAIL) {
+ if (panelMode == PANEL_DETAIL && detailFocused) {
if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) {
detailScroll = Math.max(0, detailScroll - 10);
return true;
@@ -183,22 +191,30 @@ class EndpointsTab extends AbstractTableTab {
@Override
public void navigateUp() {
- tableState.selectPrevious();
- detailScroll = 0;
+ if (detailFocused) {
+ detailScroll = Math.max(0, detailScroll - 1);
+ } else {
+ tableState.selectPrevious();
+ detailScroll = 0;
+ }
}
@Override
public void navigateDown() {
- IntegrationInfo info = ctx.findSelectedIntegration();
- if (info != null) {
- List<EndpointInfo> filtered = new ArrayList<>(info.endpoints);
- if (filter == 1) {
- filtered.removeIf(ep -> !ep.remote);
- } else if (filter == 2) {
- filtered.removeIf(ep -> !ep.remote && !ep.stub);
+ if (detailFocused) {
+ detailScroll++;
+ } else {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ if (info != null) {
+ List<EndpointInfo> filtered = new ArrayList<>(info.endpoints);
+ if (filter == 1) {
+ filtered.removeIf(ep -> !ep.remote);
+ } else if (filter == 2) {
+ filtered.removeIf(ep -> !ep.remote && !ep.stub);
+ }
+ tableState.selectNext(filtered.size());
+ detailScroll = 0;
}
- tableState.selectNext(filtered.size());
- detailScroll = 0;
}
}
@@ -275,16 +291,22 @@ class EndpointsTab extends AbstractTableTab {
widths.add(Constraint.length(8));
widths.add(Constraint.fill());
+ boolean showDetailFocus = panelMode == PANEL_DETAIL;
+ Style tableBorderStyle = showDetailFocus && detailFocused ?
Theme.muted() : Style.EMPTY.fg(Theme.accent());
+ Style tableTitleStyle = showDetailFocus && detailFocused ?
Style.EMPTY.fg(Theme.accent()) : Theme.title();
+ String tableTitle = " Endpoints"
+ + (filter == 1 ? " filter:remote" : filter == 2 ?
" filter:remote+stub" : "")
+ + " ";
+
Table table = Table.builder()
.rows(rows)
.header(Row.from(headerCells))
.widths(widths.toArray(Constraint[]::new))
- .highlightStyle(Theme.selectionBg())
+ .highlightStyle(showDetailFocus && detailFocused ?
Theme.selectionBg().dim() : Theme.selectionBg())
.highlightSpacing(Table.HighlightSpacing.ALWAYS)
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
- .title(" Endpoints"
- + (filter == 1 ? " filter:remote" : filter == 2
? " filter:remote+stub" : "")
- + " ")
+ .borderStyle(tableBorderStyle)
+ .title(Title.from(Line.from(Span.styled(tableTitle,
tableTitleStyle))))
.build())
.build();
@@ -376,7 +398,8 @@ class EndpointsTab extends AbstractTableTab {
hint(spans, "a", "chart " + chartLabel);
hint(spans, "d", "detail " + (panelMode == PANEL_DETAIL ? "[on]" :
"[off]"));
if (panelMode == PANEL_DETAIL) {
- hintLast(spans, "PgUp/Dn", "scroll");
+ hint(spans, "Tab", detailFocused ? "table" : "detail");
+ hintLast(spans, TuiIcons.HINT_SCROLL, "navigate");
}
}
@@ -510,13 +533,17 @@ class EndpointsTab extends AbstractTableTab {
}
private void renderDetail(Frame frame, Rect area, List<EndpointInfo>
sortedEndpoints, IntegrationInfo info) {
+ Style detailBorderStyle = detailFocused ?
Style.EMPTY.fg(Theme.accent()) : Theme.muted();
+ Style detailTitleStyle = detailFocused ? Theme.title() :
Style.EMPTY.fg(Theme.accent());
+
Integer sel = tableState.selected();
if (sel == null || sel < 0 || sel >= sortedEndpoints.size()) {
frame.renderWidget(
MarkdownView.builder()
.source("*Select an endpoint*")
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
- .title(" Endpoint Detail ").build())
+ .borderStyle(detailBorderStyle)
+ .title(Title.from(Line.from(Span.styled("
Endpoint Detail ", detailTitleStyle)))).build())
.styles(Theme.markdownStyles())
.build(),
area);
@@ -531,7 +558,8 @@ class EndpointsTab extends AbstractTableTab {
MarkdownView.builder()
.source("*No endpoint URI*")
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
- .title(" Endpoint Detail ").build())
+ .borderStyle(detailBorderStyle)
+ .title(Title.from(Line.from(Span.styled("
Endpoint Detail ", detailTitleStyle)))).build())
.styles(Theme.markdownStyles())
.build(),
area);
@@ -631,7 +659,8 @@ class EndpointsTab extends AbstractTableTab {
.source(md.toString())
.scroll(detailScroll)
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
- .title(title).build())
+ .borderStyle(detailBorderStyle)
+ .title(Title.from(Line.from(Span.styled(title,
detailTitleStyle)))).build())
.styles(Theme.markdownStyles())
.build(),
area);
@@ -681,6 +710,14 @@ class EndpointsTab extends AbstractTableTab {
return new SelectionContext("table", items, sel != null ? sel : -1,
items.size(), "Endpoints");
}
+ @Override
+ public Boolean isDetailFocused() {
+ if (panelMode != PANEL_DETAIL) {
+ return null;
+ }
+ return detailFocused;
+ }
+
@Override
public String description() {
return "Registered endpoints with usage statistics (hits, direction)";
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 fbe5f491ae11..41be33d787b7 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
@@ -107,6 +107,7 @@ class HistoryTab extends AbstractTab {
private boolean showDescription;
+ private boolean detailFocused;
private boolean showWaterfall;
private int waterfallScroll;
private final ScrollbarState waterfallScrollState = new ScrollbarState();
@@ -264,9 +265,16 @@ class HistoryTab extends AbstractTab {
boolean tracerActive = !traces.get().isEmpty();
+ if (ke.isKey(KeyCode.TAB)) {
+ if (!showWaterfall && ((tracerActive && traceDetailView) ||
!tracerActive)) {
+ detailFocused = !detailFocused;
+ }
+ return true;
+ }
+
if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) {
if (tracerActive && traceDetailView) {
- if (showWaterfall) {
+ if (showWaterfall || !detailFocused) {
for (int i = 0; i < 10; i++) {
traceStepTableState.selectPrevious();
}
@@ -274,7 +282,7 @@ class HistoryTab extends AbstractTab {
traceDetailScroll = Math.max(0, traceDetailScroll - 5);
}
} else {
- if (showWaterfall) {
+ if (showWaterfall || !detailFocused) {
for (int i = 0; i < 10; i++) {
historyTableState.selectPrevious();
}
@@ -286,7 +294,7 @@ class HistoryTab extends AbstractTab {
}
if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) {
if (tracerActive && traceDetailView) {
- if (showWaterfall) {
+ if (showWaterfall || !detailFocused) {
List<TraceEntry> steps =
getTraceStepsDepthFirst(traceSelectedExchangeId);
for (int i = 0; i < 10; i++) {
traceStepTableState.selectNext(steps.size());
@@ -295,7 +303,7 @@ class HistoryTab extends AbstractTab {
traceDetailScroll += 5;
}
} else {
- if (showWaterfall) {
+ if (showWaterfall || !detailFocused) {
for (int i = 0; i < 10; i++) {
historyTableState.selectNext(historyVisibleCount);
}
@@ -379,6 +387,7 @@ class HistoryTab extends AbstractTab {
}
if (ke.isCharIgnoreCase('g')) {
showWaterfall = !showWaterfall;
+ detailFocused = false;
waterfallScroll = 0;
return true;
}
@@ -398,6 +407,7 @@ class HistoryTab extends AbstractTab {
if (sel != null && sel >= 0 && sel <
traceSortedExchangeIds.size()) {
traceSelectedExchangeId = traceSortedExchangeIds.get(sel);
traceDetailView = true;
+ detailFocused = false;
traceStepTableState.select(0);
traceDetailScroll = 0;
}
@@ -435,6 +445,7 @@ class HistoryTab extends AbstractTab {
}
if (ke.isCharIgnoreCase('g')) {
showWaterfall = !showWaterfall;
+ detailFocused = false;
waterfallScroll = 0;
return true;
}
@@ -458,6 +469,7 @@ class HistoryTab extends AbstractTab {
traceFilePositions.clear();
traces.set(Collections.emptyList());
traceDetailView = false;
+ detailFocused = false;
traceSelectedExchangeId = null;
traceDetailScroll = 0;
traceDetailHScroll = 0;
@@ -479,6 +491,7 @@ class HistoryTab extends AbstractTab {
}
if (traceDetailView) {
traceDetailView = false;
+ detailFocused = false;
traceSelectedExchangeId = null;
traceDetailScroll = 0;
showWaterfall = false;
@@ -614,14 +627,22 @@ class HistoryTab extends AbstractTab {
public void navigateUp() {
if (!traces.get().isEmpty()) {
if (traceDetailView) {
- traceStepTableState.selectPrevious();
- traceDetailScroll = 0;
+ if (detailFocused && !showWaterfall) {
+ traceDetailScroll = Math.max(0, traceDetailScroll - 1);
+ } else {
+ traceStepTableState.selectPrevious();
+ traceDetailScroll = 0;
+ }
} else {
traceTableState.selectPrevious();
}
} else {
- historyTableState.selectPrevious();
- historyDetailScroll = 0;
+ if (detailFocused && !showWaterfall) {
+ historyDetailScroll = Math.max(0, historyDetailScroll - 1);
+ } else {
+ historyTableState.selectPrevious();
+ historyDetailScroll = 0;
+ }
}
}
@@ -629,16 +650,24 @@ class HistoryTab extends AbstractTab {
public void navigateDown() {
if (!traces.get().isEmpty()) {
if (traceDetailView) {
- List<TraceEntry> steps =
getTraceStepsDepthFirst(traceSelectedExchangeId);
- traceStepTableState.selectNext(steps.size());
- traceDetailScroll = 0;
+ if (detailFocused && !showWaterfall) {
+ traceDetailScroll++;
+ } else {
+ List<TraceEntry> steps =
getTraceStepsDepthFirst(traceSelectedExchangeId);
+ traceStepTableState.selectNext(steps.size());
+ traceDetailScroll = 0;
+ }
} else {
List<String> exchangeIds = getTraceExchangeIds();
traceTableState.selectNext(exchangeIds.size());
}
} else {
- historyTableState.selectNext(historyVisibleCount);
- historyDetailScroll = 0;
+ if (detailFocused && !showWaterfall) {
+ historyDetailScroll++;
+ } else {
+ historyTableState.selectNext(historyVisibleCount);
+ historyDetailScroll = 0;
+ }
}
}
@@ -770,8 +799,10 @@ class HistoryTab extends AbstractTab {
boolean tracerActive = !traces.get().isEmpty();
if (tracerActive && traceDetailView) {
hint(spans, "Esc", "back");
+ if (!showWaterfall) {
+ hint(spans, "Tab", detailFocused ? "table" : "detail");
+ }
hint(spans, TuiIcons.HINT_SCROLL, "navigate");
- hint(spans, "PgUp/PgDn", "scroll");
if (!showWaterfall && !traceWordWrap) {
hint(spans, TuiIcons.HINT_H, "h-scroll");
}
@@ -792,8 +823,10 @@ class HistoryTab extends AbstractTab {
hintLast(spans, "F5", "refresh");
} else {
hint(spans, "Esc", "back");
+ if (!showWaterfall) {
+ hint(spans, "Tab", detailFocused ? "table" : "detail");
+ }
hint(spans, TuiIcons.HINT_SCROLL, "navigate");
- hint(spans, "PgUp/PgDn", "scroll");
if (!showWaterfall && !historyWordWrap) {
hint(spans, TuiIcons.HINT_H, "h-scroll");
}
@@ -1264,12 +1297,16 @@ class HistoryTab extends AbstractTab {
String stepTitle
= String.format(" Trace [%s] — %d steps ",
TuiHelper.truncate(traceSelectedExchangeId, 30), steps.size());
+ boolean showFocus = !showWaterfall;
+ Style tableBorderStyle = showFocus && detailFocused ? Theme.muted() :
Style.EMPTY.fg(Theme.accent());
+ Style tableHighlight = showFocus && detailFocused ?
Theme.selectionBg().dim() : Theme.selectionBg();
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);
+ buildStepTable(rows, stepTitle, showDescription,
tableBorderStyle, tableHighlight),
+ chunks.get(0), traceStepTableState);
renderTableScrollbar(frame, lastTraceStepArea, traceStepTableState,
traceStepScrollState,
steps.size());
@@ -1283,6 +1320,8 @@ class HistoryTab extends AbstractTab {
}
private void renderTraceStepDetail(Frame frame, Rect area,
List<TraceEntry> steps) {
+ Style detailBorderStyle = detailFocused ?
Style.EMPTY.fg(Theme.accent()) : Theme.muted();
+ Style detailTitleStyle = detailFocused ? Theme.title() :
Style.EMPTY.fg(Theme.accent());
Integer sel = traceStepTableState.selected();
if (sel == null || sel < 0 || sel >= steps.size()) {
@@ -1291,7 +1330,8 @@ class HistoryTab extends AbstractTab {
.text(Text.from(Line.from(
Span.styled(" Select a trace step to view
details",
Style.EMPTY.dim()))))
-
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).build())
+
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .borderStyle(detailBorderStyle).build())
.build(),
area);
return;
@@ -1327,7 +1367,8 @@ class HistoryTab extends AbstractTab {
int[] scroll = { traceDetailScroll };
int[] hScroll = { traceDetailHScroll };
- renderDetailPanel(frame, area, lines, traceWordWrap, hScroll, scroll,
traceDetailScrollState, " Detail ");
+ renderDetailPanel(frame, area, lines, traceWordWrap, hScroll, scroll,
traceDetailScrollState,
+ " Detail ", detailBorderStyle,
Title.from(Line.from(Span.styled(" Detail ", detailTitleStyle))));
traceDetailScroll = scroll[0];
traceDetailHScroll = hScroll[0];
}
@@ -1542,12 +1583,16 @@ class HistoryTab extends AbstractTab {
}
Title historyTitle = buildHistoryTitle(current);
+ boolean showFocus = !showWaterfall;
+ Style tableBorderStyle = showFocus && detailFocused ? Theme.muted() :
Style.EMPTY.fg(Theme.accent());
+ Style tableHighlight = showFocus && detailFocused ?
Theme.selectionBg().dim() : Theme.selectionBg();
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);
+ buildStepTable(rows, historyTitle, showDescription,
tableBorderStyle, tableHighlight),
+ chunks.get(0), historyTableState);
renderTableScrollbar(frame, lastHistoryTableArea, historyTableState,
historyTableScrollState,
current.size());
@@ -1561,6 +1606,8 @@ class HistoryTab extends AbstractTab {
}
private void renderHistoryDetail(Frame frame, Rect area,
List<HistoryEntry> current) {
+ Style detailBorderStyle = detailFocused ?
Style.EMPTY.fg(Theme.accent()) : Theme.muted();
+ Style detailTitleStyle = detailFocused ? Theme.title() :
Style.EMPTY.fg(Theme.accent());
Integer sel = historyTableState.selected();
if (sel == null || sel < 0 || sel >= current.size()) {
@@ -1570,7 +1617,8 @@ class HistoryTab extends AbstractTab {
Span.styled(" Select a history entry to
view details",
Style.EMPTY.dim()))))
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
- .title(" Detail ").build())
+ .borderStyle(detailBorderStyle)
+ .title(Title.from(Line.from(Span.styled("
Detail ", detailTitleStyle)))).build())
.build(),
area);
return;
@@ -1606,7 +1654,8 @@ class HistoryTab extends AbstractTab {
int[] scroll = { historyDetailScroll };
int[] hScroll = { historyDetailHScroll };
- renderDetailPanel(frame, area, lines, historyWordWrap, hScroll,
scroll, historyDetailScrollState, " Detail ");
+ renderDetailPanel(frame, area, lines, historyWordWrap, hScroll,
scroll, historyDetailScrollState,
+ " Detail ", detailBorderStyle,
Title.from(Line.from(Span.styled(" Detail ", detailTitleStyle))));
historyDetailScroll = scroll[0];
historyDetailHScroll = hScroll[0];
}
@@ -1964,6 +2013,12 @@ class HistoryTab extends AbstractTab {
}
private static Table buildStepTable(List<Row> rows, Object title, boolean
descriptionMode) {
+ return buildStepTable(rows, title, descriptionMode, Style.EMPTY,
Theme.selectionBg());
+ }
+
+ private static Table buildStepTable(
+ List<Row> rows, Object title, boolean descriptionMode,
+ Style borderStyle, Style highlightStyle) {
Row header = Row.from(
rightCell("#", 3, Style.EMPTY.bold()),
Cell.from(Span.styled("", Style.EMPTY.bold())),
@@ -1974,8 +2029,9 @@ class HistoryTab extends AbstractTab {
Cell.from(Span.styled("BHPV", Style.EMPTY.bold())),
rightCell("ELAPSED", 10, Style.EMPTY.bold()));
Block block = title instanceof Title t
- ?
Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title(t).build()
- :
Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title(title.toString()).build();
+ ?
Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).borderStyle(borderStyle).title(t).build()
+ :
Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).borderStyle(borderStyle)
+ .title(title.toString()).build();
return Table.builder()
.rows(rows)
.header(header)
@@ -1988,7 +2044,7 @@ class HistoryTab extends AbstractTab {
Constraint.fill(),
Constraint.length(4),
Constraint.length(11))
- .highlightStyle(Theme.selectionBg())
+ .highlightStyle(highlightStyle)
.highlightSpacing(Table.HighlightSpacing.ALWAYS)
.block(block)
.build();
@@ -2188,8 +2244,17 @@ class HistoryTab extends AbstractTab {
static void renderDetailPanel(
Frame frame, Rect area, List<Line> lines,
boolean wordWrap, int[] hScroll, int[] scroll, ScrollbarState
scrollState, String title) {
- Block.Builder bb =
Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL);
- if (title != null) {
+ renderDetailPanel(frame, area, lines, wordWrap, hScroll, scroll,
scrollState, title, Style.EMPTY, null);
+ }
+
+ static void renderDetailPanel(
+ Frame frame, Rect area, List<Line> lines,
+ boolean wordWrap, int[] hScroll, int[] scroll, ScrollbarState
scrollState,
+ String title, Style borderStyle, Title styledTitle) {
+ Block.Builder bb =
Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).borderStyle(borderStyle);
+ if (styledTitle != null) {
+ bb.title(styledTitle);
+ } else if (title != null) {
bb.title(title);
}
Block block = bb.build();
@@ -2313,6 +2378,21 @@ class HistoryTab extends AbstractTab {
return new SelectionContext("table", items, sel != null ? sel : -1,
items.size(), "History");
}
+ @Override
+ public Boolean isDetailFocused() {
+ if (showWaterfall) {
+ return null;
+ }
+ boolean tracerActive = !traces.get().isEmpty();
+ if (tracerActive && traceDetailView) {
+ return detailFocused;
+ }
+ if (!tracerActive) {
+ return detailFocused;
+ }
+ return null;
+ }
+
@Override
public String description() {
return "Message history trace showing route path, headers, body, and
timing";
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
index 5130e4767da6..60436bbcffae 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
@@ -329,6 +329,11 @@ class McpFacade {
return tab != null ? tab.getSelectionContext() : null;
}
+ Boolean isDetailFocused() {
+ MonitorTab tab = bridge.activeTab();
+ return tab != null ? tab.isDetailFocused() : null;
+ }
+
List<String> getIntegrationNames() {
return data.get().stream()
.filter(i -> !i.vanishing)
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorTab.java
index 506767cc5d6a..c965a750fd79 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorTab.java
@@ -89,4 +89,8 @@ interface MonitorTab {
default boolean isOverlayActive() {
return false;
}
+
+ default Boolean isDetailFocused() {
+ return null;
+ }
}
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 a3333a65d253..ba959cc04d49 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
@@ -35,6 +35,7 @@ import dev.tamboui.tui.event.MouseEvent;
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.paragraph.Paragraph;
import dev.tamboui.widgets.table.Cell;
import dev.tamboui.widgets.table.Row;
@@ -57,6 +58,7 @@ class ThreadsTab extends AbstractTableTab {
private int threadCount;
private int peakThreadCount;
private int traceScroll;
+ private boolean detailFocused;
private String lastPid;
private long lastRefreshTime;
@@ -95,13 +97,19 @@ class ThreadsTab extends AbstractTableTab {
@Override
protected boolean handleTabKeyEvent(KeyEvent ke) {
- if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) {
- traceScroll = Math.max(0, traceScroll - 10);
+ if (ke.isKey(KeyCode.TAB)) {
+ detailFocused = !detailFocused;
return true;
}
- if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) {
- traceScroll += 10;
- return true;
+ if (detailFocused) {
+ if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) {
+ traceScroll = Math.max(0, traceScroll - 10);
+ return true;
+ }
+ if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) {
+ traceScroll += 10;
+ return true;
+ }
}
if (ke.isCharIgnoreCase('f')) {
filter = (filter + 1) % FILTER_LABELS.length;
@@ -121,14 +129,22 @@ class ThreadsTab extends AbstractTableTab {
@Override
public void navigateUp() {
- super.navigateUp();
- traceScroll = 0;
+ if (detailFocused) {
+ traceScroll = Math.max(0, traceScroll - 1);
+ } else {
+ super.navigateUp();
+ traceScroll = 0;
+ }
}
@Override
public void navigateDown() {
- super.navigateDown();
- traceScroll = 0;
+ if (detailFocused) {
+ traceScroll++;
+ } else {
+ super.navigateDown();
+ traceScroll = 0;
+ }
}
@Override
@@ -184,6 +200,9 @@ class ThreadsTab extends AbstractTableTab {
String title = String.format(" Threads [%d/%d] peak:%d filter:%s ",
visible.size(), threadCount, peakThreadCount,
FILTER_LABELS[filter]);
+ Style tableBorderStyle = detailFocused ? Theme.muted() :
Style.EMPTY.fg(Theme.accent());
+ Style tableTitleStyle = detailFocused ? Style.EMPTY.fg(Theme.accent())
: Theme.title();
+
Table table = Table.builder()
.rows(rows)
.header(Row.from(
@@ -198,9 +217,11 @@ class ThreadsTab extends AbstractTableTab {
Constraint.length(16),
Constraint.length(14),
Constraint.length(15))
- .highlightStyle(Theme.selectionBg())
+ .highlightStyle(detailFocused ? Theme.selectionBg().dim() :
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(tableBorderStyle)
+ .title(Title.from(Line.from(Span.styled(title,
tableTitleStyle)))).build())
.build();
lastTableArea = area;
@@ -209,12 +230,17 @@ class ThreadsTab extends AbstractTableTab {
}
private void renderTrace(Frame frame, Rect area, List<ThreadData> visible)
{
+ Style detailBorderStyle = detailFocused ?
Style.EMPTY.fg(Theme.accent()) : Theme.muted();
+ Style detailTitleStyle = detailFocused ? Theme.title() :
Style.EMPTY.fg(Theme.accent());
+
Integer sel = tableState.selected();
if (sel == null || sel < 0 || sel >= visible.size()) {
frame.renderWidget(
Paragraph.builder()
.text(Text.from(Line.from(Span.styled(" Select a
thread", Style.EMPTY.dim()))))
-
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title("
Stack Trace ")
+
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .borderStyle(detailBorderStyle)
+ .title(Title.from(Line.from(Span.styled("
Stack Trace ", detailTitleStyle))))
.build())
.build(),
area);
@@ -229,7 +255,9 @@ class ThreadsTab extends AbstractTableTab {
frame.renderWidget(
Paragraph.builder()
.text(Text.from(Line.from(Span.styled(" No stack
trace available", Style.EMPTY.dim()))))
-
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title(title).build())
+
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .borderStyle(detailBorderStyle)
+
.title(Title.from(Line.from(Span.styled(title, detailTitleStyle)))).build())
.build(),
area);
return;
@@ -256,7 +284,9 @@ class ThreadsTab 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(detailBorderStyle)
+ .title(Title.from(Line.from(Span.styled(title,
detailTitleStyle)))).build())
.build(),
area);
}
@@ -266,6 +296,7 @@ class ThreadsTab extends AbstractTableTab {
hint(spans, "Esc", "back");
hint(spans, "s", "sort");
hint(spans, "f", "filter [" + FILTER_LABELS[filter] + "]");
+ hint(spans, "Tab", detailFocused ? "table" : "trace");
hint(spans, TuiIcons.HINT_SCROLL, "navigate");
hintLast(spans, "PgUp/Dn", "scroll");
}
@@ -281,6 +312,11 @@ class ThreadsTab extends AbstractTableTab {
return new SelectionContext("table", items, sel != null ? sel : -1,
items.size(), "Threads");
}
+ @Override
+ public Boolean isDetailFocused() {
+ return detailFocused;
+ }
+
private List<ThreadData> sortedThreads() {
List<ThreadData> result = new ArrayList<>();
for (ThreadData t : allThreads) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
index 5d6b5f9d77da..77c6b2fba371 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
@@ -155,7 +155,8 @@ class TuiToolRegistry {
+ "Shows exactly what the user sees in their
terminal. "
+ "Use ansi=true to include ANSI color codes
for color-related questions. "
+ "Also returns a 'selection' field with
structured metadata about the active list/table "
- + "(type, items, selectedIndex, totalItems,
label) when available.",
+ + "(type, items, selectedIndex, totalItems,
label) when available. "
+ + "On tabs with master/detail panels,
includes 'detailFocused' (true=detail, false=table).",
Map.of("ansi", propDef("boolean", "Include ANSI color codes in
the output (default false)")))));
tools.add(toToolDef(toolDef(
"tui_get_events",
@@ -168,7 +169,10 @@ class TuiToolRegistry {
+ "and integration count. "
+ "Includes a 'selection' field with
structured metadata about the active list/table. "
+ "captionVisible indicates if a caption
overlay is on screen. "
- + "keystrokesVisible indicates if the
keystroke overlay is on; toggle with Ctrl+K.",
+ + "keystrokesVisible indicates if the
keystroke overlay is on; toggle with Ctrl+K. "
+ + "detailFocused (boolean, present on tabs
with master/detail panels) indicates "
+ + "which panel has focus: true=detail panel,
false=table panel. "
+ + "Press Tab to toggle focus. Up/Down and
PgUp/PgDn operate on the focused panel.",
Map.of())));
tools.add(toToolDef(toolDef(
"tui_show_caption",
@@ -1619,6 +1623,10 @@ class TuiToolRegistry {
sel.put("items", items);
result.put("selection", sel);
}
+ Boolean detailFocused = facade.isDetailFocused();
+ if (detailFocused != null) {
+ result.put("detailFocused", detailFocused);
+ }
}
private void addFooterActions(JsonObject result) {