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 e6fe7371176b Camel TUI: Drop the accent pane border while the shell or
AI panel is open
e6fe7371176b is described below
commit e6fe7371176be1ec856a6e0dd3c56c1f6a522268
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Sep 8 13:33:22 2026 +0200
Camel TUI: Drop the accent pane border while the shell or AI panel is open
Tabs mark the pane that receives keys with an accent border. When the
shell (F6) or AI (F8) panel is open that panel owns the focus, yet the
tab's previously focused pane kept its accent border, so two panes
looked focused at once. Pane borders now go through
MonitorContext.paneBorder, which returns the muted style for every tab
pane while a bottom panel is open.
Camel TUI: Add Panel Position and Panel Space settings for the shell and AI
panels
Two new settings control how the shell (F6) and AI (F8) panels open:
- Panel Position (camel.tui.panelPosition): bottom (default) or top.
- Panel Space (camel.tui.panelSpace): move (default) takes rows away
from the tab as before; overlay draws the panel on top of the tab so
the tab keeps its full height underneath.
Both are cyclers in F2 -> Settings and take effect immediately on save.
The panel border drag-resize follows the panel edge that faces the tab
in either position. Layout is computed by CamelMonitor.panelLayout,
which is covered by tests, and the settings round-trip and popup rows
are tested as well. Documented in the TUI user manual.
Camel TUI: Keep the pinned log visible when the shell or AI panel opens at
the top
With Panel Position set to top the bottom of the screen stays free, so
the pinned log no longer disappears while the shell (F6) or AI (F8)
panel is open: the panel takes the top, the tab the middle and the
pinned log the bottom. The pin shrinks when needed so the panel and at
least three rows of tab remain, and vanishes only when there is no room.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../modules/ROOT/pages/camel-jbang-tui.adoc | 5 +
.../dsl/jbang/core/commands/tui/BeansTab.java | 4 +-
.../dsl/jbang/core/commands/tui/BrowseTab.java | 4 +-
.../dsl/jbang/core/commands/tui/CamelMonitor.java | 107 ++++++++++++++++-----
.../jbang/core/commands/tui/ConfigurationTab.java | 4 +-
.../dsl/jbang/core/commands/tui/EndpointsTab.java | 4 +-
.../dsl/jbang/core/commands/tui/HistoryTab.java | 8 +-
.../dsl/jbang/core/commands/tui/HttpProbe.java | 6 +-
.../jbang/core/commands/tui/MonitorContext.java | 22 +++++
.../dsl/jbang/core/commands/tui/OverviewTab.java | 4 +-
.../dsl/jbang/core/commands/tui/SettingsPopup.java | 67 ++++++++++---
.../dsl/jbang/core/commands/tui/SourceTab.java | 6 +-
.../dsl/jbang/core/commands/tui/TuiSettings.java | 34 +++++++
.../commands/tui/CamelMonitorPanelLayoutTest.java | 79 +++++++++++++++
.../commands/tui/MonitorContextPaneBorderTest.java | 49 ++++++++++
.../jbang/core/commands/tui/SettingsPopupTest.java | 64 ++++++++++--
.../jbang/core/commands/tui/TuiSettingsTest.java | 6 ++
17 files changed, 414 insertions(+), 59 deletions(-)
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
index 7acf42b6c811..06713c524c54 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
@@ -546,12 +546,17 @@ Open the *F2* actions menu and choose *Settings...* to
change TUI preferences in
of this setting.
* *Default Folder* -- the folder pre-filled in *Run from Folder*. The most
recently used folder
still takes precedence; this default is used only when there is no
remembered folder.
+* *Panel Position* -- where the shell (*F6*) and AI (*F8*) panels open:
*bottom* (default) or
+ *top*.
+* *Panel Space* -- whether those panels push the tab content aside (*move*,
default) or are drawn
+ on top of it (*overlay*), so the tab keeps its full height underneath.
Use *↑*/*↓* to move between rows, *Space* (or *←*/*→*) to cycle the theme and
tab settings, type to
edit the default folder, *Enter* to save, and *Esc* to cancel.
Settings are stored under `camel.tui.*` keys (`camel.tui.theme`,
`camel.tui.startTab`,
`camel.tui.selectTab`, `camel.tui.confirmActions`, `camel.tui.defaultFolder`,
+`camel.tui.panelPosition`, `camel.tui.panelSpace`,
`camel.tui.shell.history`, `camel.tui.ai.promptHistory`) in the Camel CLI
configuration file. Each key is read from and
written back to the file where it currently lives: a key present in the local
`./camel-cli.properties` is treated as a project-level override and stays
local, while every
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 552f7dba8177..9a5b599e8287 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
@@ -227,7 +227,7 @@ 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 tableBorderStyle = ctx.paneBorder(!detailFocused);
Style tableTitleStyle = detailFocused ? Style.EMPTY.fg(Theme.accent())
: Theme.title();
Table table = Table.builder()
@@ -251,7 +251,7 @@ 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 detailBorderStyle = ctx.paneBorder(detailFocused);
Style detailTitleStyle = detailFocused ? Theme.title() :
Style.EMPTY.fg(Theme.accent());
Integer sel = tableState.selected();
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 f1f7fa2a2319..51ef08fbb6a0 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
@@ -411,9 +411,9 @@ class BrowseTab extends AbstractTab {
}
private void renderDetail(Frame frame, Rect area) {
- Style tableBorderStyle = detailFocused ? Theme.muted() :
Style.EMPTY.fg(Theme.accent());
+ Style tableBorderStyle = ctx.paneBorder(!detailFocused);
Style tableHighlight = detailFocused ? Theme.selectionBg().dim() :
Theme.selectionBg();
- Style detailBorderStyle = detailFocused ?
Style.EMPTY.fg(Theme.accent()) : Theme.muted();
+ Style detailBorderStyle = ctx.paneBorder(detailFocused);
Style detailTitleStyle = detailFocused ? Theme.title() :
Style.EMPTY.fg(Theme.accent());
Integer sel = messageTableState.selected();
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 b39116a00c6e..d8bf3bb6d5a2 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
@@ -33,6 +33,7 @@ import java.util.Queue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;
+import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import dev.tamboui.buffer.Buffer;
@@ -57,6 +58,7 @@ import dev.tamboui.tui.event.MouseEvent;
import dev.tamboui.tui.event.MouseEventKind;
import dev.tamboui.tui.event.PasteEvent;
import dev.tamboui.tui.event.TickEvent;
+import dev.tamboui.widgets.Clear;
import dev.tamboui.widgets.paragraph.Paragraph;
import dev.tamboui.widgets.tabs.Tabs;
import dev.tamboui.widgets.tabs.TabsState;
@@ -797,6 +799,80 @@ public class CamelMonitor extends CamelCommand {
ctx.logPinPercent = Integer.parseInt(logPin);
}
+ /**
+ * Renders the shell or AI panel according to the panel settings: at the
bottom or top of the content area, and
+ * either taking space away from the tab (move) or drawn on top of it
(overlay).
+ */
+ private void renderSidePanel(Frame frame, Rect contentArea, int ph,
BiConsumer<Frame, Rect> panel) {
+ // A panel at the top leaves the bottom free, so a pinned log stays
visible there.
+ Rect upper = contentArea;
+ Rect pinArea = null;
+ boolean showPin = ctx.panelTop && logPinned &&
tabRegistry.selectedTabIndex() != TAB_LOG;
+ if (showPin) {
+ logPinAnim.initHeight(contentArea.height());
+ int pinH = pinnedLogHeight(contentArea.height(), ph,
logPinAnim.panelHeight());
+ if (pinH > 0) {
+ upper = new Rect(contentArea.x(), contentArea.y(),
contentArea.width(), contentArea.height() - pinH);
+ pinArea = new Rect(contentArea.x(), upper.bottom(),
contentArea.width(), pinH);
+ }
+ }
+ ctx.logPinVisible = pinArea != null;
+
+ Rect[] layout = panelLayout(upper, ph, ctx.panelTop, ctx.panelOverlay);
+ Rect tabArea = layout[0];
+ Rect panelArea = layout[1];
+ // tabs hide their charts when little space is left: report how much
of the area they lost
+ ctx.shellPercent = (contentArea.height() - tabArea.height()) * 100 /
Math.max(1, contentArea.height());
+ if (tabArea.height() > 0) {
+ renderContent(frame, tabArea);
+ }
+ if (pinArea != null) {
+ tabRegistry.logTab().render(frame, pinArea);
+ }
+ if (ctx.panelOverlay) {
+ frame.renderWidget(Clear.INSTANCE, panelArea);
+ }
+ panel.accept(frame, panelArea);
+ // the draggable border is the panel edge that faces the tab
+ panelSplit.setBorderPos(ctx.panelTop ? panelArea.bottom() - 1 :
panelArea.y());
+ }
+
+ /**
+ * Rows for the pinned log below a top panel: the requested pin height,
reduced so the panel and at least three rows
+ * of tab remain; zero when there is no room.
+ */
+ static int pinnedLogHeight(int contentHeight, int panelHeight, int
pinHeight) {
+ int room = contentHeight - panelHeight - 3;
+ return Math.max(0, Math.min(pinHeight, room));
+ }
+
+ /**
+ * Splits the content area between the tab and a side panel of {@code
panelHeight} rows.
+ *
+ * @param top place the panel at the top of the area instead of the
bottom
+ * @param overlay keep the full area for the tab and let the panel cover
part of it
+ * @return the tab area (height 0 when the panel fills everything)
and the panel area
+ */
+ static Rect[] panelLayout(Rect content, int panelHeight, boolean top,
boolean overlay) {
+ int h = content.height();
+ int ph = Math.max(0, Math.min(panelHeight, h));
+ if (ph >= h) {
+ return new Rect[] { new Rect(content.x(), content.y(),
content.width(), 0), content };
+ }
+ Rect panelArea = top
+ ? new Rect(content.x(), content.y(), content.width(), ph)
+ : new Rect(content.x(), content.y() + h - ph, content.width(),
ph);
+ Rect tabArea;
+ if (overlay) {
+ tabArea = content;
+ } else if (top) {
+ tabArea = new Rect(content.x(), content.y() + ph, content.width(),
h - ph);
+ } else {
+ tabArea = new Rect(content.x(), content.y(), content.width(), h -
ph);
+ }
+ return new Rect[] { tabArea, panelArea };
+ }
+
private void applyRatePer() {
String ratePer = TuiSettings.load().getRatePer();
ctx.ratePerMinute = "minutes".equals(ratePer);
@@ -806,6 +882,8 @@ public class CamelMonitor extends CamelCommand {
TuiSettings settings = TuiSettings.load();
ctx.confirmActions = settings.isConfirmActions();
ctx.validateOnSave = settings.isValidateOnSave();
+ ctx.panelTop = settings.isPanelTop();
+ ctx.panelOverlay = settings.isPanelOverlay();
}
// ---- Event Handling ----
@@ -1147,7 +1225,10 @@ public class CamelMonitor extends CamelCommand {
if (panelSplit.isDragging() && me.kind() == MouseEventKind.DRAG) {
int contentHeight = lastContentArea.height();
if (contentHeight > 0) {
- int newHeight = lastContentArea.y() + contentHeight -
me.y();
+ boolean top = ctx.panelTop && (shellPanel.isOpen() ||
aiPanel.isOpen());
+ int newHeight = top
+ ? me.y() - lastContentArea.y() + 1
+ : lastContentArea.y() + contentHeight - me.y();
newHeight = Math.max(3, Math.min(contentHeight - 3,
newHeight));
if (shellPanel.isOpen()) {
shellPanel.setPanelHeight(newHeight);
@@ -1499,33 +1580,15 @@ public class CamelMonitor extends CamelCommand {
shellPanel.tickAnimation();
aiPanel.tickAnimation();
logPinAnim.tickAnimation();
+ ctx.bottomPanelFocused = shellPanel.isOpen() || aiPanel.isOpen();
if (canvasOverlay.isVisible()) {
canvasOverlay.render(frame, contentArea);
} else if (shellPanel.isOpen()) {
shellPanel.initHeight(contentArea.height());
- int ph = shellPanel.panelHeight();
- ctx.shellPercent = ph * 100 / Math.max(1, contentArea.height());
- if (ph >= contentArea.height()) {
- shellPanel.render(frame, contentArea);
- panelSplit.setBorderPos(contentArea.y());
- } else {
- List<Rect> splitChunks = Layout.vertical()
- .constraints(Constraint.fill(), Constraint.length(ph))
- .split(contentArea);
- renderContent(frame, splitChunks.get(0));
- shellPanel.render(frame, splitChunks.get(1));
- panelSplit.setBorderPos(splitChunks.get(1).y());
- }
+ renderSidePanel(frame, contentArea, shellPanel.panelHeight(),
shellPanel::render);
} else if (aiPanel.isOpen()) {
aiPanel.initHeight(contentArea.height());
- int ph = aiPanel.panelHeight();
- ctx.shellPercent = ph * 100 / Math.max(1, contentArea.height());
- List<Rect> splitChunks = Layout.vertical()
- .constraints(Constraint.fill(), Constraint.length(ph))
- .split(contentArea);
- renderContent(frame, splitChunks.get(0));
- aiPanel.render(frame, splitChunks.get(1));
- panelSplit.setBorderPos(splitChunks.get(1).y());
+ renderSidePanel(frame, contentArea, aiPanel.panelHeight(),
aiPanel::render);
} else if (logPinned && tabRegistry.selectedTabIndex() != TAB_LOG) {
logPinAnim.initHeight(contentArea.height());
int ph = logPinAnim.panelHeight();
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 03f696a635a5..8bf37a59c810 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
@@ -190,7 +190,7 @@ class ConfigurationTab extends AbstractTableTab {
String title = String.format(" Configuration [%d] ", props.size());
- Style tableBorderStyle = detailFocused ? Theme.muted() :
Style.EMPTY.fg(Theme.accent());
+ Style tableBorderStyle = ctx.paneBorder(!detailFocused);
Style tableTitleStyle = detailFocused ? Style.EMPTY.fg(Theme.accent())
: Theme.title();
Table table = Table.builder()
@@ -216,7 +216,7 @@ 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 detailBorderStyle = ctx.paneBorder(detailFocused);
Style detailTitleStyle = detailFocused ? Theme.title() :
Style.EMPTY.fg(Theme.accent());
Integer sel = tableState.selected();
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 5801e48c71ba..6fb44bcc993d 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
@@ -292,7 +292,7 @@ class EndpointsTab extends AbstractTableTab {
widths.add(Constraint.fill());
boolean showDetailFocus = panelMode == PANEL_DETAIL;
- Style tableBorderStyle = showDetailFocus && detailFocused ?
Theme.muted() : Style.EMPTY.fg(Theme.accent());
+ Style tableBorderStyle = ctx.paneBorder(!(showDetailFocus &&
detailFocused));
Style tableTitleStyle = showDetailFocus && detailFocused ?
Style.EMPTY.fg(Theme.accent()) : Theme.title();
String tableTitle = " Endpoints"
+ (filter == 1 ? " filter:remote" : filter == 2 ?
" filter:remote+stub" : "")
@@ -559,7 +559,7 @@ 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 detailBorderStyle = ctx.paneBorder(detailFocused);
Style detailTitleStyle = detailFocused ? Theme.title() :
Style.EMPTY.fg(Theme.accent());
Integer sel = tableState.selected();
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 4db498ef1f9d..8e1808c2fad3 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
@@ -1295,7 +1295,7 @@ 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 tableBorderStyle = ctx.paneBorder(!(showFocus && detailFocused));
Style tableHighlight = showFocus && detailFocused ?
Theme.selectionBg().dim() : Theme.selectionBg();
lastTraceStepArea = chunks.get(0);
detailSplit.setBorderPos(chunks.get(1).y());
@@ -1317,7 +1317,7 @@ 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 detailBorderStyle = ctx.paneBorder(detailFocused);
Style detailTitleStyle = detailFocused ? Theme.title() :
Style.EMPTY.fg(Theme.accent());
Integer sel = traceStepTableState.selected();
@@ -1581,7 +1581,7 @@ class HistoryTab extends AbstractTab {
Title historyTitle = buildHistoryTitle(current);
boolean showFocus = !showWaterfall;
- Style tableBorderStyle = showFocus && detailFocused ? Theme.muted() :
Style.EMPTY.fg(Theme.accent());
+ Style tableBorderStyle = ctx.paneBorder(!(showFocus && detailFocused));
Style tableHighlight = showFocus && detailFocused ?
Theme.selectionBg().dim() : Theme.selectionBg();
lastHistoryTableArea = chunks.get(0);
vSplit.setBorderPos(chunks.get(1).y());
@@ -1603,7 +1603,7 @@ 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 detailBorderStyle = ctx.paneBorder(detailFocused);
Style detailTitleStyle = detailFocused ? Theme.title() :
Style.EMPTY.fg(Theme.accent());
Integer sel = historyTableState.selected();
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpProbe.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpProbe.java
index 32933ed55de6..3e7449cfb384 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpProbe.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpProbe.java
@@ -1273,7 +1273,7 @@ class HttpProbe {
Span.raw(" " + probePathState.text() + " ")));
boolean requestFocused = probeField != PROBE_HISTORY;
- Style borderStyle = requestFocused ? Style.EMPTY.fg(Theme.accent()) :
Style.EMPTY;
+ Style borderStyle = ctx.paneBorder(requestFocused, Style.EMPTY);
Block block =
Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
.borderStyle(borderStyle).title(title).build();
frame.renderWidget(block, area);
@@ -1382,7 +1382,7 @@ class HttpProbe {
int bodyH = 6;
FormHelper.renderLabel(frame, innerX, row, labelW, "Body:", probeField
== PROBE_BODY);
Rect bodyArea = new Rect(innerX + labelW, row, fieldW, bodyH);
- Style bodyBorderStyle = probeField == PROBE_BODY ?
Style.EMPTY.fg(Theme.accent()) : Theme.muted();
+ Style bodyBorderStyle = ctx.paneBorder(probeField == PROBE_BODY);
Block bodyBlock = Block.builder()
.borders(Borders.ALL)
.borderType(BorderType.ROUNDED)
@@ -1627,7 +1627,7 @@ class HttpProbe {
private void renderProbeHistory(Frame frame, Rect area) {
String title = " History [" + probeHistory.size() + "] ";
boolean historyFocused = probeField == PROBE_HISTORY;
- Style histBorderStyle = historyFocused ?
Style.EMPTY.fg(Theme.accent()) : Style.EMPTY;
+ Style histBorderStyle = ctx.paneBorder(historyFocused, Style.EMPTY);
if (probeHistory.isEmpty()) {
frame.renderWidget(
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
index 6b4e5cbb395c..093889fcedb1 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
@@ -26,6 +26,7 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
+import dev.tamboui.style.Style;
import dev.tamboui.tui.TuiRunner;
import org.apache.camel.catalog.CamelCatalog;
import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
@@ -57,6 +58,12 @@ class MonitorContext {
boolean ratePerMinute;
boolean confirmActions;
boolean validateOnSave = true;
+ /** True while the shell (F6) or AI (F8) panel is open and owns keyboard
focus. */
+ boolean bottomPanelFocused;
+ /** Shell/AI panel opens at the top of the content area instead of the
bottom. */
+ boolean panelTop;
+ /** Shell/AI panel is drawn over the tab instead of taking space away from
it. */
+ boolean panelOverlay;
BiConsumer<String, Boolean> notificationCallback;
BiConsumer<String, String> openMarkdownCallback;
OpenOptionsCallback openOptionsCallback;
@@ -98,6 +105,21 @@ class MonitorContext {
return findSelectedInfra() != null;
}
+ /**
+ * Border style for a focusable pane. The accent color marks the pane that
receives keys; while the shell or AI
+ * panel is open that panel owns the focus, so every tab pane is drawn
muted.
+ */
+ Style paneBorder(boolean focused) {
+ return paneBorder(focused, Theme.muted());
+ }
+
+ /**
+ * Same as {@link #paneBorder(boolean)} with a custom style for the
unfocused state.
+ */
+ Style paneBorder(boolean focused, Style unfocused) {
+ return focused && !bottomPanelFocused ? Style.EMPTY.fg(Theme.accent())
: unfocused;
+ }
+
String selectedName() {
IntegrationInfo info = findSelectedIntegration();
if (info != null) {
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 e1c7bda1c0fc..990053c91c7b 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
@@ -571,7 +571,7 @@ class OverviewTab extends AbstractTab {
}
String integrationTitle = infraCount > 0 ? " Integrations " : "
Overview ";
- Style intBorderStyle = infraFocused ? Theme.muted() :
Style.EMPTY.fg(Theme.accent());
+ Style intBorderStyle = ctx.paneBorder(!infraFocused);
Style intTitleStyle = infraFocused ? Style.EMPTY.fg(Theme.accent()) :
Theme.title();
Table.Builder tableBuilder = Table.builder()
.rows(rows)
@@ -995,7 +995,7 @@ class OverviewTab extends AbstractTab {
Constraint.fill())
.highlightSpacing(Table.HighlightSpacing.ALWAYS)
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
- .borderStyle(infraFocused ?
Style.EMPTY.fg(Theme.accent()) : Theme.muted())
+ .borderStyle(ctx.paneBorder(infraFocused))
.title(Title.from(Line.from(
Span.styled(" Dev/Infra Services ",
infraFocused ? Theme.title() :
Style.EMPTY.fg(Theme.accent())))))
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopup.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopup.java
index a4a403695dd7..67e0d02e3505 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopup.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopup.java
@@ -50,20 +50,24 @@ class SettingsPopup {
private static final int ROW_SELECT_TAB = 2;
private static final int ROW_LOG_PIN = 3;
private static final int ROW_RATE_PER = 4;
- private static final int ROW_CONFIRM_ACTIONS = 5;
- private static final int ROW_VALIDATE_ON_SAVE = 6;
- private static final int ROW_FOLDER = 7;
- private static final int ROW_PROXY_HOST = 8;
- private static final int ROW_PROXY_PORT = 9;
- private static final int ROW_SHELL_HISTORY = 10;
- private static final int ROW_AI_PROVIDER = 11;
- private static final int ROW_AI_MODEL = 12;
- private static final int ROW_AI_URL = 13;
- private static final int ROW_AI_PROMPT_HISTORY = 14;
- private static final int ROW_COUNT = 15;
+ private static final int ROW_PANEL_POSITION = 5;
+ private static final int ROW_PANEL_SPACE = 6;
+ private static final int ROW_CONFIRM_ACTIONS = 7;
+ private static final int ROW_VALIDATE_ON_SAVE = 8;
+ private static final int ROW_FOLDER = 9;
+ private static final int ROW_PROXY_HOST = 10;
+ private static final int ROW_PROXY_PORT = 11;
+ private static final int ROW_SHELL_HISTORY = 12;
+ private static final int ROW_AI_PROVIDER = 13;
+ private static final int ROW_AI_MODEL = 14;
+ private static final int ROW_AI_URL = 15;
+ private static final int ROW_AI_PROMPT_HISTORY = 16;
+ private static final int ROW_COUNT = 17;
private static final String[] LOG_PIN_OPTIONS = { "off", "25", "50", "75"
};
private static final String[] RATE_PER_OPTIONS = { "seconds", "minutes" };
+ private static final String[] PANEL_POSITION_OPTIONS = { "bottom", "top" };
+ private static final String[] PANEL_SPACE_OPTIONS = { "move", "overlay" };
private static final List<String> AI_PROVIDERS = buildAiProviderList();
private static List<String> buildAiProviderList() {
@@ -84,6 +88,8 @@ class SettingsPopup {
private int selectTabIndex;
private int logPinIndex;
private int ratePerIndex;
+ private int panelPositionIndex;
+ private int panelSpaceIndex;
private int confirmActionsIndex;
private int validateOnSaveIndex;
private int aiProviderIndex;
@@ -160,6 +166,8 @@ class SettingsPopup {
String currentRatePer = settings.getRatePer() != null ?
settings.getRatePer() : "seconds";
ratePerIndex = "minutes".equals(currentRatePer) ? 1 : 0;
+ panelPositionIndex = settings.isPanelTop() ? 1 : 0;
+ panelSpaceIndex = settings.isPanelOverlay() ? 1 : 0;
confirmActionsIndex = settings.isConfirmActions() ? 1 : 0;
validateOnSaveIndex = settings.isValidateOnSave() ? 1 : 0;
@@ -259,6 +267,18 @@ class SettingsPopup {
}
return true;
}
+ if (selectedRow == ROW_PANEL_POSITION) {
+ if (ke.isChar(' ') || ke.isRight() || ke.isLeft()) {
+ panelPositionIndex = panelPositionIndex == 0 ? 1 : 0;
+ }
+ return true;
+ }
+ if (selectedRow == ROW_PANEL_SPACE) {
+ if (ke.isChar(' ') || ke.isRight() || ke.isLeft()) {
+ panelSpaceIndex = panelSpaceIndex == 0 ? 1 : 0;
+ }
+ return true;
+ }
if (selectedRow == ROW_CONFIRM_ACTIONS) {
if (ke.isChar(' ') || ke.isRight() || ke.isLeft()) {
confirmActionsIndex = confirmActionsIndex == 0 ? 1 : 0;
@@ -327,6 +347,12 @@ class SettingsPopup {
if (monitorContext != null) {
monitorContext.ratePerMinute = "minutes".equals(ratePerValue);
}
+ settings.setPanelPosition(panelPositionIndex == 1 ? "top" : null);
+ settings.setPanelSpace(panelSpaceIndex == 1 ? "overlay" : null);
+ if (monitorContext != null) {
+ monitorContext.panelTop = panelPositionIndex == 1;
+ monitorContext.panelOverlay = panelSpaceIndex == 1;
+ }
settings.setConfirmActions(confirmActionsIndex == 1 ? "true" :
"false");
if (monitorContext != null) {
monitorContext.confirmActions = confirmActionsIndex == 1;
@@ -405,6 +431,16 @@ class SettingsPopup {
renderValue(frame, innerX + labelW, rowY, fieldW,
RATE_PER_OPTIONS[ratePerIndex], selectedRow == ROW_RATE_PER);
rowY++;
+ renderLabel(frame, innerX, rowY, labelW, "Panel Position:",
selectedRow == ROW_PANEL_POSITION);
+ renderValue(frame, innerX + labelW, rowY, fieldW,
PANEL_POSITION_OPTIONS[panelPositionIndex],
+ selectedRow == ROW_PANEL_POSITION);
+ rowY++;
+
+ renderLabel(frame, innerX, rowY, labelW, "Panel Space:", selectedRow
== ROW_PANEL_SPACE);
+ renderValue(frame, innerX + labelW, rowY, fieldW,
PANEL_SPACE_OPTIONS[panelSpaceIndex],
+ selectedRow == ROW_PANEL_SPACE);
+ rowY++;
+
renderDivider(frame, innerX, rowY, innerW);
rowY++;
@@ -467,6 +503,7 @@ class SettingsPopup {
void renderFooter(List<Span> spans) {
if (selectedRow == ROW_THEME || selectedRow == ROW_START_TAB ||
selectedRow == ROW_SELECT_TAB
|| selectedRow == ROW_LOG_PIN || selectedRow == ROW_RATE_PER
+ || selectedRow == ROW_PANEL_POSITION || selectedRow ==
ROW_PANEL_SPACE
|| selectedRow == ROW_CONFIRM_ACTIONS || selectedRow ==
ROW_VALIDATE_ON_SAVE
|| selectedRow == ROW_AI_PROVIDER) {
hint(spans, "Space", "cycle");
@@ -584,6 +621,14 @@ class SettingsPopup {
return LOG_PIN_OPTIONS[logPinIndex];
}
+ String selectedPanelPosition() {
+ return PANEL_POSITION_OPTIONS[panelPositionIndex];
+ }
+
+ String selectedPanelSpace() {
+ return PANEL_SPACE_OPTIONS[panelSpaceIndex];
+ }
+
String folderText() {
return folderInput != null ? folderInput.text() : "";
}
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 696aed167974..3270877fa6e4 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
@@ -2801,7 +2801,7 @@ class SourceTab extends AbstractTab {
}
private void renderFileList(Frame frame, Rect area) {
- Style fileBorderStyle = focusOnViewer ? Theme.muted() :
Style.EMPTY.fg(Theme.accent());
+ Style fileBorderStyle = ctx.paneBorder(!focusOnViewer);
if (entries.isEmpty()) {
String noFilesMsg = rootDir == null ? "No source directory found"
: "No files found";
frame.renderWidget(
@@ -2903,7 +2903,7 @@ class SourceTab extends AbstractTab {
}
}
- Style infoBorderStyle = focusOnViewer ? Theme.muted() :
Style.EMPTY.fg(Theme.accent());
+ Style infoBorderStyle = ctx.paneBorder(!focusOnViewer);
frame.renderWidget(
Paragraph.builder()
.text(Text.from(lines))
@@ -2919,7 +2919,7 @@ class SourceTab extends AbstractTab {
private void renderSourcePanel(Frame frame, Rect area) {
Style sourceTitleStyle = focusOnViewer ? Theme.title() :
Style.EMPTY.fg(Theme.accent());
- Style sourceBorderStyle = focusOnViewer ?
Style.EMPTY.fg(Theme.accent()) : Theme.muted();
+ Style sourceBorderStyle = ctx.paneBorder(focusOnViewer);
if (sourceViewer.isVisible()) {
sourceViewer.setTitleStyle(sourceTitleStyle);
sourceViewer.setBorderStyle(sourceBorderStyle);
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiSettings.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiSettings.java
index 233084687d6d..542aba850790 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiSettings.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiSettings.java
@@ -45,6 +45,8 @@ final class TuiSettings {
static final String PROP_AI_PROMPT_HISTORY = "camel.tui.ai.promptHistory";
static final String PROP_CONFIRM_ACTIONS = "camel.tui.confirmActions";
static final String PROP_VALIDATE_ON_SAVE = "camel.tui.validateOnSave";
+ static final String PROP_PANEL_POSITION = "camel.tui.panelPosition";
+ static final String PROP_PANEL_SPACE = "camel.tui.panelSpace";
private String themeId;
private String startTab;
@@ -61,6 +63,8 @@ final class TuiSettings {
private String aiPromptHistory;
private String confirmActions;
private String validateOnSave;
+ private String panelPosition;
+ private String panelSpace;
String getThemeId() {
return themeId;
@@ -198,6 +202,32 @@ final class TuiSettings {
return !"false".equals(validateOnSave);
}
+ /** Where the shell (F6) and AI (F8) panels open: {@code bottom} (default)
or {@code top}. */
+ String getPanelPosition() {
+ return panelPosition;
+ }
+
+ void setPanelPosition(String panelPosition) {
+ this.panelPosition = panelPosition;
+ }
+
+ boolean isPanelTop() {
+ return "top".equalsIgnoreCase(panelPosition);
+ }
+
+ /** Whether those panels push the tab aside ({@code move}, default) or are
drawn over it ({@code overlay}). */
+ String getPanelSpace() {
+ return panelSpace;
+ }
+
+ void setPanelSpace(String panelSpace) {
+ this.panelSpace = panelSpace;
+ }
+
+ boolean isPanelOverlay() {
+ return "overlay".equalsIgnoreCase(panelSpace);
+ }
+
/**
* Loads the current settings, resolving each key with per-key
local/global precedence via {@link TuiUserConfig}.
* Unset keys yield {@code null} fields; a read failure yields an object
with {@code null} fields rather than
@@ -221,6 +251,8 @@ final class TuiSettings {
settings.aiPromptHistory =
trimToNull(TuiUserConfig.read(PROP_AI_PROMPT_HISTORY));
settings.confirmActions =
trimToNull(TuiUserConfig.read(PROP_CONFIRM_ACTIONS));
settings.validateOnSave =
trimToNull(TuiUserConfig.read(PROP_VALIDATE_ON_SAVE));
+ settings.panelPosition =
trimToNull(TuiUserConfig.read(PROP_PANEL_POSITION));
+ settings.panelSpace =
trimToNull(TuiUserConfig.read(PROP_PANEL_SPACE));
} catch (RuntimeException e) {
// best-effort: return an object with null fields on read failure
}
@@ -249,6 +281,8 @@ final class TuiSettings {
TuiUserConfig.write(PROP_AI_PROMPT_HISTORY, aiPromptHistory);
TuiUserConfig.write(PROP_CONFIRM_ACTIONS, confirmActions);
TuiUserConfig.write(PROP_VALIDATE_ON_SAVE, validateOnSave);
+ TuiUserConfig.write(PROP_PANEL_POSITION, panelPosition);
+ TuiUserConfig.write(PROP_PANEL_SPACE, panelSpace);
} catch (RuntimeException e) {
// best-effort: a save failure must not disrupt the TUI
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitorPanelLayoutTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitorPanelLayoutTest.java
new file mode 100644
index 000000000000..b05975d3a17b
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitorPanelLayoutTest.java
@@ -0,0 +1,79 @@
+/*
+ * 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 dev.tamboui.layout.Rect;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests how the content area is split between the tab and the shell/AI panel
for the Panel Position (bottom/top) and
+ * Panel Space (move/overlay) settings.
+ */
+class CamelMonitorPanelLayoutTest {
+
+ private static final Rect CONTENT = new Rect(0, 3, 120, 30);
+
+ @Test
+ void bottomMoveTakesRowsFromTheTab() {
+ Rect[] layout = CamelMonitor.panelLayout(CONTENT, 10, false, false);
+ assertThat(layout[0]).isEqualTo(new Rect(0, 3, 120, 20));
+ assertThat(layout[1]).isEqualTo(new Rect(0, 23, 120, 10));
+ }
+
+ @Test
+ void topMovePushesTheTabDown() {
+ Rect[] layout = CamelMonitor.panelLayout(CONTENT, 10, true, false);
+ assertThat(layout[1]).isEqualTo(new Rect(0, 3, 120, 10));
+ assertThat(layout[0]).isEqualTo(new Rect(0, 13, 120, 20));
+ }
+
+ @Test
+ void overlayKeepsTheFullTabArea() {
+ Rect[] bottom = CamelMonitor.panelLayout(CONTENT, 10, false, true);
+ assertThat(bottom[0]).isEqualTo(CONTENT);
+ assertThat(bottom[1]).isEqualTo(new Rect(0, 23, 120, 10));
+
+ Rect[] top = CamelMonitor.panelLayout(CONTENT, 10, true, true);
+ assertThat(top[0]).isEqualTo(CONTENT);
+ assertThat(top[1]).isEqualTo(new Rect(0, 3, 120, 10));
+ }
+
+ @Test
+ void fullHeightPanelLeavesNoTabArea() {
+ Rect[] layout = CamelMonitor.panelLayout(CONTENT, 30, true, false);
+ assertThat(layout[0].height()).isZero();
+ assertThat(layout[1]).isEqualTo(CONTENT);
+
+ Rect[] oversized = CamelMonitor.panelLayout(CONTENT, 99, false, true);
+ assertThat(oversized[1]).isEqualTo(CONTENT);
+ }
+
+ @Test
+ void pinnedLogKeepsItsHeightBelowATopPanelWhenThereIsRoom() {
+ assertThat(CamelMonitor.pinnedLogHeight(30, 10, 7)).isEqualTo(7);
+ }
+
+ @Test
+ void pinnedLogShrinksSoPanelAndSomeTabRemain() {
+ // 30 rows: 20 for the panel, 3 kept for the tab, leaves 7 for the pin
even if 10 were requested
+ assertThat(CamelMonitor.pinnedLogHeight(30, 20, 10)).isEqualTo(7);
+ assertThat(CamelMonitor.pinnedLogHeight(30, 28, 10)).isZero();
+ assertThat(CamelMonitor.pinnedLogHeight(30, 30, 10)).isZero();
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContextPaneBorderTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContextPaneBorderTest.java
new file mode 100644
index 000000000000..ba36d86d7308
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContextPaneBorderTest.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import dev.tamboui.style.Style;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The accent border marks the pane that receives keys. While the shell (F6)
or AI (F8) panel is open it owns the focus,
+ * so no tab pane may keep the accent border.
+ */
+class MonitorContextPaneBorderTest {
+
+ private final MonitorContext ctx = new MonitorContext(
+ new AtomicReference<>(List.of()), new
AtomicReference<>(List.of()));
+
+ @Test
+ void focusedPaneUsesAccentWhenNoBottomPanelIsOpen() {
+
assertThat(ctx.paneBorder(true)).isEqualTo(Style.EMPTY.fg(Theme.accent()));
+ assertThat(ctx.paneBorder(false)).isEqualTo(Theme.muted());
+ }
+
+ @Test
+ void bottomPanelTakesFocusFromEveryPane() {
+ ctx.bottomPanelFocused = true;
+ assertThat(ctx.paneBorder(true)).isEqualTo(Theme.muted());
+ assertThat(ctx.paneBorder(false)).isEqualTo(Theme.muted());
+ assertThat(ctx.paneBorder(true, Style.EMPTY)).isEqualTo(Style.EMPTY);
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopupTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopupTest.java
index 98929a84b48d..efba01b08d02 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopupTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopupTest.java
@@ -139,7 +139,9 @@ class SettingsPopupTest {
popup.handleKeyEvent(key(KeyCode.DOWN));
popup.handleKeyEvent(key(KeyCode.DOWN));
popup.handleKeyEvent(key(KeyCode.DOWN));
- assertEquals(7, popup.selectedRow());
+ popup.handleKeyEvent(key(KeyCode.DOWN));
+ popup.handleKeyEvent(key(KeyCode.DOWN));
+ assertEquals(9, popup.selectedRow());
popup.handleKeyEvent(KeyEvent.ofChar('/'));
popup.handleKeyEvent(KeyEvent.ofChar('a'));
assertEquals("/a", popup.folderText());
@@ -159,7 +161,9 @@ class SettingsPopupTest {
popup.handleKeyEvent(key(KeyCode.DOWN));
popup.handleKeyEvent(key(KeyCode.DOWN));
popup.handleKeyEvent(key(KeyCode.DOWN));
- assertEquals(7, popup.selectedRow());
+ popup.handleKeyEvent(key(KeyCode.DOWN));
+ popup.handleKeyEvent(key(KeyCode.DOWN));
+ assertEquals(9, popup.selectedRow());
popup.handleKeyEvent(KeyEvent.ofChar(0x01));
popup.handleKeyEvent(KeyEvent.ofChar(0x00));
popup.handleKeyEvent(KeyEvent.ofChar('x'));
@@ -185,7 +189,9 @@ class SettingsPopupTest {
popup.handleKeyEvent(key(KeyCode.DOWN));
popup.handleKeyEvent(key(KeyCode.DOWN));
popup.handleKeyEvent(key(KeyCode.DOWN));
- assertEquals(11, popup.selectedRow());
+ popup.handleKeyEvent(key(KeyCode.DOWN));
+ popup.handleKeyEvent(key(KeyCode.DOWN));
+ assertEquals(13, popup.selectedRow());
assertEquals("auto", popup.selectedAiProvider());
popup.handleKeyEvent(KeyEvent.ofChar(' '));
assertEquals("ollama", popup.selectedAiProvider());
@@ -218,10 +224,10 @@ class SettingsPopupTest {
popup.open();
// navigate to Shell History (row 10)
- for (int i = 0; i < 10; i++) {
+ for (int i = 0; i < 12; i++) {
popup.handleKeyEvent(key(KeyCode.DOWN));
}
- assertEquals(10, popup.selectedRow());
+ assertEquals(12, popup.selectedRow());
for (char c : "50".toCharArray()) {
popup.handleKeyEvent(KeyEvent.ofChar(c));
}
@@ -231,7 +237,7 @@ class SettingsPopupTest {
for (int i = 0; i < 4; i++) {
popup.handleKeyEvent(key(KeyCode.DOWN));
}
- assertEquals(14, popup.selectedRow());
+ assertEquals(16, popup.selectedRow());
for (char c : "200".toCharArray()) {
popup.handleKeyEvent(KeyEvent.ofChar(c));
}
@@ -264,6 +270,8 @@ class SettingsPopupTest {
popup.handleKeyEvent(key(KeyCode.DOWN)); // select tab
popup.handleKeyEvent(key(KeyCode.DOWN)); // log pin
popup.handleKeyEvent(key(KeyCode.DOWN)); // rate per
+ popup.handleKeyEvent(key(KeyCode.DOWN)); // panel position
+ popup.handleKeyEvent(key(KeyCode.DOWN)); // panel space
popup.handleKeyEvent(key(KeyCode.DOWN)); // confirm actions
popup.handleKeyEvent(key(KeyCode.DOWN)); // validate on save
popup.handleKeyEvent(key(KeyCode.DOWN)); // folder
@@ -315,4 +323,48 @@ class SettingsPopupTest {
assertEquals("light", Theme.mode(), "the previewed theme must apply
immediately, without saving");
assertTrue(cleared[0], "the screen must refresh so the live preview is
visible");
}
+
+ @Test
+ void panelRowsPersistPositionAndSpace(@TempDir Path tempDir) {
+ useHome(tempDir);
+ SettingsPopup popup = new SettingsPopup();
+ popup.setTabEntries(tabs());
+ popup.open();
+
+ for (int i = 0; i < 5; i++) {
+ popup.handleKeyEvent(key(KeyCode.DOWN));
+ }
+ assertEquals(5, popup.selectedRow());
+ assertEquals("bottom", popup.selectedPanelPosition(), "panels open at
the bottom by default");
+ popup.handleKeyEvent(KeyEvent.ofChar(' '));
+ assertEquals("top", popup.selectedPanelPosition());
+
+ popup.handleKeyEvent(key(KeyCode.DOWN));
+ assertEquals("move", popup.selectedPanelSpace(), "panels take space
from the tab by default");
+ popup.handleKeyEvent(KeyEvent.ofChar(' '));
+ assertEquals("overlay", popup.selectedPanelSpace());
+
+ popup.handleKeyEvent(key(KeyCode.ENTER));
+
+ TuiSettings persisted = TuiSettings.load();
+ assertEquals("top", persisted.getPanelPosition());
+ assertEquals("overlay", persisted.getPanelSpace());
+ assertTrue(persisted.isPanelTop());
+ assertTrue(persisted.isPanelOverlay());
+ }
+
+ @Test
+ void panelRowsDefaultValuesAreNotPersisted(@TempDir Path tempDir) {
+ useHome(tempDir);
+ SettingsPopup popup = new SettingsPopup();
+ popup.setTabEntries(tabs());
+ popup.open();
+ popup.handleKeyEvent(key(KeyCode.ENTER));
+
+ TuiSettings persisted = TuiSettings.load();
+ assertEquals(null, persisted.getPanelPosition(), "the default position
is not written to the config file");
+ assertEquals(null, persisted.getPanelSpace(), "the default space mode
is not written to the config file");
+ assertFalse(persisted.isPanelTop());
+ assertFalse(persisted.isPanelOverlay());
+ }
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiSettingsTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiSettingsTest.java
index b41d4c3856ed..53da3f8a7649 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiSettingsTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiSettingsTest.java
@@ -65,6 +65,8 @@ class TuiSettingsTest {
settings.setAiUrl("https://generativelanguage.googleapis.com");
settings.setShellHistory("25");
settings.setAiPromptHistory("50");
+ settings.setPanelPosition("top");
+ settings.setPanelSpace("overlay");
settings.save();
TuiSettings loaded = TuiSettings.load();
@@ -76,6 +78,10 @@ class TuiSettingsTest {
assertThat(loaded.getAiUrl()).isEqualTo("https://generativelanguage.googleapis.com");
assertThat(loaded.getShellHistory()).isEqualTo("25");
assertThat(loaded.getAiPromptHistory()).isEqualTo("50");
+ assertThat(loaded.getPanelPosition()).isEqualTo("top");
+ assertThat(loaded.isPanelTop()).isTrue();
+ assertThat(loaded.getPanelSpace()).isEqualTo("overlay");
+ assertThat(loaded.isPanelOverlay()).isTrue();
}
@Test