diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalLayoutModel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalLayoutModel.java index cc5ff995ee..cbc6c14c97 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalLayoutModel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalLayoutModel.java @@ -29,9 +29,11 @@ import docking.widgets.fieldpanel.listener.IndexMapper; import docking.widgets.fieldpanel.listener.LayoutModelListener; import docking.widgets.fieldpanel.support.FieldLocation; import docking.widgets.fieldpanel.support.FieldRange; +import generic.theme.Gui; import ghidra.app.plugin.core.terminal.vt.*; import ghidra.app.plugin.core.terminal.vt.AnsiColorResolver.ReverseVideo; import ghidra.app.plugin.core.terminal.vt.VtCharset.G; +import ghidra.framework.Application; import ghidra.util.*; /** @@ -94,6 +96,7 @@ public class TerminalLayoutModel implements LayoutModel, VtHandler { // Flags for what's been enabled protected boolean showCursor; protected boolean bracketedPaste; + protected boolean themeChangeNotification; protected boolean win32InputMode; // not implemented protected boolean reportMousePress; protected boolean reportMouseRelease; @@ -109,7 +112,6 @@ public class TerminalLayoutModel implements LayoutModel, VtHandler { * @param panel the panel to receive commands from the model's VT/ANSI parser * @param charset the charset for decoding bytes to characters * @param metrics font metrics for the monospaced terminal font - * @param fontSizeAdjustment the font size adjustment * @param colors a resolver for ANSI colors */ public TerminalLayoutModel(TerminalPanel panel, Charset charset, FontMetrics metrics, @@ -140,6 +142,7 @@ public class TerminalLayoutModel implements LayoutModel, VtHandler { buffer = bufPrimary; bracketedPaste = false; + themeChangeNotification = false; win32InputMode = false; reportMousePress = false; reportMouseRelease = false; @@ -221,7 +224,7 @@ public class TerminalLayoutModel implements LayoutModel, VtHandler { if (i < layouts.size()) { TerminalLayout layout = layouts.get(i); if (layout.line == line) { - return; // Already checked for line.clearDirty() + return; } layout = layoutCache.computeIfAbsent(line, this::newLayout); layouts.set(i, layout); @@ -285,8 +288,24 @@ public class TerminalLayoutModel implements LayoutModel, VtHandler { while (cb.hasRemaining()) { try { // A little strange using both unicode and vt charsets.... - buffer.putChar(curVtCharset.mapChar(cb.get())); - buffer.moveCursorRight(1, true, showCursor); + char maybeHighSurrogate = cb.get(); + if (Character.isHighSurrogate(maybeHighSurrogate)) { + char betterBeLowSurrogate = cb.get(); + if (Character.isLowSurrogate(betterBeLowSurrogate)) { + buffer.putCodePoint(curVtCharset.mapCodePoint( + Character.toCodePoint(maybeHighSurrogate, betterBeLowSurrogate))); + buffer.moveCursorRight(1, true, showCursor); + } + else { + buffer.putCodePoint(curVtCharset.mapCodePoint(maybeHighSurrogate)); + buffer.putCodePoint(curVtCharset.mapCodePoint(betterBeLowSurrogate)); + buffer.moveCursorRight(2, true, showCursor); + } + } + else { + buffer.putCodePoint(curVtCharset.mapCodePoint(maybeHighSurrogate)); + buffer.moveCursorRight(1, true, showCursor); + } } catch (Throwable t) { Msg.error(this, "Error handling character: " + t, t); @@ -499,7 +518,23 @@ public class TerminalLayoutModel implements LayoutModel, VtHandler { public void handleBracketedPasteMode(boolean en) { this.bracketedPaste = en; } - + + @Override + public void handleThemeChangeNotification(boolean en) { + this.themeChangeNotification = en; + } + + @Override + public void handleQueryTheme() { + panel.responseEncoder.reportDarkMode(Gui.isDarkTheme()); + } + + @Override + public void handleXTVersion() { + panel.responseEncoder.reportXTVersion("GhidraTerminal", + Application.getApplicationVersion()); + } + @Override public void handleWin32InputMode(boolean en) { this.win32InputMode = en; diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalPanel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalPanel.java index 0f7ad43bdb..5c7e151d22 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalPanel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalPanel.java @@ -381,6 +381,9 @@ public class TerminalPanel extends JPanel implements FieldLocationListener, Fiel public void themeChanged(ThemeEvent event) { if (event.isLookAndFeelChanged()) { setFont(Gui.getFont(DEFAULT_FONT_ID)); + if (model.themeChangeNotification) { + responseEncoder.reportDarkMode(Gui.isDarkTheme()); + } } if (event.isFontChanged(DEFAULT_FONT_ID)) { setFont(Gui.getFont(DEFAULT_FONT_ID)); @@ -775,8 +778,13 @@ public class TerminalPanel extends JPanel implements FieldLocationListener, Fiel return; } Rectangle bounds = scroller.getViewportBorderBounds(); - int cols = bounds.width / metrics.charWidth('M'); - int rows = bounds.height / metrics.getHeight(); + int mWidth = metrics.charWidth('M'); + int mHeight = metrics.getHeight(); + if (mWidth == 0 || mHeight == 0) { + return; + } + int cols = bounds.width / mWidth; + int rows = bounds.height / mHeight; resizeTerminal((short) cols, (short) rows); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalTextFieldElement.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalTextFieldElement.java index adcf731613..07610ace05 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalTextFieldElement.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/TerminalTextFieldElement.java @@ -29,7 +29,7 @@ import ghidra.app.plugin.core.terminal.vt.*; * A text field element for rendering a full line of terminal text * *

- * {@link TerminalTextFields} are populated by a single element. The typical pattern seems to be to + * {@link TerminalTextField}s are populated by a single element. The typical pattern seems to be to * create a separate element for each bit of text having common attributes. This pattern would * generate quite a bit of garbage, since the terminal contents change frequently. Every time a line * content changed, we'd have to re-construct the elements. Instead, we use a single re-usable @@ -103,7 +103,9 @@ public class TerminalTextFieldElement implements FieldElement { @Override public char charAt(int index) { - return line.getChar(index); + StringBuilder sb = new StringBuilder(); + line.gatherText(sb, 0, line.length()); + return sb.charAt(index); } @Override @@ -162,7 +164,7 @@ public class TerminalTextFieldElement implements FieldElement { protected void paintChars(JComponent c, Graphics g, int x, int y, VtAttributes attrs, int start, int end) { - char[] ch = line.getCharBuffer(); + int[] cps = line.getCodePointBuffer(); int descent = metrics.getDescent(); int height = metrics.getHeight(); int left = x + start * em; @@ -205,6 +207,12 @@ public class TerminalTextFieldElement implements FieldElement { case NONE: } + /** + * Draw each character individually, so I can force grid alignment, even in cases where + * the glyph happens to not match width. (Could be a special glyph or a non-monospaced + * font.) + */ + char[] ch = new char[2]; // Max length of UTF-16 encoding for a single code point for (int i = start; i < end; i++) { /** * HACK: The default monospaced font selected by Java may not have glyphs for the @@ -213,16 +221,17 @@ public class TerminalTextFieldElement implements FieldElement { * monospaced. This is not acceptable. To deal with that, when we find a glyph whose * width does not match, we'll scale it horizontally so that it does. */ - int chW = metrics.charWidth(ch[i]); + int chW = metrics.charWidth(cps[i]); + int len = Character.toChars(cps[i], ch, 0); if (chW != em) { try (SaveTransform st = new SaveTransform(g)) { st.g.translate(x + em * i, 0); st.g.scale((double) em / chW, 1.0); - st.g.drawChars(ch, i, 1, 0, 0); + st.g.drawChars(ch, 0, len, 0, 0); } } else { - g.drawChars(ch, i, 1, x + em * i, 0); + g.drawChars(ch, 0, len, x + em * i, 0); } } if (attrs.strikeThrough()) { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtBuffer.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtBuffer.java index ded24e78b0..ed045c3c70 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtBuffer.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtBuffer.java @@ -22,12 +22,12 @@ import ghidra.app.plugin.core.terminal.vt.VtHandler.Erasure; /** * A buffer for a terminal display and scroll-back - * *

* This object implements all of the buffer, line, and character manipulations available in the * terminal. It's likely more will need to be added in the future. While the ANSI VT parser * determines what commands to execute, this buffer provides the actual implementation of those - * commands. + * commands. To best support unicode, including supplemental characters, this buffer and its + * {@link VtLine}s operate on arrays of code points. */ public class VtBuffer { public static final int DEFAULT_ROWS = 25; @@ -91,7 +91,6 @@ public class VtBuffer { /** * Get the number of rows in the display - * *

* This is not just the number of rows currently being used. This is the "rows" dimension of the * display, i.e., the maximum number of rows it can display before scrolling. @@ -104,7 +103,6 @@ public class VtBuffer { /** * Get the number of columns in the display - * *

* This is not just the number of columns currently being used. This is the "columns" dimension * of the display, i.e., the maximum number of characters in a rows before wrapping. @@ -117,7 +115,6 @@ public class VtBuffer { /** * Put the given character at the cursor, and move the cursor forward - * *

* The cursor's current attributes are applied to the character. * @@ -125,7 +122,7 @@ public class VtBuffer { * @see #setAttributes(VtAttributes) * @see #getAttributes() */ - public void putChar(char c) { + public void putCodePoint(int c) { if (c == 0) { return; } @@ -138,7 +135,7 @@ public class VtBuffer { curY = Math.max(0, curY - 1); } // At this point, we have no choice but to wrap - lines.get(curY).putChar(curX, c, curAttrs); + lines.get(curY).putCodePoint(curX, c, curAttrs); } /** @@ -162,7 +159,6 @@ public class VtBuffer { /** * Move the cursor back to the beginning of the line - * *

* This does not move the cursor down. */ @@ -179,11 +175,12 @@ public class VtBuffer { /** * Scroll the viewport down a line - * *

* The lines are shifted upward. The line at the top of the viewport is removed, and a blank * line is inserted at the bottom of the viewport. If the viewport includes the display's top * line and intoScrollBack is specified, the line is shifted into the scroll-back buffer. + * + * @param intoScrollBack true to shift the top lines into the scroll-back buffer */ public void scrollViewportDown(boolean intoScrollBack) { if (scrollStart == scrollEnd) { @@ -209,7 +206,6 @@ public class VtBuffer { /** * Scroll the viewport up a line - * *

* The lines are shifted downward. The line at the bottom of the viewport is removed, and a * blank line is inserted at the top of the viewport. @@ -234,10 +230,10 @@ public class VtBuffer { /** * Move the cursor up n rows - * *

* The cursor cannot move above the top of the display. The value of n must be positive, - * otherwise behavior is undefined. To move the cursor down, use {@link #moveCursorDown(int)}. + * otherwise behavior is undefined. To move the cursor down, use + * {@link #moveCursorDown(int, boolean)}. * * @param n the number of rows to move the cursor up */ @@ -247,12 +243,10 @@ public class VtBuffer { /** * Move the cursor down n rows - * *

* If the cursor would move below the bottom of the display, the viewport will be scrolled so * that the cursor remains in the display. The value of n must be positive, otherwise behavior * is undefined. To move the cursor up, use {@link #moveCursorUp(int)}. - * *

* ConPty has a habit of moving the cursor past the end of the current line before sending CRLF. * (Though, I imagine there are other applications that might do this.) The {@code dedupWrap} @@ -278,13 +272,13 @@ public class VtBuffer { /** * Move the cursor left (backward) n columns - * *

* The cursor is clamped into the display. If wrap is specified, the cursor would exceed the * left side of the display, and the previous line was wrapped onto the current line, then the * cursor will instead be moved to the end of the previous line. (It doesn't matter how far the * cursor would exceed the left; it moves up at most one line.) The value of n must be positive, - * otherwise behavior is undefined. To move the cursor right, use {@link #moveCursorRight(int)}. + * otherwise behavior is undefined. To move the cursor right, use + * {@link #moveCursorRight(int, boolean, boolean)}. * * @param n the number of columns * @param wrap whether to wrap the cursor to the previous line if would exceed the left of the @@ -303,17 +297,18 @@ public class VtBuffer { /** * Move the cursor right (forward) n columns - * *

* The cursor is clamped into the display. If wrap is specified and the cursor would exceed the * right side of the display, the cursor will instead be wrapped to the start of the next line, * possibly scrolling the viewport down. (It doesn't matter how far the cursor exceeds the * right; the cursor moves down exactly one line.) The value of n must be positive, otherwise - * behavior is undefined. To move the cursor left, use {@link #moveCursorLeft(int)}. + * behavior is undefined. To move the cursor left, use {@link #moveCursorLeft(int, boolean)}. * * @param n the number of columns * @param wrap whether to wrap the cursor to the next line if it would exceed the right of the * display + * @param isCursorShowing indicates whether the cursor is currently showing, which may affect + * wrapping behavior */ public void moveCursorRight(int n, boolean wrap, boolean isCursorShowing) { if (wrap && curX + n >= cols) { @@ -349,7 +344,6 @@ public class VtBuffer { /** * Save the current cursor position - * *

* There is only one slot for the saved cursor. It is not a stack or anything fancy. To restore * the cursor, use {@link #restoreCursorPos()}. The advantage to using this vice @@ -363,7 +357,6 @@ public class VtBuffer { /** * Restore a saved cursor position - * *

* If there was no previous call to {@link #saveCursorPos()}, the cursor is placed at the * top-left of the display. @@ -376,7 +369,6 @@ public class VtBuffer { /** * Move the cursor to the given row and column - * *

* The position is clamped to the dimensions of the display. No scrolling will take place if * {@code col} exceeds the number of rows. @@ -392,10 +384,9 @@ public class VtBuffer { /** * Get the cursor's current attributes - * *

- * Characters put into the buffer via {@link #putChar(char)} are assigned the cursor's current - * attributes at the time they are inserted. + * Characters put into the buffer via {@link #putCodePoint(int)} are assigned the cursor's + * current attributes at the time they are inserted. * * @see #setAttributes(VtAttributes) * @return the current attributes @@ -406,11 +397,11 @@ public class VtBuffer { /** * Set the cursor's current attributes - * *

* These are usually the attributes given by the ANSI SGR control sequences. They may not affect * the display of the cursor itself, but rather of the characters placed at the cursor via - * {@link #putChar(char)}. NOTE: Not all attributes are necessarily supported by the renderer. + * {@link #putCodePoint(int)}. NOTE: Not all attributes are necessarily supported by the + * renderer. * * @param attributes the desired attributes */ @@ -420,7 +411,6 @@ public class VtBuffer { /** * Erase (clear) some portion of the display buffer - * *

* If the current line is erased from start to the cursor, the cursor's attributes are applied * to the cleared columns. @@ -488,7 +478,6 @@ public class VtBuffer { /** * Insert n blank lines at the cursor - * *

* Lines at the bottom of the viewport are removed and all the lines between the cursor and the * bottom of the viewport are shifted down, to make room for n blank lines. None of the lines @@ -506,7 +495,6 @@ public class VtBuffer { /** * Delete n lines at the cursor - * *

* Lines at (and immediately below) the cursor are removed and all lines between the cursor and * the bottom of the viewport are shifted up to make room for n blank lines inserted at (and @@ -524,7 +512,6 @@ public class VtBuffer { /** * Insert n blank characters at the cursor - * *

* Any characters right the cursor on the same line are shifted right to make room and n blanks * are inserted at (and to the right) of the cursor. No wrapping occurs. Characters that would @@ -542,7 +529,6 @@ public class VtBuffer { /** * Delete n characters at the cursor - * *

* Characters at (and {@code n-1} to the right) of the cursor are deleted. The remaining * characters to the right are shifted left {@code n} columns. @@ -558,7 +544,6 @@ public class VtBuffer { /** * Erase n characters at the cursor - * *

* Characters at (and {@code n-1} to the right) of the cursor are erased, i.e., replaced with * blanks. No shifting takes place. @@ -574,7 +559,6 @@ public class VtBuffer { /** * Specify the scrolling viewport of the buffer - * *

* By default, the viewport is the entire display, and scrolling the viewport downward may cause * lines to enter the scroll-back buffer. The buffer manages these boundaries so that they can @@ -609,7 +593,6 @@ public class VtBuffer { /** * Resize the buffer to the given number of rows and columns - * *

* The viewport is reset to include the full display. Each line, including those in the * scroll-back buffer are resized to match the requested number of columns. If the row count is @@ -669,7 +652,6 @@ public class VtBuffer { /** * Adjust the maximum number of lines in the scroll-back buffer - * *

* If the scroll-back buffer exceeds the given maximum, it is immediately culled. * @@ -722,7 +704,6 @@ public class VtBuffer { /** * Get the total number of lines, including scroll-back lines, in the buffer - * *

* This is equal to {@link #getScrollBackSize()}{@code +}{@link #getRows()}. * @@ -792,15 +773,13 @@ public class VtBuffer { /** * Get the text between two locations in the buffer - * *

* The buffer attempts to avoid extraneous space at the end of each line. This isn't always * perfect and depends on how lines are cleared. If they are cleared using * {@link #erase(Erasure)}, then the buffer will cull the trailing spaces resulting from the - * clear. If they are cleared using {@link #putChar(char)} passing a space {@code ' '}, then the - * inserted spaces will be included. In practice, this depends on the application controlling - * the terminal. - * + * clear. If they are cleared using {@link #putCodePoint(int)} passing a space {@code ' '}, then + * the inserted spaces will be included. In practice, this depends on the application + * controlling the terminal. *

* Like the other methods, locations are specified 0 up, top to bottom, and left to right. * Unlike the other methods, the ending character is excluded from the result. diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtCharset.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtCharset.java index 5ec2145ed6..bed5ecac2f 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtCharset.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtCharset.java @@ -4,9 +4,9 @@ * Licensed 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. @@ -17,15 +17,13 @@ package ghidra.app.plugin.core.terminal.vt; /** * A legacy style charset - * *

* Finding the particulars for these online has not been fun, so these are implemented on an * as-needed basis. There's probably a simple translation to some unicode code pages, since those * seem to be ordered by some of these legacy character sets. The default implementation for each - * charset will just be equivalent to US-ASCII. There's a lot of plumbing missing around these, two. + * charset will just be equivalent to US-ASCII. There's a lot of plumbing missing around these, too. * For example, I'm assuming that switching to "the alternate charset" means using G1 instead of G0. * I've not read carefully enough to know how G2 or G3 are used. - * *

* It'd be nice to just use UTF-8, but the application would have to agree. */ @@ -49,7 +47,7 @@ public enum VtCharset { DEC_SPECIAL_LINES { @Override - public char mapChar(char c) { + public int mapCodePoint(int c) { switch (c) { case 'j': return '\u2518'; // 1pt lower-right corner @@ -64,7 +62,7 @@ public enum VtCharset { case 'x': return '\u2502'; // 1pt vertical line } - return super.mapChar(c); + return super.mapCodePoint(c); } }, DEC_SUPPLEMENTAL, @@ -101,11 +99,15 @@ public enum VtCharset { /** * Map a character, as decoded using US-ASCII, into the actual character for the character set. + *

+ * This is generally only applicable when the application is using 8-bit non-unicode encoding. + * The "code point" passed in will be that decoded as if using US-ASCII, i.e., it will be + * between 0 and 255. This should return the unicode code point for the desired character. * * @param c the character from US-ASCII. - * @return the mapped character + * @return the mapped character's unicode code point */ - public char mapChar(char c) { + public int mapCodePoint(int c) { return c; } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtHandler.java index 71742e0af0..4b8ad875d3 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtHandler.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtHandler.java @@ -115,6 +115,7 @@ public interface VtHandler { // Various parameters for the 'h' and 'l' final CSI bytes public static final byte[] _4 = ascii("4"); + public static final byte[] G0 = ascii(">0"); public static final byte[] Q1 = ascii("?1"); public static final byte[] Q7 = ascii("?7"); public static final byte[] Q12 = ascii("?12"); @@ -127,6 +128,7 @@ public interface VtHandler { public static final byte[] Q1048 = ascii("?1048"); public static final byte[] Q1049 = ascii("?1049"); public static final byte[] Q2004 = ascii("?2004"); + public static final byte[] Q2031 = ascii("?2031"); public static final byte[] Q9001 = ascii("?9001"); /** @@ -338,6 +340,10 @@ public interface VtHandler { * For 8-bit colors, one of the 216 colors from the RGB cube *

* The r, g, and b fields give the "step" number from 0 to 5, dimmest to brightest. + * + * @param r red + * @param g green + * @param b blue */ public record Ansi216Color(int r, int g, int b) implements AnsiColor {} @@ -346,6 +352,8 @@ public interface VtHandler { *

* The v field is a value from 0 to 23, 0 being the dimmest, but not true black, and 23 being * the brightest, but not true white. + * + * @param v value */ public record AnsiGrayscaleColor(int v) implements AnsiColor {} @@ -353,6 +361,10 @@ public interface VtHandler { * A 24-bit color *

* The r, g, and b fields are values from 0 to 255 dimmest to brightest. + * + * @param r red + * @param g green + * @param b blue */ public record Ansi24BitColor(int r, int g, int b) implements AnsiColor {} @@ -630,29 +642,15 @@ public interface VtHandler { default void handleCharExc(byte b) { try { switch (b) { - case 7: - handleBell(); - return; - case 8: - handleBackSpace(); - return; - case 9: - handleTab(); - return; - case 10: - handleLineFeed(); - return; - case 13: - handleCarriageReturn(); - return; - case 14: - handleAltCharset(true); - return; - case 15: - handleAltCharset(false); - return; + case 7 -> handleBell(); + case 8 -> handleBackSpace(); + case 9 -> handleTab(); + case 10 -> handleLineFeed(); + case 13 -> handleCarriageReturn(); + case 14 -> handleAltCharset(true); + case 15 -> handleAltCharset(false); + default -> handleChar(b); } - handleChar(b); } catch (Exception e) { Msg.error(this, "Exception handling terminal character output " + charInfo(b) + ":" + e, @@ -777,6 +775,9 @@ public interface VtHandler { else if (bufEq(csiParam, Q2004)) { handleBracketedPasteMode(en); } + else if (bufEq(csiParam, Q2031)) { + handleThemeChangeNotification(en); + } else if (bufEq(csiParam, Q9001)) { handleWin32InputMode(en); } @@ -785,6 +786,15 @@ public interface VtHandler { } } + default void handleQStuff(ByteBuffer csiParam) { + if (bufEq(csiParam, G0)) { + handleXTVersion(); + } + else { + throw new UnknownCsiException(); + } + } + /** * Handle XTerm CSI commands that manipulate the window titles * @@ -796,49 +806,29 @@ public interface VtHandler { throw new UnknownCsiException(); } switch (bits.nextInt()) { - case 22: { + case 22 -> { switch (bits.nextInt()) { - case 0: { + case 0 -> { handleSaveIconTitle(); handleSaveWindowTitle(); - return; - } - case 1: { - handleSaveIconTitle(); - return; - } - case 2: { - handleSaveWindowTitle(); - return; - } - default: { - throw new UnknownCsiException(); } + case 1 -> handleSaveIconTitle(); + case 2 -> handleSaveWindowTitle(); + default -> throw new UnknownCsiException(); } } - case 23: { + case 23 -> { switch (bits.nextInt()) { - case 0: { + case 0 -> { handleRestoreIconTitle(); handleRestoreWindowTitle(); - return; - } - case 1: { - handleRestoreIconTitle(); - return; - } - case 2: { - handleRestoreWindowTitle(); - return; - } - default: { - throw new UnknownCsiException(); } + case 1 -> handleRestoreIconTitle(); + case 2 -> handleRestoreWindowTitle(); + default -> throw new UnknownCsiException(); } } - default: { - throw new UnknownCsiException(); - } + default -> throw new UnknownCsiException(); } } @@ -854,123 +844,98 @@ public interface VtHandler { throws Exception { try { switch (csiFinal) { - case '@': { // Insert characters + case '@' -> { // Insert characters OfInt bits = parseCsiInts(csiParam); int n = bits.hasNext() ? bits.nextInt() : 1; handleInsertCharacters(n); - return; } - case 'A': // Cursor up - case 'B': // Cursor down - case 'C': // Cursor forward - case 'D': /* Cursor back */ { + case 'A', 'B', 'C', 'D' -> { // Cursor up, down, forward, back Direction dir = Direction.forCsiFinal(csiFinal); OfInt bits = parseCsiInts(csiParam); int n = bits.hasNext() ? bits.nextInt() : 1; handleMoveCursor(dir, n); - return; } - case 'G': { // Cursor character absolute + case 'G' -> { // Cursor character absolute OfInt bits = parseCsiInts(csiParam); int n = bits.hasNext() ? bits.nextInt() : 1; handleMoveCursorCol(n - 1); - return; } - case 'f': // Horizontal and Vertical Position (same as CUP) - case 'H': { // Cursor position + // Horizontal and Vertical Position (same as CUP), Cursor Position + case 'f', 'H' -> { OfInt bits = parseCsiInts(csiParam); int n = bits.hasNext() ? bits.nextInt() : 1; int m = bits.hasNext() ? bits.nextInt() : 1; handleMoveCursor(n - 1, m - 1); - return; } - case 'J': { // Erase in display + case 'J' -> { // Erase in display OfInt bits = parseCsiInts(csiParam); int n = bits.hasNext() ? bits.nextInt() : 0; handleErase(Erasure.fromED(n)); - return; } - case 'K': { // Erase in line + case 'K' -> { // Erase in line OfInt bits = parseCsiInts(csiParam); int n = bits.hasNext() ? bits.nextInt() : 0; handleErase(Erasure.fromEL(n)); - return; } - case 'L': { // Insert lines + case 'L' -> { // Insert lines OfInt bits = parseCsiInts(csiParam); int n = bits.hasNext() ? bits.nextInt() : 1; handleInsertLines(n); - return; } - case 'M': { // Delete lines + case 'M' -> { // Delete lines OfInt bits = parseCsiInts(csiParam); int n = bits.hasNext() ? bits.nextInt() : 1; handleDeleteLines(n); - return; } - case 'P': { // Delete characters + case 'P' -> { // Delete characters OfInt bits = parseCsiInts(csiParam); int n = bits.hasNext() ? bits.nextInt() : 1; handleDeleteCharacters(n); - return; } - case 'S': { // Scroll up lines + case 'S' -> { // Scroll up lines OfInt bits = parseCsiInts(csiParam); int n = bits.hasNext() ? bits.nextInt() : 1; handleScrollLinesUp(n, false); - return; } - case 'T': { // Scroll down lines + case 'T' -> { // Scroll down lines OfInt bits = parseCsiInts(csiParam); int n = bits.hasNext() ? bits.nextInt() : 1; handleScrollLinesDown(n); - return; } - case 'X': { // Erase characters + case 'X' -> { // Erase characters OfInt bits = parseCsiInts(csiParam); int n = bits.hasNext() ? bits.nextInt() : 1; handleEraseCharacters(n); - return; } - case 'Z': { // Cursor backward tabulation + case 'Z' -> { // Cursor backward tabulation OfInt bits = parseCsiInts(csiParam); int n = bits.hasNext() ? bits.nextInt() : 1; handleBackwardTab(n); - return; } - case 'b': { // Repeat last character + case 'b' -> { // Repeat last character OfInt bits = parseCsiInts(csiParam); int n = bits.hasNext() ? bits.nextInt() : 1; handleRepeatChar(n); - return; } - case 'c': { // Send Device Attributes - Msg.trace(this, "TODO: Send Device Attributes"); - return; - } - case 'd': { // Line position absolute + case 'c' -> Msg.trace(this, "TODO: Send Device Attributes"); + case 'd' -> { // Line position absolute OfInt bits = parseCsiInts(csiParam); int n = bits.hasNext() ? bits.nextInt() : 1; handleMoveCursorRow(n - 1); - return; } - case 'h': { - handleHOrLStuff(csiParam, true); - return; - } - case 'l': { - handleHOrLStuff(csiParam, false); - return; - } - case 'm': { // Select Graphic Rendition (SGR) + case 'h' -> handleHOrLStuff(csiParam, true); + case 'l' -> handleHOrLStuff(csiParam, false); + case 'm' -> { // Select Graphic Rendition (SGR) if (csiParam.hasRemaining()) { switch (csiParam.get(csiParam.position())) { - case '>': // Set key modifier options + case '>' -> { // Set key modifier options Msg.trace(this, "TODO: Set key modifier options"); return; - case '?': // Query key modifier options + } + case '?' -> { // Query key modifier options Msg.trace(this, "TODO: Query key modifier options"); return; + } } } OfInt bits = parseCsiInts(csiParam); @@ -980,49 +945,31 @@ public interface VtHandler { while (bits.hasNext()) { handleSgrAttribute(bits); } - return; } - case 'n': { // Device Status Report + case 'n' -> { // Device Status Report OfInt bits = parseCsiInts(csiParam); if (!bits.hasNext()) { throw new UnknownCsiException(); } switch (bits.nextInt()) { - case 6: // Report Cursor Position - handleReportCursorPos(); - return; - case 5: // Status Report (Not implemented) - default: - throw new UnknownCsiException(); + case 5 -> throw new UnknownCsiException(); // Status Report (Not implemented) + case 6 -> handleReportCursorPos(); + case 996 -> handleQueryTheme(); + default -> throw new UnknownCsiException(); } } - case 'p': { // Soft terminal reset - // TODO: Not sure how/if this should differ from "full" reset - handleFullReset(); - return; - } - case 'r': { // Scroll screen + case 'p' -> handleFullReset(); // Really Soft, but how's it different? + case 'q' -> handleQStuff(csiParam); + case 'r' -> { // Scroll screen OfInt bits = parseCsiInts(csiParam); Integer start = bits.hasNext() ? bits.nextInt() - 1 : null; Integer end = bits.hasNext() ? bits.nextInt() - 1 : null; handleSetScrollRange(start, end); - return; - } - case 's': { - handleSaveCursorPos(); - return; - } - case 't': { // Window manipulation - handleWindowManipulation(csiParam); - return; - } - case 'u': { - handleRestoreCursorPos(); - return; - } - default: { - throw new UnknownCsiException(); } + case 's' -> handleSaveCursorPos(); + case 't' -> handleWindowManipulation(csiParam); + case 'u' -> handleRestoreCursorPos(); + default -> throw new UnknownCsiException(); } } catch (UnknownCsiException e) { @@ -1229,69 +1176,27 @@ public interface VtHandler { return; } switch (code) { - case 0: - handleResetAttributes(); - return; - case 1: - handleIntensity(Intensity.BOLD); - return; - case 2: - handleIntensity(Intensity.DIM); - return; - case 3: - handleFont(AnsiFont.ITALIC); - return; - case 4: - handleUnderline(Underline.SINGLE); - return; - case 5: - handleBlink(Blink.SLOW); - return; - case 6: - handleBlink(Blink.FAST); - return; - case 7: - handleReverseVideo(ReverseVideo.REVERSED); - return; - case 8: - handleHidden(true); - return; - case 9: - handleStrikeThrough(true); - return; - case 20: - handleFont(AnsiFont.BLACK_LETTER); - return; - case 21: - handleUnderline(Underline.DOUBLE); - return; - case 22: - handleIntensity(Intensity.NORMAL); - return; - case 23: - handleFont(AnsiFont.NORMAL); - return; - case 24: - handleUnderline(Underline.NONE); - return; - case 25: - handleBlink(Blink.NONE); - return; - case 26: - handleProportionalSpacing(true); - return; - case 27: - handleReverseVideo(ReverseVideo.NORMAL); - return; - case 28: - handleHidden(false); - return; - case 29: - handleStrikeThrough(false); - return; - default: - Msg.warn(this, "Unrecognized SGR attribute: " + code); - return; + case 0 -> handleResetAttributes(); + case 1 -> handleIntensity(Intensity.BOLD); + case 2 -> handleIntensity(Intensity.DIM); + case 3 -> handleFont(AnsiFont.ITALIC); + case 4 -> handleUnderline(Underline.SINGLE); + case 5 -> handleBlink(Blink.SLOW); + case 6 -> handleBlink(Blink.FAST); + case 7 -> handleReverseVideo(ReverseVideo.REVERSED); + case 8 -> handleHidden(true); + case 9 -> handleStrikeThrough(true); + case 20 -> handleFont(AnsiFont.BLACK_LETTER); + case 21 -> handleUnderline(Underline.DOUBLE); + case 22 -> handleIntensity(Intensity.NORMAL); + case 23 -> handleFont(AnsiFont.NORMAL); + case 24 -> handleUnderline(Underline.NONE); + case 25 -> handleBlink(Blink.NONE); + case 26 -> handleProportionalSpacing(true); + case 27 -> handleReverseVideo(ReverseVideo.NORMAL); + case 28 -> handleHidden(false); + case 29 -> handleStrikeThrough(false); + default -> Msg.warn(this, "Unrecognized SGR attribute: " + code); } } @@ -1522,6 +1427,26 @@ public interface VtHandler { */ void handleBracketedPasteMode(boolean en); + /** + * Toggle unsolicited theme-change notifications + *

+ * This is the Dark and Light Mode detection as documented by the Contour + * Terminal Emulator. + * + * @param en true to send notifications of theme changes + */ + void handleThemeChangeNotification(boolean en); + + /** + * Solicit the current theme + *

+ * This is the Dark and Light Mode detection as documented by the Contour + * Terminal Emulator. + */ + void handleQueryTheme(); + /** * Toggle Win32 input mode * @@ -1687,13 +1612,13 @@ public interface VtHandler { /** * Set the range of rows (viewport) involved in scrolling. *

- * This applies not only to {@link #handleScrollUp()} and {@link #handleScrollDown()}, but also - * to when the cursor moves far enough down that the display must scroll. Normally, start is 0 - * and end is rows-1 (The parser will adjust the 1-up indices to 0-up) so that the entire - * display is scrolled. If the cursor moves past end (not just the end of the device, but the - * end given here) then the scrolling region must be scrolled. The top line is removed, the - * interior lines are moved up, and the bottom line is cleared. If the terminal is resized, the - * scroll range is reset to the whole display. + * This applies not only to {@link #handleScrollLinesUp(int, boolean)} and + * {@link #handleScrollLinesDown(int)}, but also to when the cursor moves far enough down that + * the display must scroll. Normally, start is 0 and end is rows-1 (The parser will adjust the + * 1-up indices to 0-up) so that the entire display is scrolled. If the cursor moves past end + * (not just the end of the device, but the end given here) then the scrolling region must be + * scrolled. The top line is removed, the interior lines are moved up, and the bottom line is + * cleared. If the terminal is resized, the scroll range is reset to the whole display. * * @param start the first row (0-up) in the scrolling region. If omitted, the first row of the * display. @@ -1718,7 +1643,7 @@ public interface VtHandler { * Scroll the display n lines up, considering only those lines in the scrolling range. * * @param n the number of lines to scroll - * @see #handleScrollDown() + * @see #handleScrollLinesDown(int) * @see #handleSetScrollRange(Integer, Integer) */ void handleScrollViewportUp(int n); @@ -1769,4 +1694,9 @@ public interface VtHandler { * reset to their defaults. */ void handleFullReset(); + + /** + * Handle an XTVERSION request + */ + void handleXTVersion(); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtLine.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtLine.java index 4bb1100d4b..fe0415db57 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtLine.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtLine.java @@ -4,9 +4,9 @@ * Licensed 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. @@ -21,7 +21,7 @@ package ghidra.app.plugin.core.terminal.vt; public class VtLine { protected int cols; protected int len; - protected char[] chars; + protected int[] cps; protected boolean wrappedToNext; private VtAttributes[] cellAttrs; @@ -40,12 +40,12 @@ public class VtLine { * @param x the column, 0 up * @return the character */ - public char getChar(int x) { - return chars[x]; + public int getCodePoint(int x) { + return cps[x]; } /** - * Get the full character buffer + * Get the full code point buffer * *

* This is a reference to the buffer, which is very useful when rendering. Modifying this buffer @@ -53,8 +53,8 @@ public class VtLine { * * @return the buffer */ - public char[] getCharBuffer() { - return chars; + public int[] getCodePointBuffer() { + return cps; } /** @@ -75,18 +75,18 @@ public class VtLine { * Place the given character with attributes into the given column * * @param x the column, 0 up - * @param c the character + * @param c the unicode code point * @param attrs the attributes */ - public void putChar(int x, char c, VtAttributes attrs) { + public void putCodePoint(int x, int c, VtAttributes attrs) { int oldLen = len; len = Math.max(len, x + 1); wrappedToNext = false; // Maybe remove for (int i = oldLen; i < x; i++) { - chars[i] = ' '; + cps[i] = ' '; cellAttrs[i] = VtAttributes.DEFAULTS; } - chars[x] = c; + cps[x] = c; if (attrs != null) { cellAttrs[x] = attrs; } @@ -101,28 +101,28 @@ public class VtLine { this.cols = cols; // NB. Don't forget the characters in the buffer. User may resize back again. // TODO: Could/should we re-wrap? Would need to record wraps vs returns, though. - if (cols <= chars.length) { + if (cols <= cps.length) { return; } - char[] newChars = new char[cols]; + int[] newCodePoints = new int[cols]; VtAttributes[] newCellAttrs = new VtAttributes[cols]; - System.arraycopy(chars, 0, newChars, 0, Math.min(cols, chars.length)); + System.arraycopy(cps, 0, newCodePoints, 0, Math.min(cols, cps.length)); System.arraycopy(cellAttrs, 0, newCellAttrs, 0, Math.min(cols, cellAttrs.length)); - this.chars = newChars; + this.cps = newCodePoints; this.cellAttrs = newCellAttrs; } /** * Reset the line * - * @param cols + * @param cols the new number of columns */ public void reset(int cols) { this.cols = cols; this.len = 0; this.wrappedToNext = false; - if (this.cols != cols || this.chars == null) { - this.chars = new char[cols]; + if (this.cols != cols || this.cps == null) { + this.cps = new int[cols]; this.cellAttrs = new VtAttributes[cols]; } } @@ -176,7 +176,7 @@ public class VtLine { return; } for (int i = 0; i <= x; i++) { - chars[i] = ' '; + cps[i] = ' '; cellAttrs[i] = attrs; } } @@ -196,7 +196,7 @@ public class VtLine { int shift = end - start; len -= shift; for (int x = start; x < end; x++) { - chars[x] = chars[x + shift]; + cps[x] = cps[x + shift]; cellAttrs[x] = cellAttrs[x + shift]; } } @@ -220,7 +220,7 @@ public class VtLine { return; } for (int x = start; x < end; x++) { - chars[x] = ' '; + cps[x] = ' '; cellAttrs[x] = attrs; } } @@ -239,11 +239,11 @@ public class VtLine { // TODO: What about colors/attributes? int end = Math.min(cols, start + n); for (int x = cols - 1; x >= end; x--) { - chars[x] = chars[x - n]; + cps[x] = cps[x - n]; cellAttrs[x] = cellAttrs[x - n]; } for (int x = start; x < end; x++) { - chars[x] = ' '; + cps[x] = ' '; } len = Math.min(cols, len + n); wrappedToNext = false; @@ -296,7 +296,9 @@ public class VtLine { public void gatherText(StringBuilder sb, int start, int end) { start = Math.max(0, Math.min(start, len)); end = Math.max(0, Math.min(end, len)); - sb.append(chars, start, end - start); + for (int i = start; i < end; i++) { + sb.appendCodePoint(cps[i]); + } } /** @@ -305,11 +307,11 @@ public class VtLine { *

* This is used both when selecting words, and when requiring search to find whole words. * - * @param ch the character + * @param cp the unicode code point * @return true if the character is part of a word */ - public static boolean isWordChar(char ch) { - return Character.isLetterOrDigit(ch) || ch == '_' || ch == '-' || ch == '@'; + public static boolean isWordChar(int cp) { + return Character.isLetterOrDigit(cp) || cp == '_' || cp == '-' || cp == '@'; } /** @@ -322,8 +324,8 @@ public class VtLine { public int findWord(int x, boolean forward) { int step = forward ? 1 : -1; for (int i = x; i < len && i >= 0; i += step) { - char ch = chars[i]; - if (isWordChar(ch)) { + int cp = cps[i]; + if (isWordChar(cp)) { continue; } if (forward) { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtParser.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtParser.java index ae246e02fa..45784575f2 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtParser.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtParser.java @@ -16,6 +16,7 @@ package ghidra.app.plugin.core.terminal.vt; import java.nio.ByteBuffer; +import java.util.Objects; /** * The parser for a terminal emulator @@ -141,10 +142,10 @@ public class VtParser { protected void debugChar(char c) { if (!Character.isISOControl(c)) { - System.err.print("%c".formatted(c)); + System.err.println("\\x%02x (%c)".formatted(c & 0xff, c)); } else { - System.err.print("\\x%02x".formatted(c & 0xff)); + System.err.println("\\x%02x".formatted(c & 0xff)); } } @@ -156,7 +157,7 @@ public class VtParser { * @return the new state node */ protected VtState doProcessByte(VtState state, byte b) { - return state.handleNext(b, this, handler); + return Objects.requireNonNull(state.handleNext(b, this, handler)); } /** diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtResponseEncoder.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtResponseEncoder.java index c6c655feef..c3c4fb74a7 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtResponseEncoder.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtResponseEncoder.java @@ -4,9 +4,9 @@ * Licensed 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. @@ -23,8 +23,10 @@ import ghidra.util.Msg; public abstract class VtResponseEncoder { protected static final byte[] PASTE_START = VtHandler.ascii("\033[200~"); protected static final byte[] PASTE_END = VtHandler.ascii("\033[201~"); + protected static final byte[] THEME_DARK_MODE = VtHandler.ascii("\033[?997;1n"); + protected static final byte[] THEME_LIGHT_MODE = VtHandler.ascii("\033[?997;2n"); - protected final ByteBuffer bb = ByteBuffer.allocate(16); + protected ByteBuffer bb = ByteBuffer.allocate(16); protected final Charset charset; @@ -61,4 +63,22 @@ public abstract class VtResponseEncoder { bb.put(PASTE_END); generateBytesExc(); } + + public void reportDarkMode(boolean isDark) { + bb.put(isDark ? THEME_DARK_MODE : THEME_LIGHT_MODE); + generateBytesExc(); + } + + public void reportXTVersion(String name, String version) { + byte[] bytes = "\033P>|%s(%s)\033\\".formatted(name, version).getBytes(charset); + if (bytes.length > bb.capacity()) { + int newCap = bb.capacity(); + while (bytes.length > newCap) { + newCap *= 2; + } + bb = ByteBuffer.allocate(newCap); + } + bb.put(bytes); + generateBytesExc(); + } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtState.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtState.java index b0eba58043..d60ffed52a 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtState.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/terminal/vt/VtState.java @@ -4,9 +4,9 @@ * Licensed 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. @@ -38,48 +38,60 @@ public enum VtState { ESC { @Override protected VtState handleNext(byte b, VtParser parser, VtHandler handler) { - switch (b) { - case '7': + return switch (b) { + case '7' -> { handler.handleSaveCursorPos(); - return CHAR; - case '8': + yield CHAR; + } + case '8' -> { handler.handleRestoreCursorPos(); - return CHAR; - case '(': + yield CHAR; + } + case '(' -> { parser.csG = G.G0; - return CHARSET; - case ')': + yield CHARSET; + } + case ')' -> { parser.csG = G.G1; - return CHARSET; - case '*': + yield CHARSET; + } + case '*' -> { parser.csG = G.G2; - return CHARSET; - case '+': + yield CHARSET; + } + case '+' -> { parser.csG = G.G3; - return CHARSET; - case '[': - return CSI_PARAM; - case ']': - return OSC_PARAM; - case '=': + yield CHARSET; + } + case 'P' -> DCS_PARAM; + case '[' -> CSI_PARAM; + case ']' -> OSC_PARAM; + case '\\' -> CHAR; // ST, just go back to CHAR + case '=' -> { handler.handleKeypadMode(KeyMode.APPLICATION); - return CHAR; - case '>': + yield CHAR; + } + case '>' -> { handler.handleKeypadMode(KeyMode.NORMAL); - return CHAR; - case 'D': + yield CHAR; + } + case 'D' -> { handler.handleScrollViewportDown(1, true); - return CHAR; - case 'M': + yield CHAR; + } + case 'M' -> { handler.handleScrollViewportUp(1); - return CHAR; - case 'c': + yield CHAR; + } + case 'c' -> { handler.handleFullReset(); - return CHAR; - } - handler.handleCharExc((byte) 0x1b); - handler.handleCharExc(b); - return CHAR; + yield CHAR; + } + default -> { + handler.handleCharExc((byte) 0x1b); // Is this correct? + yield parser.doProcessByte(CHAR, b); + } + }; } }, /** @@ -89,67 +101,78 @@ public enum VtState { CHARSET { @Override protected VtState handleNext(byte b, VtParser parser, VtHandler handler) { - switch (b) { - case '"': - return CHARSET_QUOTE; - case '%': - return CHARSET_PERCENT; - case '&': - return CHARSET_AMPERSAND; - case 'A': + return switch (b) { + case '"' -> CHARSET_QUOTE; + case '%' -> CHARSET_PERCENT; + case '&' -> CHARSET_AMPERSAND; + case 'A' -> { handler.handleSetCharset(parser.csG, VtCharset.UK); - return CHAR; - case 'B': + yield CHAR; + } + case 'B' -> { handler.handleSetCharset(parser.csG, VtCharset.USASCII); - return CHAR; - case 'C': - case '5': + yield CHAR; + } + case 'C', '5' -> { handler.handleSetCharset(parser.csG, VtCharset.FINNISH); - return CHAR; - case 'H': - case '7': + yield CHAR; + } + case 'H', '7' -> { handler.handleSetCharset(parser.csG, VtCharset.SWEDISH); - return CHAR; - case 'K': + yield CHAR; + } + case 'K' -> { handler.handleSetCharset(parser.csG, VtCharset.GERMAN); - return CHAR; - case 'Q': - case '9': + yield CHAR; + } + case 'Q', '9' -> { handler.handleSetCharset(parser.csG, VtCharset.FRENCH_CANADIAN); - return CHAR; - case 'R': - case 'f': + yield CHAR; + } + case 'R', 'f' -> { handler.handleSetCharset(parser.csG, VtCharset.FRENCH); - return CHAR; - case 'Y': + yield CHAR; + } + case 'Y' -> { handler.handleSetCharset(parser.csG, VtCharset.ITALIAN); - return CHAR; - case 'Z': + yield CHAR; + } + case 'Z' -> { handler.handleSetCharset(parser.csG, VtCharset.SPANISH); - return CHAR; - case '4': + yield CHAR; + } + case '4' -> { handler.handleSetCharset(parser.csG, VtCharset.DUTCH); - return CHAR; - case '=': + yield CHAR; + } + case '=' -> { handler.handleSetCharset(parser.csG, VtCharset.SWISS); - return CHAR; - case '`': - case 'E': - case '6': + yield CHAR; + } + case '`', 'E', '6' -> { handler.handleSetCharset(parser.csG, VtCharset.NORWEGIAN_DANISH); - return CHAR; - case '0': + yield CHAR; + } + case '0' -> { handler.handleSetCharset(parser.csG, VtCharset.DEC_SPECIAL_LINES); - return CHAR; - case '<': + yield CHAR; + } + case '<' -> { handler.handleSetCharset(parser.csG, VtCharset.DEC_SUPPLEMENTAL); - return CHAR; - case '>': + yield CHAR; + } + case '>' -> { handler.handleSetCharset(parser.csG, VtCharset.DEC_TECHNICAL); - return CHAR; - } - handler.handleCharExc((byte) 0x1b); - return parser.doProcessByte(parser.doProcessByte(CHAR, parser.csG.b), b); + yield CHAR; + } + default -> { + handler.handleCharExc((byte) 0x1b); + VtState st = CHAR; + st = parser.doProcessByte(st, parser.csG.b); + st = parser.doProcessByte(st, b); + yield st; + } + }; } }, /** @@ -158,20 +181,28 @@ public enum VtState { CHARSET_QUOTE { @Override protected VtState handleNext(byte b, VtParser parser, VtHandler handler) { - switch (b) { - case '>': + return switch (b) { + case '>' -> { handler.handleSetCharset(parser.csG, VtCharset.GREEK); - return CHAR; - case '4': + yield CHAR; + } + case '4' -> { handler.handleSetCharset(parser.csG, VtCharset.DEC_HEBREW); - return CHAR; - case '?': + yield CHAR; + } + case '?' -> { handler.handleSetCharset(parser.csG, VtCharset.DEC_GREEK); - return CHAR; - } - handler.handleCharExc((byte) 0x1b); - return parser.doProcessByte( - parser.doProcessByte(parser.doProcessByte(CHAR, parser.csG.b), (byte) '"'), b); + yield CHAR; + } + default -> { + handler.handleCharExc((byte) 0x1b); + VtState st = CHAR; + st = parser.doProcessByte(st, parser.csG.b); + st = parser.doProcessByte(st, (byte) '"'); + st = parser.doProcessByte(st, b); + yield st; + } + }; } }, /** @@ -180,26 +211,36 @@ public enum VtState { CHARSET_PERCENT { @Override protected VtState handleNext(byte b, VtParser parser, VtHandler handler) { - switch (b) { - case '2': + return switch (b) { + case '2' -> { handler.handleSetCharset(parser.csG, VtCharset.TURKISH); - return CHAR; - case '6': + yield CHAR; + } + case '6' -> { handler.handleSetCharset(parser.csG, VtCharset.PORTUGESE); - return CHAR; - case '=': + yield CHAR; + } + case '=' -> { handler.handleSetCharset(parser.csG, VtCharset.HEBREW); - return CHAR; - case '0': + yield CHAR; + } + case '0' -> { handler.handleSetCharset(parser.csG, VtCharset.DEC_TURKISH); - return CHAR; - case '5': + yield CHAR; + } + case '5' -> { handler.handleSetCharset(parser.csG, VtCharset.DEC_SUPPLEMENTAL_GRAPHICS); - return CHAR; - } - handler.handleCharExc((byte) 0x1b); - return parser.doProcessByte( - parser.doProcessByte(parser.doProcessByte(CHAR, parser.csG.b), (byte) '%'), b); + yield CHAR; + } + default -> { + handler.handleCharExc((byte) 0x1b); + VtState st = CHAR; + st = parser.doProcessByte(st, parser.csG.b); + st = parser.doProcessByte(st, (byte) '%'); + st = parser.doProcessByte(st, b); + yield st; + } + }; } }, /** @@ -208,14 +249,35 @@ public enum VtState { CHARSET_AMPERSAND { @Override protected VtState handleNext(byte b, VtParser parser, VtHandler handler) { - switch (b) { - case '4': + return switch (b) { + case '4' -> { handler.handleSetCharset(parser.csG, VtCharset.DEC_CYRILLIC); - return CHAR; - } - handler.handleCharExc((byte) 0x1b); - return parser.doProcessByte( - parser.doProcessByte(parser.doProcessByte(CHAR, parser.csG.b), (byte) '&'), b); + yield CHAR; + } + default -> { + handler.handleCharExc((byte) 0x1b); + VtState st = CHAR; + st = parser.doProcessByte(st, parser.csG.b); + st = parser.doProcessByte(st, (byte) '&'); + st = parser.doProcessByte(st, b); + yield st; + } + }; + } + }, + /** + * We've encountered {@code DCS} + *

+ * This implementation is entirely incorrect, but it's here to clean up all the VT-100 (or not) + * garbage that Claude Code emits. + */ + DCS_PARAM { + @Override + protected VtState handleNext(byte b, VtParser parser, VtHandler handler) { + return switch (b) { + case 0x1b -> CHAR; // This is really supposed to be terminated by ST (ESC \) + default -> DCS_PARAM; + }; } }, /** @@ -284,8 +346,8 @@ public enum VtState { } }, /** - * We've encountered {@code ESC} part of , so now we're parsing parameters until we encounter - * {@code BEL} or {@code ST}. + * We've encountered the {@code ESC} part of {@code OSC}, so now we're parsing parameters until + * we encounter {@code BEL} or {@code ST}. */ OSC_ESC { @Override diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/app/plugin/core/terminal/TerminalProviderTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/app/plugin/core/terminal/TerminalProviderTest.java index 1a02120ffd..90d30ef023 100644 --- a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/app/plugin/core/terminal/TerminalProviderTest.java +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/app/plugin/core/terminal/TerminalProviderTest.java @@ -56,6 +56,48 @@ public class TerminalProviderTest extends AbstractGhidraHeadedDebuggerTest { TerminalService terminalService; ClipboardService clipboardService; + @Test + public void testDecodeUnicode() throws Exception { + // Two non-supplemental characters + char[] c = new char[] { + 0x274c, + 0x277c + }; + byte[] b = new String(c).getBytes("utf8"); + + terminalService = addPlugin(tool, TerminalPlugin.class); + clipboardService = addPlugin(tool, ClipboardPlugin.class); + env.showFrontEndTool(); + + try (Terminal term = terminalService.createNullTerminal(Charset.forName("utf8"), out -> { + out.reset(); + })) { + term.injectDisplayOutput(b); + term.injectDisplayOutput("\u001b[1D".getBytes("utf8")); + + assertEquals(1, term.getCursorColumn()); + } + } + + @Test + public void testDecodeUnicode24Bit() throws Exception { + byte[] b = bytes(0xf0, 0x93, 0x90, 0xb3); // A supplemental character + + terminalService = addPlugin(tool, TerminalPlugin.class); + clipboardService = addPlugin(tool, ClipboardPlugin.class); + env.showFrontEndTool(); + + try (Terminal term = terminalService.createNullTerminal(Charset.forName("utf8"), out -> { + out.reset(); + })) { + term.injectDisplayOutput(b); + term.injectDisplayOutput(b); + term.injectDisplayOutput("\u001b[1D".getBytes("utf8")); + + assertEquals(1, term.getCursorColumn()); + } + } + @Test @SuppressWarnings("resource") public void testBash() throws Exception { @@ -80,7 +122,7 @@ public class TerminalProviderTest extends AbstractGhidraHeadedDebuggerTest { term.addTerminalListener(new TerminalListener() { @Override public void resized(short cols, short rows) { - System.err.println("resized: " + cols + "x" + rows); + // System.err.println("resized: " + cols + "x" + rows); child.setWindowSize(cols, rows); } }); @@ -115,7 +157,7 @@ public class TerminalProviderTest extends AbstractGhidraHeadedDebuggerTest { term.addTerminalListener(new TerminalListener() { @Override public void resized(short cols, short rows) { - System.err.println("resized: " + cols + "x" + rows); + // System.err.println("resized: " + cols + "x" + rows); child.setWindowSize(cols, rows); } }); @@ -147,7 +189,7 @@ public class TerminalProviderTest extends AbstractGhidraHeadedDebuggerTest { term.addTerminalListener(new TerminalListener() { @Override public void resized(short cols, short rows) { - System.err.println("resized: " + cols + "x" + rows); + // System.err.println("resized: " + cols + "x" + rows); child.setWindowSize(cols, rows); } });