davsclaus commented on code in PR #23605:
URL: https://github.com/apache/camel/pull/23605#discussion_r3320598078


##########
dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanel.java:
##########
@@ -0,0 +1,401 @@
+/*
+ * 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.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Layout;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Color;
+import dev.tamboui.style.Overflow;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.text.Text;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.widgets.paragraph.Paragraph;
+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.VersionHelper;
+import org.jline.builtins.InteractiveCommandGroup;
+import org.jline.builtins.PosixCommandGroup;
+import org.jline.builtins.ScreenTerminal;
+import org.jline.builtins.ScreenTerminalOutputStream;
+import org.jline.picocli.PicocliCommandRegistry;
+import org.jline.reader.LineReader;
+import org.jline.terminal.Size;
+import org.jline.terminal.impl.LineDisciplineTerminal;
+import org.jline.utils.AttributedStringBuilder;
+import org.jline.utils.AttributedStyle;
+
+class ShellPanel {
+
+    private boolean visible;
+
+    private ScreenTerminal screenTerminal;
+    private LineDisciplineTerminal virtualTerminal;
+    private Thread shellThread;
+
+    private int lastWidth;
+    private int lastHeight;
+
+    boolean isOpen() {
+        return visible;
+    }
+
+    void open() {
+        if (visible) {
+            return;
+        }
+        visible = true;
+    }
+
+    void close() {
+        visible = false;
+        stopShell();
+    }
+
+    boolean handleKeyEvent(KeyEvent ke) {
+        if (!visible) {
+            return false;
+        }
+
+        // F3 closes the shell panel
+        if (ke.isKey(KeyCode.F3)) {
+            close();
+            return true;
+        }
+
+        // Forward everything else to the virtual terminal
+        if (virtualTerminal != null) {
+            try {
+                byte[] bytes = encodeKeyEvent(ke);
+                if (bytes != null && bytes.length > 0) {
+                    virtualTerminal.processInputBytes(bytes);
+                }
+            } catch (IOException e) {
+                // terminal closed
+            }
+        }
+        return true;
+    }
+
+    void render(Frame frame, Rect area) {
+        if (!visible) {
+            return;
+        }
+
+        // Reserve 1 row for separator line at the top
+        int innerWidth = area.width();
+        int innerHeight = area.height() - 1;
+
+        // Start shell on first render (we now know the size)
+        if (screenTerminal == null && innerWidth > 2 && innerHeight > 2) {
+            startShell(innerWidth, innerHeight);
+        }
+
+        // Handle resize
+        if (screenTerminal != null && (innerWidth != lastWidth || innerHeight 
!= lastHeight)) {
+            screenTerminal.setSize(innerWidth, innerHeight);
+            if (virtualTerminal != null) {
+                virtualTerminal.setSize(new Size(innerWidth, innerHeight));
+            }
+            lastWidth = innerWidth;
+            lastHeight = innerHeight;
+        }
+
+        if (screenTerminal == null) {
+            frame.renderWidget(
+                    Paragraph.from(Line.from(Span.styled("Starting shell...", 
Style.EMPTY.dim()))),
+                    area);
+            return;
+        }
+
+        // Dump screen buffer
+        long[] screen = new long[innerWidth * innerHeight];
+        int[] cursor = new int[2];
+        screenTerminal.dump(screen, cursor);
+
+        // Convert to TamboUI lines
+        List<Line> lines = new ArrayList<>(innerHeight);
+        for (int row = 0; row < innerHeight; row++) {
+            List<Span> spans = new ArrayList<>();
+            int col = 0;
+            while (col < innerWidth) {
+                long cell = screen[row * innerWidth + col];
+                int ch = (int) (cell & 0xffffffffL);
+                long attr = cell >>> 32;
+                Style style = convertAttrToStyle(attr);
+
+                // Merge consecutive cells with same attributes
+                StringBuilder sb = new StringBuilder();
+                sb.appendCodePoint(ch == 0 ? ' ' : ch);
+                int nextCol = col + 1;
+                while (nextCol < innerWidth) {
+                    long nextCell = screen[row * innerWidth + nextCol];
+                    long nextAttr = nextCell >>> 32;
+                    if (nextAttr != attr) {
+                        break;
+                    }
+                    int nextCh = (int) (nextCell & 0xffffffffL);
+                    sb.appendCodePoint(nextCh == 0 ? ' ' : nextCh);
+                    nextCol++;
+                }
+                spans.add(Span.styled(sb.toString(), style));
+                col = nextCol;
+            }
+            lines.add(Line.from(spans));
+        }
+
+        // Render separator line with title
+        List<Rect> chunks = Layout.vertical()
+                .constraints(Constraint.length(1), Constraint.fill())
+                .split(area);
+        String sep = "─".repeat(Math.max(0, innerWidth - 8));
+        frame.renderWidget(
+                Paragraph.from(Line.from(
+                        Span.styled("── ", Style.EMPTY.dim()),
+                        Span.styled("Shell", Style.EMPTY.bold()),
+                        Span.styled(" " + sep, Style.EMPTY.dim()))),
+                chunks.get(0));
+
+        // Render shell content
+        frame.renderWidget(
+                Paragraph.builder()
+                        .text(Text.from(lines))
+                        .overflow(Overflow.CLIP)
+                        .build(),
+                chunks.get(1));
+    }
+
+    void renderFooter(List<Span> spans) {
+        MonitorContext.hint(spans, "F3", "close");
+    }
+
+    private void startShell(int width, int height) {
+        try {
+            screenTerminal = new ScreenTerminal(width, height);
+            lastWidth = width;
+            lastHeight = height;
+
+            // Delegate OutputStream to break the circular dependency:
+            // LineDisciplineTerminal needs masterOutput at construction,
+            // but ScreenTerminalOutputStream needs the terminal for feedback.
+            DelegateOutputStream delegateOut = new DelegateOutputStream();
+            virtualTerminal = new LineDisciplineTerminal(
+                    "tui-shell", "screen-256color", delegateOut, 
StandardCharsets.UTF_8);
+            virtualTerminal.setSize(new Size(width, height));
+
+            // Feedback loop: VT100 responses go back as terminal input
+            OutputStream feedbackOutput = new OutputStream() {
+                @Override
+                public void write(int b) throws IOException {
+                    virtualTerminal.processInputByte(b);
+                }
+            };
+            delegateOut.delegate = new ScreenTerminalOutputStream(
+                    screenTerminal, StandardCharsets.UTF_8, feedbackOutput);
+
+            shellThread = new Thread(() -> runShell(virtualTerminal), 
"tui-shell");
+            shellThread.setDaemon(true);
+            shellThread.start();
+        } catch (Exception e) {
+            screenTerminal = null;
+            virtualTerminal = null;
+        }
+    }
+
+    private void runShell(LineDisciplineTerminal terminal) {
+        try {
+            PicocliCommandRegistry registry = new 
PicocliCommandRegistry(CamelJBangMain.getCommandLine());
+            String camelVersion = VersionHelper.extractCamelVersion();
+
+            try (org.jline.shell.Shell shell = org.jline.shell.Shell.builder()
+                    .terminal(terminal)
+                    .prompt(() -> buildPrompt(camelVersion))
+                    .groups(registry, new PosixCommandGroup(), new 
InteractiveCommandGroup())
+                    .historyCommands(true)
+                    .helpCommands(true)
+                    .commandHighlighter(true)
+                    .variable(LineReader.LIST_MAX, 50)
+                    .build()) {
+                EnvironmentHelper.setActiveTerminal(terminal);
+                shell.run();
+            } finally {
+                EnvironmentHelper.setActiveTerminal(null);
+            }
+        } catch (Exception e) {
+            // shell exited
+        }
+    }
+
+    private static String buildPrompt(String camelVersion) {
+        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();
+    }
+
+    private void stopShell() {
+        if (shellThread != null) {
+            shellThread.interrupt();
+            shellThread = null;
+        }
+        if (virtualTerminal != null) {
+            try {
+                virtualTerminal.close();
+            } catch (IOException e) {
+                // ignore
+            }
+            virtualTerminal = null;
+        }
+        screenTerminal = null;

Review Comment:
   FQCN: `org.jline.shell.Shell` should be imported rather than used inline 
(per project convention in CLAUDE.md). The OpenRewrite build plugin will 
auto-fix this on `mvn clean install`, so just make sure to rebuild before 
pushing.
   
   ```suggestion
               try (Shell shell = Shell.builder()
   ```
   
   (with `import org.jline.shell.Shell;` added to the imports block)



##########
dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanel.java:
##########
@@ -0,0 +1,401 @@
+/*
+ * 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.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Layout;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Color;
+import dev.tamboui.style.Overflow;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.text.Text;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.widgets.paragraph.Paragraph;
+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.VersionHelper;
+import org.jline.builtins.InteractiveCommandGroup;
+import org.jline.builtins.PosixCommandGroup;
+import org.jline.builtins.ScreenTerminal;
+import org.jline.builtins.ScreenTerminalOutputStream;
+import org.jline.picocli.PicocliCommandRegistry;
+import org.jline.reader.LineReader;
+import org.jline.terminal.Size;
+import org.jline.terminal.impl.LineDisciplineTerminal;
+import org.jline.utils.AttributedStringBuilder;
+import org.jline.utils.AttributedStyle;
+
+class ShellPanel {
+
+    private boolean visible;
+
+    private ScreenTerminal screenTerminal;
+    private LineDisciplineTerminal virtualTerminal;
+    private Thread shellThread;
+
+    private int lastWidth;
+    private int lastHeight;
+
+    boolean isOpen() {
+        return visible;
+    }
+
+    void open() {
+        if (visible) {
+            return;
+        }
+        visible = true;
+    }
+
+    void close() {
+        visible = false;
+        stopShell();
+    }
+
+    boolean handleKeyEvent(KeyEvent ke) {
+        if (!visible) {
+            return false;
+        }
+
+        // F3 closes the shell panel
+        if (ke.isKey(KeyCode.F3)) {
+            close();
+            return true;
+        }
+
+        // Forward everything else to the virtual terminal
+        if (virtualTerminal != null) {
+            try {
+                byte[] bytes = encodeKeyEvent(ke);
+                if (bytes != null && bytes.length > 0) {
+                    virtualTerminal.processInputBytes(bytes);
+                }
+            } catch (IOException e) {
+                // terminal closed
+            }
+        }
+        return true;
+    }
+
+    void render(Frame frame, Rect area) {
+        if (!visible) {
+            return;
+        }
+
+        // Reserve 1 row for separator line at the top
+        int innerWidth = area.width();
+        int innerHeight = area.height() - 1;
+
+        // Start shell on first render (we now know the size)
+        if (screenTerminal == null && innerWidth > 2 && innerHeight > 2) {
+            startShell(innerWidth, innerHeight);
+        }
+
+        // Handle resize

Review Comment:
   Minor: `close()` calls `stopShell()` which destroys the virtual terminal and 
shell thread. The PR test plan mentions "Reopen shell → verify previous session 
state", but reopening creates a fresh shell with no prior state. Consider 
updating the test plan description, or alternatively only setting `visible = 
false` without destroying the shell (and deferring `stopShell()` to TUI exit).



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to