Skip to content

Commit cd0b90f

Browse files
ammachadoclaude
andcommitted
CAMEL-23841: camel-jbang - fix TUI layout overflow and add minimum size guard
- Tab bar: switch to compact labels (no outer spaces, | divider) when terminal width < 126 chars; full labels need 126, compact needs 88 - Footer: on overflow, drop secondary F-key hints (F2/F3/F6) before tab-specific action hints (stop/kill/restart) to preserve the most actionable hints; right-side decorations (MCP, recording) dropped first - Minimum size: render a btop-style centered message when terminal is below 88x24 instead of clipping content; resumes normal rendering as soon as the terminal is resized above the threshold Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent c061eae commit cd0b90f

1 file changed

Lines changed: 129 additions & 28 deletions

File tree

  • dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui

dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java

Lines changed: 129 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,12 @@ public class CamelMonitor extends CamelCommand {
9999
private static final int MAX_TRACES = 200;
100100
private static final int NUM_TABS = 10;
101101

102+
// Compact tab bar (10 labels + 9 "|" dividers) needs 88 chars — that is the true minimum
103+
private static final int MIN_WIDTH = 88;
104+
private static final int MIN_HEIGHT = 24;
105+
// Full tab bar (10 labels + 9 " | " dividers) needs 126 chars; use compact below that
106+
private static final int TABS_FULL_MIN_WIDTH = 126;
107+
102108
// Tab indices
103109
private static final int TAB_OVERVIEW = 0;
104110
private static final int TAB_LOG = 1;
@@ -968,6 +974,11 @@ private void navigateDown() {
968974
private void render(Frame frame) {
969975
Rect area = frame.area();
970976

977+
if (area.width() < MIN_WIDTH || area.height() < MIN_HEIGHT) {
978+
renderTooSmall(frame, area);
979+
return;
980+
}
981+
971982
// Layout: header (1 row) + spacer (1 row) + tabs (2 rows) + spacer (1 row) + content (fill) + footer (1 row)
972983
List<Rect> mainChunks = Layout.vertical()
973984
.constraints(
@@ -1066,15 +1077,62 @@ private void renderHeader(Frame frame, Rect area) {
10661077
area);
10671078
}
10681079

1080+
private void renderTooSmall(Frame frame, Rect area) {
1081+
Style orange = Style.EMPTY.fg(Color.rgb(0xF6, 0x91, 0x23));
1082+
Style normal = Style.EMPTY;
1083+
Style bold = Style.EMPTY.bold();
1084+
1085+
String line1 = "Terminal size too small:";
1086+
String wLabel = " Width = ";
1087+
String wVal = String.valueOf(area.width());
1088+
String hLabel = " Height = ";
1089+
String hVal = String.valueOf(area.height());
1090+
String line2 = wLabel + wVal + hLabel + hVal;
1091+
1092+
String line4 = "Needed for current config:";
1093+
String line5 = " Width = " + MIN_WIDTH + " Height = " + MIN_HEIGHT;
1094+
1095+
// 5 content lines (2 + blank + 2 + blank), center vertically
1096+
int startY = area.y() + Math.max(0, (area.height() - 5) / 2);
1097+
1098+
int x1 = area.x() + Math.max(0, (area.width() - CharWidth.of(line1)) / 2);
1099+
frame.buffer().setString(x1, startY, line1, bold);
1100+
1101+
int x2 = area.x() + Math.max(0, (area.width() - CharWidth.of(line2)) / 2);
1102+
int wLabelW = CharWidth.of(wLabel);
1103+
int wValW = CharWidth.of(wVal);
1104+
int hLabelW = CharWidth.of(hLabel);
1105+
frame.buffer().setString(x2, startY + 1, wLabel, normal);
1106+
frame.buffer().setString(x2 + wLabelW, startY + 1, wVal,
1107+
area.width() < MIN_WIDTH ? orange : normal);
1108+
frame.buffer().setString(x2 + wLabelW + wValW, startY + 1, hLabel, normal);
1109+
frame.buffer().setString(x2 + wLabelW + wValW + hLabelW, startY + 1, hVal,
1110+
area.height() < MIN_HEIGHT ? orange : normal);
1111+
1112+
int x4 = area.x() + Math.max(0, (area.width() - CharWidth.of(line4)) / 2);
1113+
frame.buffer().setString(x4, startY + 3, line4, bold);
1114+
1115+
int x5 = area.x() + Math.max(0, (area.width() - CharWidth.of(line5)) / 2);
1116+
frame.buffer().setString(x5, startY + 4, line5, normal);
1117+
}
1118+
10691119
private void renderTabs(Frame frame, Rect area) {
1120+
boolean compact = area.width() < TABS_FULL_MIN_WIDTH;
1121+
String dividerStr = compact ? "|" : " | ";
1122+
Span divider = Span.styled(dividerStr, Style.EMPTY.dim());
10701123
boolean infraSelected = isInfraSelected();
10711124

10721125
if (infraSelected) {
10731126
// Infra mode: only Overview and Log tabs
1074-
Line[] labels = {
1075-
Line.from(" 1 Overview "),
1076-
Line.from(" 2 Log "),
1077-
};
1127+
Line[] labels = compact
1128+
? new Line[] {
1129+
Line.from("1 Overview"),
1130+
Line.from("2 Log"),
1131+
}
1132+
: new Line[] {
1133+
Line.from(" 1 Overview "),
1134+
Line.from(" 2 Log "),
1135+
};
10781136

10791137
// Map real tab index to infra tab index for highlight
10801138
int infraTabIdx = tabsState.selected() == TAB_LOG ? 1 : 0;
@@ -1083,7 +1141,7 @@ private void renderTabs(Frame frame, Rect area) {
10831141
Tabs tabs = Tabs.builder()
10841142
.titles(labels)
10851143
.highlightStyle(Style.EMPTY.fg(Color.rgb(0xF6, 0x91, 0x23)).bold())
1086-
.divider(Span.styled(" | ", Style.EMPTY.dim()))
1144+
.divider(divider)
10871145
.build();
10881146

10891147
Rect labelsArea = area.height() >= 2
@@ -1093,24 +1151,37 @@ private void renderTabs(Frame frame, Rect area) {
10931151
return;
10941152
}
10951153

1096-
Line[] labels = {
1097-
Line.from(" 1 Overview "),
1098-
Line.from(" 2 Log "),
1099-
Line.from(" 3 Diagram "),
1100-
Line.from(routesTab.isTopMode() ? " 4 Top " : " 4 Route "),
1101-
Line.from(" 5 Endpoint "),
1102-
Line.from(" 6 HTTP "),
1103-
Line.from(" 7 Health "),
1104-
Line.from(" 8 Inspect "),
1105-
Line.from(" 9 Errors "),
1106-
Line.from(" 0 More▾ "),
1107-
};
1154+
Line[] labels = compact
1155+
? new Line[] {
1156+
Line.from("1 Overview"),
1157+
Line.from("2 Log"),
1158+
Line.from("3 Diagram"),
1159+
Line.from(routesTab.isTopMode() ? "4 Top " : "4 Route"),
1160+
Line.from("5 Endpoint"),
1161+
Line.from("6 HTTP"),
1162+
Line.from("7 Health"),
1163+
Line.from("8 Inspect"),
1164+
Line.from("9 Errors"),
1165+
Line.from("0 More▾"),
1166+
}
1167+
: new Line[] {
1168+
Line.from(" 1 Overview "),
1169+
Line.from(" 2 Log "),
1170+
Line.from(" 3 Diagram "),
1171+
Line.from(routesTab.isTopMode() ? " 4 Top " : " 4 Route "),
1172+
Line.from(" 5 Endpoint "),
1173+
Line.from(" 6 HTTP "),
1174+
Line.from(" 7 Health "),
1175+
Line.from(" 8 Inspect "),
1176+
Line.from(" 9 Errors "),
1177+
Line.from(" 0 More▾ "),
1178+
};
11081179
currentTabLabels = labels;
11091180

11101181
Tabs tabs = Tabs.builder()
11111182
.titles(labels)
11121183
.highlightStyle(Style.EMPTY.fg(Color.rgb(0xF6, 0x91, 0x23)).bold())
1113-
.divider(Span.styled(" | ", Style.EMPTY.dim()))
1184+
.divider(divider)
11141185
.build();
11151186

11161187
Rect labelsArea = area.height() >= 2
@@ -1124,7 +1195,7 @@ private void renderTabs(Frame frame, Rect area) {
11241195
computeTabBadges(badgeTexts, badgeStyles);
11251196

11261197
int badgeY = area.y();
1127-
int dividerW = CharWidth.of(" | ");
1198+
int dividerW = CharWidth.of(dividerStr);
11281199
int tabX = 0;
11291200
for (int i = 0; i < labels.length; i++) {
11301201
if (i > 0) {
@@ -1655,6 +1726,7 @@ private void renderFooter(Frame frame, Rect area) {
16551726
screenshotMessage = null;
16561727

16571728
List<Span> spans = new ArrayList<>();
1729+
int secondaryFKey = 0;
16581730

16591731
if (helpOverlay.isVisible()) {
16601732
helpOverlay.renderFooter(spans);
@@ -1684,10 +1756,10 @@ private void renderFooter(Frame frame, Rect area) {
16841756
MonitorTab tab = activeTab();
16851757

16861758
if (tabsState.selected() == TAB_OVERVIEW) {
1687-
renderOverviewFooter(spans);
1759+
secondaryFKey = renderOverviewFooter(spans);
16881760
} else {
16891761
tab.renderFooter(spans);
1690-
insertFKeyHints(spans);
1762+
secondaryFKey = insertFKeyHints(spans);
16911763
}
16921764
}
16931765

@@ -1733,9 +1805,34 @@ private void renderFooter(Frame frame, Rect area) {
17331805
}
17341806
}
17351807

1808+
int hintsWidth = spans.stream().mapToInt(Span::width).sum();
1809+
int rightWidth = rightSpans.stream().mapToInt(Span::width).sum();
1810+
int minGap = rightSpans.isEmpty() ? 0 : 1;
1811+
1812+
if (hintsWidth + rightWidth + minGap > area.width()) {
1813+
// Drop decorative right-side content first
1814+
rightSpans.clear();
1815+
rightWidth = 0;
1816+
minGap = 0;
1817+
// Drop secondary F-key hints (F2/F3/F6) before tab-specific action hints.
1818+
// They are inserted at position 2 (after the first tab hint), so the last
1819+
// secondary F-key pair sits at index (2 + secondaryFKey - 2).
1820+
while (secondaryFKey > 0 && hintsWidth > area.width()) {
1821+
int dropIdx = 2 + secondaryFKey - 2;
1822+
Span labelSpan = spans.remove(dropIdx + 1);
1823+
Span keySpan = spans.remove(dropIdx);
1824+
hintsWidth -= keySpan.width() + labelSpan.width();
1825+
secondaryFKey -= 2;
1826+
}
1827+
// Then drop tab-specific hints from the tail, keeping at least 4 spans
1828+
while (spans.size() > 4 && hintsWidth > area.width()) {
1829+
Span labelSpan = spans.remove(spans.size() - 1);
1830+
Span keySpan = spans.remove(spans.size() - 1);
1831+
hintsWidth -= keySpan.width() + labelSpan.width();
1832+
}
1833+
}
1834+
17361835
if (!rightSpans.isEmpty()) {
1737-
int hintsWidth = spans.stream().mapToInt(s -> s.width()).sum();
1738-
int rightWidth = rightSpans.stream().mapToInt(s -> s.width()).sum();
17391836
int gap = Math.max(1, area.width() - hintsWidth - rightWidth);
17401837
spans.add(Span.raw(" ".repeat(gap)));
17411838
spans.addAll(rightSpans);
@@ -1744,11 +1841,12 @@ private void renderFooter(Frame frame, Rect area) {
17441841
frame.renderWidget(Paragraph.from(Line.from(spans)), area);
17451842
}
17461843

1747-
private void insertFKeyHints(List<Span> spans) {
1844+
private int insertFKeyHints(List<Span> spans) {
17481845
int insertPos = Math.min(2, spans.size());
17491846
List<Span> fKeySpans = new ArrayList<>();
17501847
MonitorTab tab = activeTab();
1751-
if (tab != null && tab.getHelpText() != null) {
1848+
boolean hasHelp = tab != null && tab.getHelpText() != null;
1849+
if (hasHelp) {
17521850
hint(fKeySpans, "F1", "help");
17531851
}
17541852
hint(fKeySpans, "F2", "actions");
@@ -1757,15 +1855,17 @@ private void insertFKeyHints(List<Span> spans) {
17571855
}
17581856
hint(fKeySpans, "F6", "shell");
17591857
spans.addAll(insertPos, fKeySpans);
1858+
// Return count of secondary (droppable) spans: F2/F3/F6 only, not F1
1859+
return fKeySpans.size() - (hasHelp ? 2 : 0);
17601860
}
17611861

1762-
private void renderOverviewFooter(List<Span> spans) {
1862+
private int renderOverviewFooter(List<Span> spans) {
17631863
if (actionsPopup.isVisible()) {
17641864
actionsPopup.renderFooter(spans);
1765-
return;
1865+
return 0;
17661866
}
17671867
overviewTab.renderFooter(spans);
1768-
insertFKeyHints(spans);
1868+
int secondaryFKey = insertFKeyHints(spans);
17691869
// Process action hints
17701870
if (ctx.selectedPid != null && !isInfraSelected()) {
17711871
IntegrationInfo selInfo = findSelectedIntegration();
@@ -1786,6 +1886,7 @@ private void renderOverviewFooter(List<Span> spans) {
17861886
hint(spans, "x", "stop");
17871887
hint(spans, "X", "kill");
17881888
}
1889+
return secondaryFKey;
17891890
}
17901891

17911892
// ---- Data Loading ----

0 commit comments

Comments
 (0)