This is an automated email from the ASF dual-hosted git repository.
gnodet 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 ab0bc36add49 chore: shell panel UX improvements and JLine 4.3 API
migration (#24328)
ab0bc36add49 is described below
commit ab0bc36add4973b3f4d73bf8136f5ff0f8e45a62
Author: Guillaume Nodet <[email protected]>
AuthorDate: Wed Jul 1 09:54:09 2026 +0200
chore: shell panel UX improvements and JLine 4.3 API migration (#24328)
Shell panel UX and reliability improvements for the TUI monitor:
- Configurable split height (Shift+F6 cycles 25/50/75/100%)
- Scrollback history with PageUp/Down, mouse wheel, and scrollbar
- Auto-follow on new output or key input
- Simplified prompt (camel> instead of camel 4.21.0-SNAPSHOT>)
- Hardware cursor with position tracking for blinking
- Footer hints show action verb + current state: resize (50%)
- Exception logging via System.Logger in CamelMonitor
- Fullscreen mode at 100% split
- Uses JLine 4.3 public API (getHistory, getHistorySize, cell decoding)
instead of reflection on private fields
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
.../camel/dsl/jbang/core/commands/Shell.java | 10 +-
.../camel/dsl/jbang/core/commands/tui/AiPanel.java | 2 +-
.../dsl/jbang/core/commands/tui/CamelMonitor.java | 25 +++-
.../dsl/jbang/core/commands/tui/ShellPanel.java | 156 ++++++++++++---------
.../jbang/core/commands/tui/ShellPanelTest.java | 117 +++++++++++++---
5 files changed, 204 insertions(+), 106 deletions(-)
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Shell.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Shell.java
index 5d8a3e0b7ba7..68e8af4fcdbe 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Shell.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Shell.java
@@ -80,7 +80,7 @@ public class Shell extends CamelCommand {
// org.jline.shell.Shell is used via FQCN to avoid clash with this
class name
ShellBuilder builder = org.jline.shell.Shell.builder()
- .prompt(() -> buildPrompt(camelVersion, colorEnabled))
+ .prompt(() -> buildPrompt(colorEnabled))
.rightPrompt(() -> buildRightPrompt(colorEnabled))
.groups(registry, new PosixCommandGroup(), new
InteractiveCommandGroup())
.historyFile(history)
@@ -114,16 +114,12 @@ public class Shell extends CamelCommand {
return 0;
}
- private static String buildPrompt(String camelVersion, boolean
colorEnabled) {
+ private static String buildPrompt(boolean colorEnabled) {
if (!colorEnabled) {
- return camelVersion != null ? "camel " + camelVersion + "> " :
"camel> ";
+ return "camel> ";
}
AttributedStringBuilder sb = new AttributedStringBuilder();
sb.append("camel",
AttributedStyle.DEFAULT.bold().foregroundRgb(CAMEL_ORANGE));
- if (camelVersion != null) {
- sb.append(" ");
- sb.append(camelVersion,
AttributedStyle.DEFAULT.foreground(AttributedStyle.GREEN));
- }
sb.append("> ", AttributedStyle.DEFAULT);
return sb.toAnsi();
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
index 62cb48158807..f700cddaee5b 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
@@ -526,7 +526,7 @@ class AiPanel {
void renderFooter(List<Span> spans) {
MonitorContext.hint(spans, "F8", "close");
- MonitorContext.hint(spans, "Shift+F8", panelPercent() + "%");
+ MonitorContext.hint(spans, "Shift+F8", "resize (" +
SPLIT_PERCENTS[splitIndex] + "%)");
MonitorContext.hint(spans, "PgUp/Dn", "scroll");
if (!thinking.get()) {
MonitorContext.hint(spans, "Enter", "send");
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 6550316f5b4d..4a49103dde62 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
@@ -18,6 +18,8 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
import java.io.File;
import java.io.IOException;
+import java.lang.System.Logger;
+import java.lang.System.Logger.Level;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
@@ -69,6 +71,7 @@ import static
org.apache.camel.dsl.jbang.core.commands.tui.TabRegistry.*;
sortOptions = false)
public class CamelMonitor extends CamelCommand {
+ private static final Logger LOG =
System.getLogger(CamelMonitor.class.getName());
private static final long DEFAULT_REFRESH_MS = 100;
// Compact tab bar (10 labels + 9 "|" dividers) needs 88 chars — that is
the true minimum
@@ -800,12 +803,17 @@ public class CamelMonitor extends CamelCommand {
ctx.shellPercent = shellPanel.isOpen() ? shellPanel.panelPercent()
: aiPanel.isOpen() ? aiPanel.panelPercent() : 0;
if (shellPanel.isOpen()) {
- List<Rect> splitChunks = Layout.vertical()
- .constraints(Constraint.percentage(100 -
shellPanel.panelPercent()),
- Constraint.percentage(shellPanel.panelPercent()))
- .split(contentArea);
- renderContent(frame, splitChunks.get(0));
- shellPanel.render(frame, splitChunks.get(1));
+ if (shellPanel.panelPercent() >= 100) {
+ // At 100% the shell fills the entire content area
+ shellPanel.render(frame, contentArea);
+ } else {
+ List<Rect> splitChunks = Layout.vertical()
+ .constraints(Constraint.percentage(100 -
shellPanel.panelPercent()),
+
Constraint.percentage(shellPanel.panelPercent()))
+ .split(contentArea);
+ renderContent(frame, splitChunks.get(0));
+ shellPanel.render(frame, splitChunks.get(1));
+ }
} else if (aiPanel.isOpen()) {
List<Rect> splitChunks = Layout.vertical()
.constraints(Constraint.percentage(100 -
aiPanel.panelPercent()),
@@ -1141,6 +1149,7 @@ public class CamelMonitor extends CamelCommand {
try {
pid = Long.parseLong(ctx.selectedPid);
} catch (NumberFormatException e) {
+ LOG.log(Level.DEBUG, "Cannot parse selected PID: {0}",
ctx.selectedPid);
return;
}
if (isInfraSelected()) {
@@ -1184,6 +1193,7 @@ public class CamelMonitor extends CamelCommand {
try {
pid = Long.parseLong(ctx.selectedPid);
} catch (NumberFormatException e) {
+ LOG.log(Level.DEBUG, "Cannot parse selected PID for restart: {0}",
ctx.selectedPid);
return;
}
IntegrationInfo info = findSelectedIntegration();
@@ -1601,6 +1611,7 @@ public class CamelMonitor extends CamelCommand {
Files.writeString(path, json);
return path;
} catch (IOException e) {
+ LOG.log(Level.WARNING, "Failed to write .mcp.json: {0}",
e.getMessage());
return null;
}
}
@@ -1610,7 +1621,7 @@ public class CamelMonitor extends CamelCommand {
try {
Files.deleteIfExists(path);
} catch (IOException e) {
- // best effort
+ LOG.log(Level.DEBUG, "Failed to delete .mcp.json: {0}",
e.getMessage());
}
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanel.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanel.java
index d8a0f98d47bf..8eea84b52b23 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanel.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanel.java
@@ -18,13 +18,15 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
import java.io.IOException;
import java.io.OutputStream;
-import java.lang.reflect.Field;
+import java.lang.System.Logger;
+import java.lang.System.Logger.Level;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Layout;
import dev.tamboui.layout.Rect;
import dev.tamboui.style.AnsiColor;
import dev.tamboui.style.Color;
@@ -43,10 +45,11 @@ 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.Scrollbar;
+import dev.tamboui.widgets.scrollbar.ScrollbarState;
import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
import org.apache.camel.dsl.jbang.core.common.EnvironmentHelper;
import org.apache.camel.dsl.jbang.core.common.Printer;
-import org.apache.camel.dsl.jbang.core.common.VersionHelper;
import org.jline.builtins.InteractiveCommandGroup;
import org.jline.builtins.PosixCommandGroup;
import org.jline.picocli.PicocliCommandRegistry;
@@ -74,6 +77,7 @@ import org.jline.utils.ScreenTerminalOutputStream;
*/
class ShellPanel {
+ private static final Logger LOG =
System.getLogger(ShellPanel.class.getName());
private static final int[] SPLIT_PERCENTS = { 25, 50, 75, 100 };
private static final int MOUSE_SCROLL_LINES = 3;
@@ -85,10 +89,14 @@ class ShellPanel {
private LineDisciplineTerminal virtualTerminal;
private Thread shellThread;
+ private final ScrollbarState scrollbarState = new ScrollbarState();
+
private int lastWidth;
private int lastHeight;
private int scrollOffset;
private int lastHistorySize;
+ private int lastCursorX = -1;
+ private int lastCursorY = -1;
private Rect lastArea;
private volatile boolean shellExited;
@@ -140,7 +148,7 @@ class ShellPanel {
// PageUp/Down for scrollback through history
if (ke.isKey(KeyCode.PAGE_UP)) {
- int histSize = screenTerminal != null ?
getHistorySize(screenTerminal) : 0;
+ int histSize = screenTerminal != null ?
screenTerminal.getHistorySize() : 0;
scrollOffset = Math.min(scrollOffset + lastHeight, histSize);
return true;
}
@@ -159,8 +167,12 @@ class ShellPanel {
if (bytes != null && bytes.length > 0) {
virtualTerminal.processInputBytes(bytes);
}
- } catch (IOException | ArrayIndexOutOfBoundsException e) {
- // terminal closed or buffer resized concurrently
+ } catch (IOException e) {
+ // terminal closed — expected during shutdown
+ LOG.log(Level.DEBUG, "Terminal I/O error forwarding key
event", e);
+ } catch (ArrayIndexOutOfBoundsException e) {
+ // ScreenTerminal buffer resized concurrently
+ LOG.log(Level.DEBUG, "Buffer resize race during key
forwarding", e);
}
}
return true;
@@ -177,7 +189,7 @@ class ShellPanel {
return false;
}
if (me.kind() == MouseEventKind.SCROLL_UP) {
- int histSize = screenTerminal != null ?
getHistorySize(screenTerminal) : 0;
+ int histSize = screenTerminal != null ?
screenTerminal.getHistorySize() : 0;
scrollOffset = Math.min(scrollOffset + MOUSE_SCROLL_LINES,
histSize);
return true;
}
@@ -246,22 +258,17 @@ class ShellPanel {
screenTerminal.dump(screen, cursor);
} catch (ArrayIndexOutOfBoundsException e) {
// buffer resized concurrently — skip this frame
+ LOG.log(Level.DEBUG, "Buffer resize race during screen dump —
skipping frame", e);
return;
}
// Auto-follow: reset scroll when new history appears
- int histSize = getHistorySize(screenTerminal);
+ int histSize = screenTerminal.getHistorySize();
if (histSize > lastHistorySize && scrollOffset > 0) {
scrollOffset = 0;
}
lastHistorySize = histSize;
- // Show a block cursor by toggling the reversed attribute on the cell
at the cursor position
- if (scrollOffset == 0 && cursor[1] >= 0 && cursor[1] < innerHeight
- && cursor[0] >= 0 && cursor[0] < innerWidth) {
- screen[cursor[1] * innerWidth + cursor[0]] ^= (1L << 57);
- }
-
List<Line> lines;
if (scrollOffset > 0) {
lines = renderScrolledView(screen, innerWidth, innerHeight);
@@ -269,17 +276,54 @@ class ShellPanel {
lines = renderLiveView(screen, innerWidth, innerHeight);
}
+ // Split the inner area: content (fill) + scrollbar (1 col) when
history exists
+ int totalLines = histSize + innerHeight;
+ boolean showScrollbar = totalLines > innerHeight;
+ Rect contentArea;
+ if (showScrollbar) {
+ List<Rect> hChunks = Layout.horizontal()
+ .constraints(Constraint.fill(), Constraint.length(1))
+ .split(inner);
+ contentArea = hChunks.get(0);
+
+ // Map scrollOffset (lines-from-bottom) to top-down position for
ScrollbarState
+ int viewStart = Math.max(0, totalLines - scrollOffset -
innerHeight);
+ scrollbarState
+ .contentLength(totalLines)
+ .viewportContentLength(innerHeight)
+ .position(viewStart);
+ frame.renderStatefulWidget(Scrollbar.builder().build(),
hChunks.get(1), scrollbarState);
+ } else {
+ contentArea = inner;
+ }
+
frame.renderWidget(
Paragraph.builder()
.text(Text.from(lines))
.overflow(Overflow.CLIP)
.build(),
- inner);
+ contentArea);
+
+ // Position the hardware cursor only when it has moved, so the
terminal's
+ // blink timer is not reset on every frame.
+ if (scrollOffset == 0 && cursor[1] >= 0 && cursor[1] < innerHeight
+ && cursor[0] >= 0 && cursor[0] < innerWidth) {
+ int cx = contentArea.x() + cursor[0];
+ int cy = contentArea.y() + cursor[1];
+ if (cx != lastCursorX || cy != lastCursorY) {
+ frame.setCursorPosition(cx, cy);
+ lastCursorX = cx;
+ lastCursorY = cy;
+ }
+ } else {
+ lastCursorX = -1;
+ lastCursorY = -1;
+ }
}
void renderFooter(List<Span> spans) {
MonitorContext.hint(spans, "F6", "close");
- MonitorContext.hint(spans, "Shift+F6", SPLIT_PERCENTS[splitIndex] +
"%");
+ MonitorContext.hint(spans, "Shift+F6", "resize (" +
SPLIT_PERCENTS[splitIndex] + "%)");
MonitorContext.hint(spans, "PgUp/Dn", "scroll");
}
@@ -292,7 +336,7 @@ class ShellPanel {
}
private List<Line> renderScrolledView(long[] screen, int width, int
height) {
- List<long[]> history = getHistory(screenTerminal);
+ List<long[]> history = screenTerminal.getHistory();
if (history.isEmpty()) {
return renderLiveView(screen, width, height);
}
@@ -323,20 +367,19 @@ class ShellPanel {
int col = 0;
while (col < width) {
long cell = buffer[offset + col];
- int ch = (int) (cell & 0xffffffffL);
- long attr = cell >>> 32;
- Style style = convertAttrToStyle(attr);
+ int ch = ScreenTerminal.cellCodePoint(cell);
+ long attr = ScreenTerminal.cellAttr(cell);
+ Style style = convertCellToStyle(cell);
StringBuilder sb = new StringBuilder();
sb.appendCodePoint(ch == 0 ? ' ' : ch);
int nextCol = col + 1;
while (nextCol < width) {
long nextCell = buffer[offset + nextCol];
- long nextAttr = nextCell >>> 32;
- if (nextAttr != attr) {
+ if (ScreenTerminal.cellAttr(nextCell) != attr) {
break;
}
- int nextCh = (int) (nextCell & 0xffffffffL);
+ int nextCh = ScreenTerminal.cellCodePoint(nextCell);
sb.appendCodePoint(nextCh == 0 ? ' ' : nextCh);
nextCol++;
}
@@ -391,8 +434,6 @@ class ShellPanel {
return "Camel";
}
};
- String camelVersion = VersionHelper.extractCamelVersion();
-
// Redirect command output (printer()) through the virtual terminal
// so it renders in the shell panel instead of the TUI's real
terminal
CamelJBangMain main = (CamelJBangMain)
CamelJBangMain.getCommandLine().getCommand();
@@ -431,7 +472,7 @@ class ShellPanel {
try (Shell shell = Shell.builder()
.terminal(terminal)
- .prompt(() -> buildPrompt(camelVersion))
+ .prompt(ShellPanel::buildPrompt)
.groups(registry, new PosixCommandGroup(), new
InteractiveCommandGroup())
.historyCommands(true)
.helpCommands(true)
@@ -456,13 +497,9 @@ class ShellPanel {
}
}
- private static String buildPrompt(String camelVersion) {
+ private static String buildPrompt() {
AttributedStringBuilder sb = new AttributedStringBuilder();
sb.append("camel",
AttributedStyle.DEFAULT.bold().foregroundRgb(0xF69123));
- if (camelVersion != null) {
- sb.append(" ");
- sb.append(camelVersion,
AttributedStyle.DEFAULT.foreground(AttributedStyle.GREEN));
- }
sb.append("> ", AttributedStyle.DEFAULT);
return sb.toAnsi();
}
@@ -476,49 +513,41 @@ class ShellPanel {
try {
virtualTerminal.close();
} catch (IOException e) {
- // ignore
+ LOG.log(Level.DEBUG, "Error closing virtual terminal during
shutdown", e);
}
virtualTerminal = null;
}
screenTerminal = null;
}
- // Attribute mask from ScreenTerminal:
- // 0xYXFFFBBB00000000L
- // X: Bit 0=Underline, Bit 1=Negative, Bit 2=Concealed, Bit 3=Bold
- // Y: Bit 0=FG set, Bit 1=BG set, Bit 2=Dim, Bit 3=Italic
- // F: Foreground r-g-b (3 hex nibbles)
- // B: Background r-g-b (3 hex nibbles)
- static Style convertAttrToStyle(long attr) {
+ /**
+ * Converts a {@link ScreenTerminal} 64-bit cell value into a TamboUI
{@link Style}, using JLine's public
+ * cell-decoding helpers.
+ */
+ static Style convertCellToStyle(long cell) {
Style style = Style.EMPTY;
- int x = (int) ((attr >> 24) & 0xF);
- int y = (int) ((attr >> 28) & 0xF);
-
- if ((x & 0x8) != 0) {
+ if (ScreenTerminal.cellBold(cell)) {
style = style.bold();
}
- if ((x & 0x1) != 0) {
+ if (ScreenTerminal.cellUnderline(cell)) {
style = style.underlined();
}
- if ((x & 0x2) != 0) {
+ if (ScreenTerminal.cellInverse(cell)) {
style = style.reversed();
}
- if ((y & 0x4) != 0) {
+ if (ScreenTerminal.cellDim(cell)) {
style = style.dim();
}
- if ((y & 0x8) != 0) {
+ if (ScreenTerminal.cellItalic(cell)) {
style = style.italic();
}
- // Foreground color (if set)
- if ((y & 0x1) != 0) {
- style = style.fg(resolveColor((int) ((attr >> 12) & 0xFFF)));
+ if (ScreenTerminal.cellHasForeground(cell)) {
+ style =
style.fg(resolveColor(ScreenTerminal.cellForeground(cell)));
}
-
- // Background color (if set)
- if ((y & 0x2) != 0) {
- style = style.bg(resolveColor((int) (attr & 0xFFF)));
+ if (ScreenTerminal.cellHasBackground(cell)) {
+ style =
style.bg(resolveColor(ScreenTerminal.cellBackground(cell)));
}
return style;
@@ -616,22 +645,6 @@ class ShellPanel {
};
}
- @SuppressWarnings("unchecked")
- private static List<long[]> getHistory(ScreenTerminal st) {
- try {
- Field f = ScreenTerminal.class.getDeclaredField("history");
- f.setAccessible(true);
- List<long[]> history = (List<long[]>) f.get(st);
- return history != null ? history : Collections.emptyList();
- } catch (Exception e) {
- return Collections.emptyList();
- }
- }
-
- private static int getHistorySize(ScreenTerminal st) {
- return getHistory(st).size();
- }
-
private static class DelegateOutputStream extends OutputStream {
volatile OutputStream delegate;
@@ -642,6 +655,7 @@ class ShellPanel {
delegate.write(b);
} catch (ArrayIndexOutOfBoundsException e) {
// ScreenTerminal buffer resized concurrently — safe to
ignore
+ LOG.log(Level.TRACE, "Buffer resize race in write(int)",
e);
}
}
}
@@ -653,6 +667,7 @@ class ShellPanel {
delegate.write(b, off, len);
} catch (ArrayIndexOutOfBoundsException e) {
// ScreenTerminal buffer resized concurrently — safe to
ignore
+ LOG.log(Level.TRACE, "Buffer resize race in
write(byte[])", e);
}
}
}
@@ -664,6 +679,7 @@ class ShellPanel {
delegate.flush();
} catch (ArrayIndexOutOfBoundsException e) {
// ScreenTerminal buffer resized concurrently — safe to
ignore
+ LOG.log(Level.TRACE, "Buffer resize race in flush()", e);
}
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanelTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanelTest.java
index 18f5c5ab0cc9..a59f197c7fa7 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanelTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanelTest.java
@@ -17,6 +17,7 @@
package org.apache.camel.dsl.jbang.core.commands.tui;
import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
import java.util.List;
import dev.tamboui.style.Modifier;
@@ -256,64 +257,130 @@ class ShellPanelTest {
assertNull(result);
}
- // ---- convertAttrToStyle tests ----
+ // ---- convertCellToStyle tests ----
+ // convertCellToStyle takes a full 64-bit cell (attr in upper 32,
codepoint in lower 32).
+ // Helper to build a cell from a 32-bit attr value.
+ private static long cellWithAttr(long attr) {
+ return attr << 32;
+ }
@Test
- void convertAttrToStyleNoFlags() {
- Style style = ShellPanel.convertAttrToStyle(0);
+ void convertCellToStyleNoFlags() {
+ Style style = ShellPanel.convertCellToStyle(cellWithAttr(0));
assertTrue(style.effectiveModifiers().isEmpty());
}
@Test
- void convertAttrToStyleBold() {
+ void convertCellToStyleBold() {
// Bold = bit 3 of X nibble (bits 24-27)
- long attr = 0x08000000L;
- Style style = ShellPanel.convertAttrToStyle(attr);
+ Style style = ShellPanel.convertCellToStyle(cellWithAttr(0x08000000L));
assertTrue(style.effectiveModifiers().contains(Modifier.BOLD));
}
@Test
- void convertAttrToStyleUnderline() {
- long attr = 0x01000000L;
- Style style = ShellPanel.convertAttrToStyle(attr);
+ void convertCellToStyleUnderline() {
+ Style style = ShellPanel.convertCellToStyle(cellWithAttr(0x01000000L));
assertTrue(style.effectiveModifiers().contains(Modifier.UNDERLINED));
}
@Test
- void convertAttrToStyleReversed() {
- long attr = 0x02000000L;
- Style style = ShellPanel.convertAttrToStyle(attr);
+ void convertCellToStyleReversed() {
+ Style style = ShellPanel.convertCellToStyle(cellWithAttr(0x02000000L));
assertTrue(style.effectiveModifiers().contains(Modifier.REVERSED));
}
@Test
- void convertAttrToStyleDim() {
+ void convertCellToStyleDim() {
// Dim = bit 2 of Y nibble (bits 28-31) → 0x4 << 28
- long attr = 0x40000000L;
- Style style = ShellPanel.convertAttrToStyle(attr);
+ Style style = ShellPanel.convertCellToStyle(cellWithAttr(0x40000000L));
assertTrue(style.effectiveModifiers().contains(Modifier.DIM));
}
@Test
- void convertAttrToStyleItalic() {
+ void convertCellToStyleItalic() {
// Italic = bit 3 of Y nibble → 0x8 << 28
- long attr = 0x80000000L;
- Style style = ShellPanel.convertAttrToStyle(attr);
+ Style style = ShellPanel.convertCellToStyle(cellWithAttr(0x80000000L));
assertTrue(style.effectiveModifiers().contains(Modifier.ITALIC));
}
@Test
- void convertAttrToStyleCombinedFgBgBoldItalic() {
+ void convertCellToStyleCombinedFgBgBoldItalic() {
// Y = 0xB (FG set + BG set + italic: bits 0+1+3), X = 0x8 (bold)
// FFF = 0xF00 (red FG), BBB = 0x080 (green BG)
- long attr = 0xB8F00080L;
- Style style = ShellPanel.convertAttrToStyle(attr);
+ Style style = ShellPanel.convertCellToStyle(cellWithAttr(0xB8F00080L));
assertTrue(style.effectiveModifiers().contains(Modifier.BOLD));
assertTrue(style.effectiveModifiers().contains(Modifier.ITALIC));
assertTrue(style.fg().isPresent());
assertTrue(style.bg().isPresent());
}
+ // ---- panelPercent / cycleHeight tests ----
+
+ @Test
+ void panelPercentDefaultIs50() {
+ ShellPanel panel = new ShellPanel();
+ assertEquals(50, panel.panelPercent());
+ }
+
+ @Test
+ void cycleHeightCyclesThroughPercents() {
+ ShellPanel panel = new ShellPanel();
+ // Default is 50% (index 1)
+ assertEquals(50, panel.panelPercent());
+
+ panel.cycleHeight();
+ assertEquals(75, panel.panelPercent());
+
+ panel.cycleHeight();
+ assertEquals(100, panel.panelPercent());
+
+ panel.cycleHeight();
+ assertEquals(25, panel.panelPercent());
+
+ panel.cycleHeight();
+ assertEquals(50, panel.panelPercent()); // wraps around
+ }
+
+ // ---- renderFooter tests ----
+
+ @Test
+ void renderFooterShowsCurrentPercentage() {
+ ShellPanel panel = new ShellPanel();
+ // Default split is 50%
+ List<Span> spans = new ArrayList<>();
+ panel.renderFooter(spans);
+
+ String footer = spansToString(spans);
+ assertTrue(footer.contains("resize (50%)"), "Footer should show
'resize (50%)'");
+ }
+
+ @Test
+ void renderFooterUpdatesAfterCycleHeight() {
+ ShellPanel panel = new ShellPanel();
+ panel.cycleHeight(); // now 75%
+
+ List<Span> spans = new ArrayList<>();
+ panel.renderFooter(spans);
+
+ String footer = spansToString(spans);
+ assertTrue(footer.contains("resize (75%)"), "Footer should show
'resize (75%)' after cycling once");
+ }
+
+ @Test
+ void renderFooterContainsExpectedHints() {
+ ShellPanel panel = new ShellPanel();
+ List<Span> spans = new ArrayList<>();
+ panel.renderFooter(spans);
+
+ String footer = spansToString(spans);
+ assertTrue(footer.contains("F6"), "Footer should contain F6 hint");
+ assertTrue(footer.contains("close"), "Footer should contain 'close'
label for F6");
+ assertTrue(footer.contains("Shift+F6"), "Footer should contain
Shift+F6 hint");
+ assertTrue(footer.contains("resize"), "Footer should contain 'resize'
action label");
+ assertTrue(footer.contains("PgUp/Dn"), "Footer should contain PgUp/Dn
hint");
+ assertTrue(footer.contains("scroll"), "Footer should contain 'scroll'
label");
+ }
+
private static String rawContent(Line line) {
StringBuilder sb = new StringBuilder();
for (Span span : line.spans()) {
@@ -321,4 +388,12 @@ class ShellPanelTest {
}
return sb.toString();
}
+
+ private static String spansToString(List<Span> spans) {
+ StringBuilder sb = new StringBuilder();
+ for (Span span : spans) {
+ sb.append(span.content());
+ }
+ return sb.toString();
+ }
}