GP-5970 - Scripting - A print/println methods to color console output

This commit is contained in:
dragonmacher 2025-09-10 12:21:42 -04:00
parent 6eab6693fc
commit 73bdee2546
13 changed files with 501 additions and 119 deletions

View file

@ -17,7 +17,7 @@ package ghidra.app.plugin.core.console;
import java.awt.*;
import java.awt.event.*;
import java.io.PrintWriter;
import java.io.*;
import javax.swing.*;
import javax.swing.text.*;
@ -29,6 +29,7 @@ import docking.widgets.FindDialog;
import docking.widgets.TextComponentSearcher;
import generic.theme.GIcon;
import generic.theme.Gui;
import ghidra.app.script.DecoratingPrintWriter;
import ghidra.app.services.*;
import ghidra.app.util.HelpTopics;
import ghidra.framework.main.ConsoleTextPane;
@ -95,8 +96,8 @@ public class ConsoleComponentProvider extends ComponentProviderAdapter implement
}
void init() {
stderr = new PrintWriter(new ConsoleWriter(this, true));
stdin = new PrintWriter(new ConsoleWriter(this, false));
stderr = new ConsolePrintWriter(true);
stdin = new ConsolePrintWriter(false);
/* call this before build() -- we get our Font here */
setVisible(true);
@ -230,6 +231,11 @@ public class ConsoleComponentProvider extends ComponentProviderAdapter implement
textPane.addPartialMessage(msg);
}
public void print(String msg, Color c) {
checkVisible();
textPane.addPartialMessage(msg, c);
}
@Override
public void printError(String errmsg) {
checkVisible();
@ -330,6 +336,84 @@ public class ConsoleComponentProvider extends ComponentProviderAdapter implement
// Inner Classes
//=================================================================================================
private class ConsolePrintWriter extends DecoratingPrintWriter {
private ColoringConsoleWriter writer;
ConsolePrintWriter(boolean error) {
this(new ColoringConsoleWriter(error));
}
private ConsolePrintWriter(ColoringConsoleWriter writer) {
super(writer);
this.writer = writer;
}
@Override
public void println(String s, Color c) {
try {
writer.setColor(c);
print(s);
println();
}
finally {
writer.setColor(null);
}
}
@Override
public void print(String s, Color c) {
try {
writer.setColor(c);
print(s);
}
finally {
writer.setColor(null);
}
}
}
private class ColoringConsoleWriter extends Writer {
private Color color;
private boolean error;
public ColoringConsoleWriter(boolean error) {
this.error = error;
}
void setColor(Color color) {
this.color = color;
}
@Override
public void write(char[] cbuf, int off, int len) throws IOException {
String s = new String(cbuf, off, len);
if (error) {
printError(s);
return;
}
if (color == null) {
print(s);
return;
}
print(s, color);
}
@Override
public void flush() throws IOException {
// stub
}
@Override
public void close() throws IOException {
clearMessages();
}
}
private class GoToMouseListener extends MouseAdapter {
@Override
public void mousePressed(MouseEvent e) {

View file

@ -1,62 +0,0 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* 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.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.console;
import ghidra.app.services.ConsoleService;
import java.io.IOException;
import java.io.Writer;
class ConsoleWriter extends Writer {
private ConsoleService console;
private boolean error;
ConsoleWriter(ConsoleService console, boolean error) {
super();
this.console = console;
this.error = error;
}
/**
* @see java.io.Writer#close()
*/
@Override
public void close() throws IOException {
console.clearMessages();
}
/**
* @see java.io.Writer#flush()
*/
@Override
public void flush() throws IOException {
}
/**
* @see java.io.Writer#write(char[], int, int)
*/
@Override
public void write(char[] cbuf, int off, int len) throws IOException {
String str = new String(cbuf, off, len);
if (error) {
console.printError(str);
}
else {
console.print(str);
}
}
}

View file

@ -35,7 +35,7 @@ import ghidra.app.plugin.core.console.CodeCompletion;
*/
public class CodeCompletionWindow extends JDialog {
private static final String FONT_ID = "font.plugin.terminal.completion.list";
static final String FONT_ID = "font.plugin.terminal.completion.list";
protected final InterpreterPanel console;
protected final JTextPane outputTextField;
@ -394,6 +394,10 @@ class CodeCompletionListSelectionModel extends DefaultListSelectionModel {
*/
class CodeCompletionListCellRenderer extends GListCellRenderer<CodeCompletion> {
CodeCompletionListCellRenderer() {
setBaseFontId(CodeCompletionWindow.FONT_ID);
}
@Override
protected String getItemText(CodeCompletion value) {
return value.getDescription();
@ -421,7 +425,6 @@ class CodeCompletionListCellRenderer extends GListCellRenderer<CodeCompletion> {
}
component.setEnabled(list.isEnabled());
component.setFont(list.getFont());
component.setComponentOrientation(list.getComponentOrientation());
Border border = null;
if (cellHasFocus) {

View file

@ -26,11 +26,14 @@ import java.util.concurrent.atomic.AtomicBoolean;
import javax.swing.*;
import javax.swing.text.*;
import org.apache.commons.io.output.WriterOutputStream;
import docking.DockingUtils;
import docking.actions.KeyBindingUtils;
import generic.theme.*;
import generic.util.WindowUtilities;
import ghidra.app.plugin.core.console.CodeCompletion;
import ghidra.app.script.DecoratingPrintWriter;
import ghidra.framework.options.OptionsChangeListener;
import ghidra.framework.options.ToolOptions;
import ghidra.framework.plugintool.PluginTool;
@ -69,8 +72,12 @@ public class InterpreterPanel extends JPanel implements OptionsChangeListener {
/* junit */ IPStdin stdin;
private OutputStream stdout;
private OutputStream stderr;
private PrintWriter outWriter;
private PrintWriter errWriter;
private InterpreterPrintWriter outWriter;
private InterpreterPrintWriter errWriter;
private AnsiRenderer stdErrRenderer = new AnsiRenderer();
private AnsiRenderer stdInRenderer = new AnsiRenderer();
private AnsiRenderer stdOutRenderer = new AnsiRenderer();
private SimpleAttributeSet STDOUT_SET;
private SimpleAttributeSet STDERR_SET;
@ -129,11 +136,12 @@ public class InterpreterPanel extends JPanel implements OptionsChangeListener {
outputScrollPane.setFocusable(false);
promptTextPane.setFocusable(false);
outWriter = new InterpreterPrintWriter(TextType.STDOUT);
errWriter = new InterpreterPrintWriter(TextType.STDERR);
stdin = new IPStdin();
stdout = new IPOut(TextType.STDOUT);
stderr = new IPOut(TextType.STDERR);
outWriter = new PrintWriter(stdout, true);
errWriter = new PrintWriter(stderr, true);
stdout = outWriter.asOutputStream();
stderr = errWriter.asOutputStream();
outputTextPane.setEditable(false);
promptTextPane.setEditable(false);
@ -270,7 +278,6 @@ public class InterpreterPanel extends JPanel implements OptionsChangeListener {
private void updateFontAttributes(Font font) {
Font boldFont = font.deriveFont(Font.BOLD);
STDOUT_SET = new GAttributes(font, NORMAL_COLOR);
STDOUT_SET = new GAttributes(font, NORMAL_COLOR);
STDERR_SET = new GAttributes(font, ERROR_COLOR);
STDIN_SET = new GAttributes(boldFont, NORMAL_COLOR);
@ -410,11 +417,7 @@ public class InterpreterPanel extends JPanel implements OptionsChangeListener {
outputTextPane.setCaretPosition(Math.max(0, outputTextPane.getDocument().getLength()));
}
AnsiRenderer stdErrRenderer = new AnsiRenderer();
AnsiRenderer stdInRenderer = new AnsiRenderer();
AnsiRenderer stdOutRenderer = new AnsiRenderer();
void addText(String text, TextType type) {
private void addText(String text, TextType type) {
SimpleAttributeSet attributes;
AnsiRenderer renderer;
switch (type) {
@ -438,29 +441,23 @@ public class InterpreterPanel extends JPanel implements OptionsChangeListener {
repositionScrollpane();
}
catch (BadLocationException e) {
Msg.error(this, "internal document positioning error", e);
// shouldn't happen
Msg.error(this, "Document positioning error", e);
}
}
private class IPOut extends OutputStream {
TextType type;
byte[] buffer = new byte[1];
private void addText(String text, Color c) {
IPOut(TextType type) {
this.type = type;
SimpleAttributeSet attributes = new GAttributes(getFont(), c);
try {
StyledDocument document = outputTextPane.getStyledDocument();
stdOutRenderer.renderString(document, text, attributes);
repositionScrollpane();
}
@Override
public void write(int b) throws IOException {
buffer[0] = (byte) b;
String text = new String(buffer);
addText(text, type);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
String text = new String(b, off, len);
addText(text, type);
catch (BadLocationException e) {
// shouldn't happen
Msg.error(this, "Document positioning error", e);
}
}
@ -565,6 +562,102 @@ public class InterpreterPanel extends JPanel implements OptionsChangeListener {
// Inner Classes
//==================================================================================================
private class InterpreterPrintWriter extends DecoratingPrintWriter {
private InterpreterConsoleWriter writer;
InterpreterPrintWriter(TextType type) {
this(new InterpreterConsoleWriter(type));
}
private InterpreterPrintWriter(InterpreterConsoleWriter writer) {
super(writer);
this.writer = writer;
}
OutputStream asOutputStream() {
try {
return WriterOutputStream.builder().setWriter(writer).getOutputStream();
}
catch (IOException e) {
Msg.error(this, "Unable to create output stream", e);
return null;
}
}
@Override
public void println(String s, Color c) {
try {
writer.setColor(c);
print(s);
println();
}
finally {
writer.setColor(null);
}
}
@Override
public void print(String s, Color c) {
try {
writer.setColor(c);
print(s);
}
finally {
writer.setColor(null);
}
}
}
private class InterpreterConsoleWriter extends Writer {
private Color color;
TextType type;
byte[] buffer = new byte[1];
public InterpreterConsoleWriter(TextType type) {
this.type = type;
}
void setColor(Color color) {
this.color = color;
}
@Override
public void write(int b) throws IOException {
buffer[0] = (byte) b;
String text = new String(buffer);
if (color != null) {
addText(text, color);
return;
}
addText(text, type);
}
@Override
public void write(char[] b, int off, int len) throws IOException {
String text = new String(b, off, len);
if (color != null) {
addText(text, color);
return;
}
addText(text, type);
}
@Override
public void flush() throws IOException {
// stub
}
@Override
public void close() throws IOException {
clear();
}
}
/**
* An {@link InputStream} that has as its source text strings being pushed into
* it by a thread, and being read by another thread.

View file

@ -487,7 +487,6 @@ public class GhidraScriptEditorComponentProvider extends ComponentProvider {
// this will overwrite any changes--be sure to resolve that before calling this method!
try {
loadScript(scriptSourceFile);
fileHash = MD5Utilities.getMD5Hash(scriptSourceFile.getFile(false));
clearChanges();
refreshAction();
}

View file

@ -0,0 +1,44 @@
/* ###
* IP: GHIDRA
*
* 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.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.script;
import java.awt.Color;
import java.io.PrintWriter;
import java.io.Writer;
/**
* A print writer that allows clients to specify the text color.
*/
public abstract class DecoratingPrintWriter extends PrintWriter {
public DecoratingPrintWriter(Writer out) {
super(out);
}
/**
* Print a line of text with the given color.
* @param s the text
* @param c the color
*/
public abstract void println(String s, Color c);
/**
* Print text with the given color.
* @param s the text
* @param c the color
*/
public abstract void print(String s, Color c);
}

View file

@ -1042,6 +1042,58 @@ public abstract class GhidraScript extends FlatProgramAPI {
}
}
/**
* Prints the {@link #decorateOutput optionally} {@link #decorate(String) decorated} message
* followed by a line feed to this script's {@code stdout} {@link PrintWriter}, which is set by
* {@link #set(GhidraState, ScriptControls)}.
* <p>
* Additionally, the always {@link #decorate(String) decorated} message is written to Ghidra's
* log.
*
* @param message the message to print
* @param color the color for the text
*/
public void println(String message, Color color) {
String decoratedMessage = decorate(message);
Msg.info(GhidraScript.class, new ScriptMessage(decoratedMessage));
if (writer instanceof DecoratingPrintWriter scriptWriter) {
scriptWriter.println(decorateOutput ? decoratedMessage : message, color);
return;
}
if (writer != null) {
writer.println(decorateOutput ? decoratedMessage : message);
}
}
/**
* Prints the undecorated message with no newline to this script's {@code stdout}
* {@link PrintWriter}, which is set by {@link #set(GhidraState, ScriptControls)}.
* <p>
* Additionally, the undecorated message is written to Ghidra's log.
*
* @param message the message to print
* @param color the color for the text
*/
public void print(String message, Color color) {
String decoratedMessage = decorate(message);
Msg.info(GhidraScript.class, new ScriptMessage(decoratedMessage));
if (writer instanceof DecoratingPrintWriter scriptWriter) {
scriptWriter.print(decorateOutput ? decoratedMessage : message, color);
return;
}
if (writer != null) {
writer.print(decorateOutput ? decoratedMessage : message);
}
}
/**
* Prints the undecorated {@link java.util.Formatter formatted message} to this script's
* {@code stdout} {@link PrintWriter}, which is set by

View file

@ -97,7 +97,7 @@ public class ScriptControls {
* @param monitor A cancellable monitor
*/
public ScriptControls(InterpreterConsole console, TaskMonitor monitor) {
this(console.getStdOut(), console.getStdErr(), monitor);
this(console.getOutWriter(), console.getErrWriter(), monitor);
}
/**

View file

@ -15,11 +15,11 @@
*/
package ghidra.app.services;
import java.io.PrintWriter;
import ghidra.app.plugin.core.console.ConsolePlugin;
import ghidra.framework.plugintool.ServiceInfo;
import java.io.PrintWriter;
/**
* Generic console interface allowing any plugin to print
* messages to console window.
@ -109,8 +109,6 @@ public interface ConsoleService {
* please throw {@link UnsupportedOperationException}.
*
* @return number of characters &gt;= 0
*
* @throws UnsupportedOperationException
*/
public int getTextLength();
@ -128,8 +126,6 @@ public interface ConsoleService {
* @param length the length of the desired string &gt;= 0
*
* @return the text, in a String of length &gt;= 0
*
* @throws UnsupportedOperationException
*/
public String getText(int offset, int length);
}

View file

@ -15,8 +15,10 @@
*/
package ghidra.framework.main;
import java.awt.Color;
import java.awt.Font;
import java.util.LinkedList;
import java.util.Objects;
import javax.swing.JTextPane;
import javax.swing.text.*;
@ -83,6 +85,10 @@ public class ConsoleTextPane extends JTextPane implements OptionsChangeListener
doAddMessage(new MessageWrapper(message));
}
public void addPartialMessage(String message, Color c) {
doAddMessage(new MessageWrapper(message, getFont(), c));
}
public void addErrorMessage(String message) {
doAddMessage(new ErrorMessage(message));
}
@ -280,15 +286,21 @@ public class ConsoleTextPane extends JTextPane implements OptionsChangeListener
//==================================================================================================
private static class MessageWrapper {
private final StringBuilder message;
protected final StringBuilder message;
private Color color;
private Font font;
private MessageWrapper(String message) {
if (message == null) {
throw new AssertException("Attempted to log a null message.");
}
Objects.requireNonNull(message, "Attempted to log a null message");
this.message = new StringBuilder(message);
}
public MessageWrapper(String message, Font font, Color color) {
this(message);
this.font = Objects.requireNonNull(font);
this.color = Objects.requireNonNull(color);
}
CharSequence getMessage() {
return message;
}
@ -297,13 +309,31 @@ public class ConsoleTextPane extends JTextPane implements OptionsChangeListener
if (getClass() != other.getClass()) {
return false;
}
if (!Objects.equals(color, other.color)) {
return false;
}
message.append(other.message);
return true;
}
AttributeSet getAttributes() {
if (color != null) {
GAttributes attrs = new GAttributes(font, color);
attrs.addAttribute(CUSTOM_ATTRIBUTE_KEY, OUTPUT_ATTRIBUTE_VALUE);
return attrs;
}
return outputAttributes;
}
@Override
public String toString() {
if (color == null) {
return message.toString();
}
return "[color=" + color + "] " + message.toString();
}
}
private static class ErrorMessage extends MessageWrapper {
@ -315,6 +345,10 @@ public class ConsoleTextPane extends JTextPane implements OptionsChangeListener
AttributeSet getAttributes() {
return errorAttributes;
}
}
@Override
public String toString() {
return "[error] " + message.toString();
}
}
}

View file

@ -1199,6 +1199,34 @@ public abstract class AbstractGhidraScriptMgrPluginTest
assertTrue("Timed-out waiting for cancelled script to complete", success);
}
protected void runScript(ResourceFile scriptFile) throws Exception {
GhidraScriptProvider scriptProvider = GhidraScriptUtil.getProvider(scriptFile);
GhidraScript script =
scriptProvider.getScriptInstance(scriptFile, new PrintWriter(System.err));
Task task = new RunScriptTask(script, plugin.getCurrentState(), console);
task.addTaskListener(provider.getTaskListener());
CountDownLatch latch = new CountDownLatch(1);
task.addTaskListener(new TaskListener() {
@Override
public void taskCompleted(Task t) {
latch.countDown();
}
@Override
public void taskCancelled(Task t) {
latch.countDown();
}
});
TaskLauncher.launch(task);
latch.await(TASK_RUN_SCRIPT_TIMEOUT_SECS, TimeUnit.SECONDS);
}
protected void startRunScriptTask(GhidraScript script) throws Exception {
Task task = new RunScriptTask(script, plugin.getCurrentState(), console);
task.addTaskListener(provider.getTaskListener());

View file

@ -17,9 +17,12 @@ package ghidra.app.plugin.core.script;
import static org.junit.Assert.*;
import java.awt.Color;
import java.io.*;
import java.nio.file.Path;
import javax.swing.text.*;
import org.apache.logging.log4j.Level;
import org.junit.Test;
@ -490,6 +493,113 @@ public class GhidraScriptMgrPlugin2Test extends AbstractGhidraScriptMgrPluginTes
"*2*", output);
}
@Test
public void testScriptPrintWithColor() throws Exception {
// create a script
ResourceFile newScriptFile = createTempScriptFile("LineColoringScript");
String filename = newScriptFile.getName();
String className = filename.replaceAll("\\.java", "");
String text1 = "This is black, ";
String text2 = "this is blue, and ";
String text3 = "this is red.\\n";
String line2 = "This is the default color.";
//@formatter:off
String newScript = """
import ghidra.app.script.GhidraScript;
import java.awt.Color;
public class %s extends GhidraScript {
@Override
public void run() throws Exception {
print("%s");
print("%s", Color.BLUE);
print("%s", Color.RED);
print("%s");
}
};
""".formatted(className, text1, text2, text3, line2);
//@formatter:on
writeStringToFile(newScriptFile, newScript);
runScript(newScriptFile);
waitForSwing();
assertConsoleTextColor(text1, Color.BLACK);
assertConsoleTextColor(text2, Color.BLUE);
assertConsoleTextColor(text3, Color.RED);
assertConsoleTextColor(text2, Color.BLACK);
}
@Test
public void testScriptPrintlnWithColor() throws Exception {
// create a script
ResourceFile newScriptFile = createTempScriptFile("LineColoringScript");
String filename = newScriptFile.getName();
String className = filename.replaceAll("\\.java", "");
String line1 = "1 This is a default line";
String line2 = "2 This is a blue line";
String line3 = "3 This is a red line";
//@formatter:off
String newScript = """
import ghidra.app.script.GhidraScript;
import java.awt.Color;
public class %s extends GhidraScript {
@Override
public void run() throws Exception {
println("%s");
println("%s", Color.BLUE);
println("%s", Color.RED);
}
};
""".formatted(className, line1, line2, line3);
//@formatter:on
writeStringToFile(newScriptFile, newScript);
runScript(newScriptFile);
waitForSwing();
assertConsoleTextColor(line1, Color.BLACK);
assertConsoleTextColor(line2, Color.BLUE);
assertConsoleTextColor(line3, Color.RED);
}
private void assertConsoleTextColor(String text, Color expectedFgColor) {
String fullText = runSwing(() -> consoleTextPane.getText());
// We have 2 layers of newlines in the test. A '\\n' that gets written to file as Java
// code. That then gets compiled and written out as a newline. Our 'text' value passed
// here is that original '\\n'. We are trying to compare that against what ends up in the
// console, which has gone through 2 string interpretations to end up as a standard newline.
// Strip off the '\\n' from the original input text before looking for it in the console.
String visibleText = text.replaceAll("\\\\n", "");
int start = fullText.indexOf(visibleText);
int end = visibleText.length();
runSwing(() -> {
StyledDocument styledDocument = (StyledDocument) consoleTextPane.getDocument();
for (int i = start; i < end; i++) {
Element element = styledDocument.getCharacterElement(i);
AttributeSet attrs = element.getAttributes();
Color actualFgColor = (Color) attrs.getAttribute(StyleConstants.Foreground);
assertEquals(expectedFgColor, actualFgColor);
}
});
}
private Path getBinDirFromScriptFile(ResourceFile sourceFile) {
ResourceFile tmpSourceDir = sourceFile.getParentFile();
String tmpSymbolicName = GhidraSourceBundle.sourceDirHash(tmpSourceDir);

View file

@ -15,6 +15,7 @@
*/
package generic.theme;
import java.awt.Color;
import java.awt.Font;
import javax.swing.text.SimpleAttributeSet;
@ -32,7 +33,7 @@ public class GAttributes extends SimpleAttributeSet {
this(f, null);
}
public GAttributes(Font f, GColor c) {
public GAttributes(Font f, Color c) {
addAttribute(StyleConstants.FontFamily, f.getFamily());
addAttribute(StyleConstants.FontSize, f.getSize());
addAttribute(StyleConstants.Bold, f.isBold());