GP-1977: Introduce Terminal Service and Plugin

This commit is contained in:
Dan 2023-08-31 14:56:38 -04:00
parent bafded084e
commit 482341f6b1
98 changed files with 7972 additions and 141 deletions

View file

@ -29,7 +29,7 @@ dependencies {
api project(':Framework-AsyncComm')
api project(':Framework-Debugging')
api project(':Debugger-gadp')
api 'com.jcraft:jsch:0.1.55'
api project(':Pty')
testImplementation project(path: ':Framework-AsyncComm', configuration: 'testArtifacts')
testImplementation project(path: ':Framework-Debugging', configuration: 'testArtifacts')

View file

@ -20,12 +20,12 @@ import java.util.concurrent.CompletableFuture;
import agent.gdb.manager.GdbManager;
import agent.gdb.model.impl.GdbModelImpl;
import agent.gdb.pty.PtyFactory;
import ghidra.dbg.DebuggerModelFactory;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.util.ConfigurableFactory.FactoryDescription;
import ghidra.dbg.util.ShellUtils;
import ghidra.program.model.listing.Program;
import ghidra.pty.PtyFactory;
@FactoryDescription(
brief = "gdb",

View file

@ -19,12 +19,12 @@ import java.util.List;
import java.util.concurrent.CompletableFuture;
import agent.gdb.model.impl.GdbModelImpl;
import agent.gdb.pty.ssh.GhidraSshPtyFactory;
import ghidra.dbg.DebuggerModelFactory;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.util.ShellUtils;
import ghidra.dbg.util.ConfigurableFactory.FactoryDescription;
import ghidra.program.model.listing.Program;
import ghidra.pty.ssh.GhidraSshPtyFactory;
@FactoryDescription(
brief = "gdb via SSH",

View file

@ -21,8 +21,8 @@ import java.util.concurrent.CompletableFuture;
import agent.gdb.gadp.GdbGadpServer;
import agent.gdb.model.impl.GdbModelImpl;
import agent.gdb.pty.PtyFactory;
import ghidra.dbg.gadp.server.AbstractGadpServer;
import ghidra.pty.PtyFactory;
public class GdbGadpServerImpl implements GdbGadpServer {
public class GadpSide extends AbstractGadpServer {

View file

@ -24,8 +24,8 @@ import java.util.concurrent.ExecutionException;
import agent.gdb.manager.breakpoint.GdbBreakpointInfo;
import agent.gdb.manager.breakpoint.GdbBreakpointInsertions;
import agent.gdb.manager.impl.GdbManagerImpl;
import agent.gdb.pty.PtyFactory;
import agent.gdb.pty.linux.LinuxPty;
import ghidra.pty.PtyFactory;
import ghidra.pty.linux.LinuxPty;
/**
* The controlling side of a GDB session, using GDB/MI, usually via a pseudo-terminal

View file

@ -40,9 +40,6 @@ import agent.gdb.manager.impl.cmd.GdbConsoleExecCommand.CompletesWithRunning;
import agent.gdb.manager.parsing.GdbMiParser;
import agent.gdb.manager.parsing.GdbMiParser.GdbMiFieldList;
import agent.gdb.manager.parsing.GdbParsingUtils.GdbParseError;
import agent.gdb.pty.*;
import agent.gdb.pty.PtyChild.Echo;
import agent.gdb.pty.windows.AnsiBufferedInputStream;
import ghidra.GhidraApplicationLayout;
import ghidra.async.*;
import ghidra.async.AsyncLock.Hold;
@ -51,6 +48,9 @@ import ghidra.dbg.util.HandlerMap;
import ghidra.dbg.util.PrefixMap;
import ghidra.framework.OperatingSystem;
import ghidra.lifecycle.Internal;
import ghidra.pty.*;
import ghidra.pty.PtyChild.Echo;
import ghidra.pty.windows.AnsiBufferedInputStream;
import ghidra.util.Msg;
import ghidra.util.SystemUtilities;
import ghidra.util.datastruct.ListenerSet;

View file

@ -24,7 +24,6 @@ import org.apache.commons.lang3.exception.ExceptionUtils;
import agent.gdb.manager.*;
import agent.gdb.manager.impl.cmd.GdbCommandError;
import agent.gdb.pty.PtyFactory;
import ghidra.async.AsyncUtils;
import ghidra.dbg.DebuggerModelClosedReason;
import ghidra.dbg.agent.AbstractDebuggerObjectModel;
@ -34,6 +33,7 @@ import ghidra.dbg.target.TargetObject;
import ghidra.dbg.target.schema.AnnotatedSchemaContext;
import ghidra.dbg.target.schema.TargetObjectSchema;
import ghidra.program.model.address.*;
import ghidra.pty.PtyFactory;
public class GdbModelImpl extends AbstractDebuggerObjectModel {
// TODO: Need some minimal memory modeling per architecture on the model/agent side.

View file

@ -35,12 +35,12 @@ import org.junit.*;
import agent.gdb.manager.*;
import agent.gdb.manager.GdbManager.StepCmd;
import agent.gdb.manager.breakpoint.GdbBreakpointInfo;
import agent.gdb.pty.PtyFactory;
import agent.gdb.pty.linux.LinuxPtyFactory;
import generic.ULongSpan;
import generic.ULongSpan.ULongSpanSet;
import ghidra.async.AsyncReference;
import ghidra.dbg.testutil.DummyProc;
import ghidra.pty.PtyFactory;
import ghidra.pty.linux.LinuxPtyFactory;
import ghidra.test.AbstractGhidraHeadlessIntegrationTest;
import ghidra.util.Msg;
import ghidra.util.SystemUtilities;

View file

@ -22,8 +22,8 @@ import java.util.concurrent.CompletableFuture;
import org.junit.Ignore;
import agent.gdb.manager.GdbManager;
import agent.gdb.pty.PtySession;
import agent.gdb.pty.linux.LinuxPty;
import ghidra.pty.PtySession;
import ghidra.pty.linux.LinuxPty;
import ghidra.util.Msg;
@Ignore("Need compatible GDB version for CI")

View file

@ -21,8 +21,8 @@ import java.util.concurrent.CompletableFuture;
import org.junit.Ignore;
import agent.gdb.manager.GdbManager;
import agent.gdb.pty.PtyFactory;
import agent.gdb.pty.windows.ConPtyFactory;
import ghidra.pty.PtyFactory;
import ghidra.pty.windows.ConPtyFactory;
@Ignore("Need compatible version on CI")
public class SpawnedWindowsMi2GdbManagerTest extends AbstractGdbManagerTest {

View file

@ -18,9 +18,9 @@ package agent.gdb.model.ssh;
import java.util.Map;
import agent.gdb.GdbOverSshDebuggerModelFactory;
import agent.gdb.pty.ssh.SshPtyTest;
import ghidra.dbg.DebuggerModelFactory;
import ghidra.dbg.test.AbstractModelHost;
import ghidra.pty.ssh.SshPtyTest;
import ghidra.util.exception.CancelledException;
public class SshGdbModelHost extends AbstractModelHost {

View file

@ -18,9 +18,9 @@ package agent.gdb.model.ssh;
import java.util.Map;
import agent.gdb.GdbOverSshDebuggerModelFactory;
import agent.gdb.pty.ssh.SshPtyTest;
import ghidra.dbg.DebuggerModelFactory;
import ghidra.dbg.test.AbstractModelHost;
import ghidra.pty.ssh.SshPtyTest;
import ghidra.util.exception.CancelledException;
public class SshJoinGdbModelHost extends AbstractModelHost {

View file

@ -25,6 +25,7 @@ apply plugin: 'eclipse'
eclipse.project.name = 'Debug Debugger-rmi-trace'
dependencies {
api project(':Pty')
api project(':Debugger')
}

View file

@ -0,0 +1,39 @@
/* ###
* 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.
*/
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import ghidra.framework.plugintool.util.PluginException;
import ghidra.pty.Pty;
import ghidra.pty.PtySession;
public class RunBashInTerminalScript extends TerminalGhidraScript {
@Override
protected void runSession(Pty pty) throws IOException, PluginException {
Map<String, String> env = new HashMap<>(System.getenv());
env.put("TERM", "xterm-256color");
PtySession session = pty.getChild().session(new String[] { "/usr/bin/bash" }, env);
displayInTerminal(pty.getParent(), () -> {
try {
session.waitExited();
}
catch (InterruptedException e) {
return;
}
});
}
}

View file

@ -0,0 +1,77 @@
/* ###
* 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.
*/
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import ghidra.app.plugin.core.terminal.TerminalListener;
import ghidra.app.plugin.core.terminal.TerminalPlugin;
import ghidra.app.script.GhidraScript;
import ghidra.app.services.Terminal;
import ghidra.app.services.TerminalService;
import ghidra.framework.plugintool.util.PluginException;
import ghidra.pty.*;
public class TerminalGhidraScript extends GhidraScript {
protected TerminalService ensureTerminalService() throws PluginException {
TerminalService termServ = state.getTool().getService(TerminalService.class);
if (termServ != null) {
return termServ;
}
state.getTool().addPlugin(TerminalPlugin.class.getName());
return state.getTool().getService(TerminalService.class);
}
protected void displayInTerminal(PtyParent parent, Runnable waiter) throws PluginException {
TerminalService terminalService = ensureTerminalService();
try (Terminal term = terminalService.createWithStreams(Charset.forName("US-ASCII"),
parent.getInputStream(), parent.getOutputStream())) {
term.addTerminalListener(new TerminalListener() {
@Override
public void resized(int cols, int rows) {
parent.setWindowSize(cols, rows);
}
});
waiter.run();
}
}
protected void runSession(Pty pty) throws IOException, PluginException {
Map<String, String> env = new HashMap<>(System.getenv());
env.put("TERM", "xterm-256color");
pty.getChild().nullSession();
displayInTerminal(pty.getParent(), () -> {
while (true) {
try {
Thread.sleep(100000);
}
catch (InterruptedException e) {
return;
}
}
});
}
@Override
protected void run() throws Exception {
PtyFactory factory = PtyFactory.local();
try (Pty pty = factory.openpty()) {
runSession(pty);
}
}
}

View file

@ -27,7 +27,7 @@ dependencies {
api project(':Generic')
api project(':SoftwareModeling')
api project(':ProposedUtils')
api "net.java.dev.jna:jna:5.4.0"
api "net.java.dev.jna:jna-platform:5.4.0"

View file

@ -106,6 +106,29 @@ color.fg.plugin.interpreter.renderer.color.intense.6 = color.palette.magenta
color.fg.plugin.interpreter.renderer.color.intense.7 = color.palette.cyan
color.fg.plugin.interpreter.renderer.color.intense.8 = color.palette.white
// Taken from Terminal.app as documented on Wikipedia
color.bg.plugin.terminal = rgb(0,0,0)
color.fg.plugin.terminal = rgb(203,204,205)
color.cursor.focused.terminal = color.cursor.focused
color.cursor.unfocused.terminal = color.cursor.unfocused
color.fg.plugin.terminal.normal.black = rgb(0,0,0)
color.fg.plugin.terminal.normal.red = rgb(194,54,33)
color.fg.plugin.terminal.normal.green = rgb(37,188,36)
color.fg.plugin.terminal.normal.yellow = rgb(173,173,39)
color.fg.plugin.terminal.normal.blue = rgb(73,46,255)
color.fg.plugin.terminal.normal.magenta = rgb(211,56,211)
color.fg.plugin.terminal.normal.cyan = rgb(51,187,200)
color.fg.plugin.terminal.normal.white = rgb(203,204,205)
color.fg.plugin.terminal.bright.black = rgb(129,131,131)
color.fg.plugin.terminal.bright.red = rgb(252,57,31)
color.fg.plugin.terminal.bright.green = rgb(49,231,34)
color.fg.plugin.terminal.bright.yellow = rgb(234,236,35)
color.fg.plugin.terminal.bright.blue = rgb(88,51,255)
color.fg.plugin.terminal.bright.magenta = rgb(249,53,248)
color.fg.plugin.terminal.bright.cyan = rgb(20,240,240)
color.fg.plugin.terminal.bright.white = rgb(233,235,235)
color.bg.plugin.locationreferences.highlight = color.palette.lightcornflowerblue
color.bg.plugin.myprogramchangesdisplay.markers.changes.unsaved = color.palette.darkgray
@ -155,7 +178,7 @@ font.plugin.tabs = SansSerif-PLAIN-11
font.plugin.tabs.list = SansSerif-BOLD-9
font.plugin.tips = Dialog-PLAIN-12
font.plugin.tips.label = font.plugin.tips[BOLD]
font.plugin.terminal = font.monospaced

View file

@ -173,7 +173,8 @@ public class ClipboardPlugin extends ProgramPlugin implements ClipboardOwner, Cl
}
/**
* @see java.awt.datatransfer.ClipboardOwner#lostOwnership(java.awt.datatransfer.Clipboard, java.awt.datatransfer.Transferable)
* @see java.awt.datatransfer.ClipboardOwner#lostOwnership(java.awt.datatransfer.Clipboard,
* java.awt.datatransfer.Transferable)
*/
@Override
public void lostOwnership(Clipboard clipboard, Transferable contents) {
@ -353,11 +354,19 @@ public class ClipboardPlugin extends ProgramPlugin implements ClipboardOwner, Cl
// maker interface
}
private String getActionOwner(ClipboardContentProviderService clipboardService) {
String owner = clipboardService.getClipboardActionOwner();
if (owner != null) {
return owner;
}
return getName();
}
private class CopyAction extends DockingAction implements ICopy {
private final ClipboardContentProviderService clipboardService;
private CopyAction(ClipboardContentProviderService clipboardService) {
super("Copy", ClipboardPlugin.this.getName());
super("Copy", getActionOwner(clipboardService));
this.clipboardService = clipboardService;
setPopupMenuData(new MenuData(new String[] { "Copy" }, "Clipboard"));
@ -365,6 +374,7 @@ public class ClipboardPlugin extends ProgramPlugin implements ClipboardOwner, Cl
"Clipboard"));
setKeyBindingData(new KeyBindingData(KeyEvent.VK_C, InputEvent.CTRL_DOWN_MASK));
setHelpLocation(new HelpLocation("ClipboardPlugin", "Copy"));
clipboardService.customizeClipboardAction(this);
}
@Override
@ -390,7 +400,7 @@ public class ClipboardPlugin extends ProgramPlugin implements ClipboardOwner, Cl
private final ClipboardContentProviderService clipboardService;
private PasteAction(ClipboardContentProviderService clipboardService) {
super("Paste", ClipboardPlugin.this.getName());
super("Paste", ClipboardPlugin.this.getActionOwner(clipboardService));
this.clipboardService = clipboardService;
setPopupMenuData(new MenuData(new String[] { "Paste" }, "Clipboard"));
@ -398,6 +408,7 @@ public class ClipboardPlugin extends ProgramPlugin implements ClipboardOwner, Cl
new ToolBarData(new GIcon("icon.plugin.clipboard.paste"), "Clipboard"));
setKeyBindingData(new KeyBindingData(KeyEvent.VK_V, InputEvent.CTRL_DOWN_MASK));
setHelpLocation(new HelpLocation("ClipboardPlugin", "Paste"));
clipboardService.customizeClipboardAction(this);
}
@Override
@ -426,12 +437,13 @@ public class ClipboardPlugin extends ProgramPlugin implements ClipboardOwner, Cl
private final ClipboardContentProviderService clipboardService;
private CopySpecialAction(ClipboardContentProviderService clipboardService) {
super("Copy Special", ClipboardPlugin.this.getName());
super("Copy Special", ClipboardPlugin.this.getActionOwner(clipboardService));
this.clipboardService = clipboardService;
setPopupMenuData(new MenuData(new String[] { "Copy Special..." }, "Clipboard"));
setEnabled(false);
setHelpLocation(new HelpLocation("ClipboardPlugin", "Copy_Special"));
clipboardService.customizeClipboardAction(this);
}
@Override
@ -457,12 +469,13 @@ public class ClipboardPlugin extends ProgramPlugin implements ClipboardOwner, Cl
private final ClipboardContentProviderService clipboardService;
private CopySpecialAgainAction(ClipboardContentProviderService clipboardService) {
super("Copy Special Again", ClipboardPlugin.this.getName());
super("Copy Special Again", ClipboardPlugin.this.getActionOwner(clipboardService));
this.clipboardService = clipboardService;
setPopupMenuData(new MenuData(new String[] { "Copy Special Again" }, "Clipboard"));
setEnabled(false);
setHelpLocation(new HelpLocation("ClipboardPlugin", "Copy_Special"));
clipboardService.customizeClipboardAction(this);
}
@Override

View file

@ -36,9 +36,6 @@ import ghidra.util.ColorUtils;
public class AnsiRenderer {
/**
* These colors are taken from Terminal.app as documented on Wikipedia as of 26 April 2022.
*
* <p>
* See <a href="https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit">ANSI escape
* code</a> on Wikipedia. They appear here in ANSI order.
*/

View file

@ -0,0 +1,67 @@
/* ###
* 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.plugin.core.terminal;
import java.nio.ByteBuffer;
import ghidra.app.plugin.core.terminal.vt.VtOutput;
import ghidra.app.services.Terminal;
import ghidra.util.Swing;
/**
* A terminal that does nothing on its own.
*
* <p>
* Everything displayed happens via {@link #injectDisplayOutput(ByteBuffer)}, and everything typed
* into it is emitted via the {@link VtOutput}, which was given at construction.
*/
public class DefaultTerminal implements Terminal {
protected final TerminalProvider provider;
public DefaultTerminal(TerminalProvider provider) {
this.provider = provider;
}
@Override
public void close() {
Swing.runIfSwingOrRunLater(() -> provider.removeFromTool());
}
@Override
public void addTerminalListener(TerminalListener listener) {
provider.addTerminalListener(listener);
}
@Override
public void removeTerminalListener(TerminalListener listener) {
provider.removeTerminalListener(listener);
}
@Override
public void injectDisplayOutput(ByteBuffer bb) {
provider.processInput(bb);
}
@Override
public void setFixedSize(int rows, int cols) {
provider.setFixedSize(rows, cols);
}
@Override
public void setDynamicSize() {
provider.setDyanmicSize();
}
}

View file

@ -0,0 +1,314 @@
/* ###
* 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.plugin.core.terminal;
import java.awt.event.*;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.*;
import ghidra.util.Msg;
/**
* An encoder which can translate AWT/Swing events into ANSI input codes.
*
* <p>
* The input system is not as well decoupled from Swing as the output system. For ease of use, the
* methods are named the same as their corresponding Swing event listener methods, though they may
* require additional arguments. These in turn invoke the {@link #generateBytes(ByteBuffer)} method,
* which the implementor must send to the appropriate recipient, usually a pty.
*/
public abstract class TerminalAwtEventEncoder {
public static final byte[] CODE_NONE = {};
public static byte[] vtseq(int number) {
try {
return ("\033[" + number + "~").getBytes("ASCII");
}
catch (UnsupportedEncodingException e) {
throw new AssertionError(e);
}
}
public static final byte ESC = (byte) 0x1b;
public static final byte FUNC = (byte) 0x4f;
public static final byte[] CODE_INSERT = vtseq(2);
public static final byte[] CODE_DELETE = vtseq(3);
public static final byte[] CODE_HOME = { ESC, '[', 'H' };
public static final byte[] CODE_END = { ESC, '[', 'F' };
public static final byte[] CODE_PAGE_UP = vtseq(5);
public static final byte[] CODE_PAGE_DOWN = vtseq(6);
public static final byte[] CODE_NUMPAD5 = { ESC, '[', 'E' };
public static final byte[] CODE_UP = { ESC, FUNC, 'A' };
public static final byte[] CODE_DOWN = { ESC, FUNC, 'B' };
public static final byte[] CODE_RIGHT = { ESC, FUNC, 'C' };
public static final byte[] CODE_LEFT = { ESC, FUNC, 'D' };
public static final byte[] CODE_F1 = { ESC, FUNC, 'P' };
public static final byte[] CODE_F2 = { ESC, FUNC, 'Q' };
public static final byte[] CODE_F3 = { ESC, FUNC, 'R' };
public static final byte[] CODE_F4 = { ESC, FUNC, 'S' };
public static final byte[] CODE_F5 = vtseq(15);
public static final byte[] CODE_F6 = vtseq(17);
public static final byte[] CODE_F7 = vtseq(18);
public static final byte[] CODE_F8 = vtseq(19);
public static final byte[] CODE_F9 = vtseq(20);
public static final byte[] CODE_F10 = vtseq(21);
public static final byte[] CODE_F11 = vtseq(23);
public static final byte[] CODE_F12 = vtseq(24);
public static final byte[] CODE_F13 = vtseq(25);
public static final byte[] CODE_F14 = vtseq(26);
public static final byte[] CODE_F15 = vtseq(28);
public static final byte[] CODE_F16 = vtseq(29);
public static final byte[] CODE_F17 = vtseq(31);
public static final byte[] CODE_F18 = vtseq(32);
public static final byte[] CODE_F19 = vtseq(33);
public static final byte[] CODE_F20 = vtseq(34);
public static final byte[] CODE_FOCUS_GAINED = { ESC, '[', 'I' };
public static final byte[] CODE_FOCUS_LOST = { ESC, '[', 'O' };
protected final Charset charset;
protected final CharsetEncoder encoder;
protected final ByteBuffer bb = ByteBuffer.allocate(16);
protected final CharBuffer cb = CharBuffer.allocate(16);
public TerminalAwtEventEncoder(String charsetName) {
this(Charset.forName(charsetName));
}
public TerminalAwtEventEncoder(Charset charset) {
this.charset = charset;
this.encoder = charset.newEncoder();
}
protected abstract void generateBytes(ByteBuffer buf);
protected byte[] getModifiedAnsiKeyCode(KeyEvent e) {
int modifier = 1;
if (e.isShiftDown()) {
modifier += 1;
}
if (e.isAltDown()) {
modifier += 2;
}
if (e.isControlDown()) {
modifier += 4;
}
if (e.isMetaDown()) {
modifier += 8;
}
int code = switch (e.getKeyCode()) {
case KeyEvent.VK_HOME -> 1;
case KeyEvent.VK_INSERT -> 2;
case KeyEvent.VK_DELETE -> 3; // TODO: Already handled?
case KeyEvent.VK_END -> 4;
case KeyEvent.VK_PAGE_UP -> 5;
case KeyEvent.VK_PAGE_DOWN -> 6;
case KeyEvent.VK_F1 -> 11;
case KeyEvent.VK_F2 -> 12;
case KeyEvent.VK_F3 -> 13;
case KeyEvent.VK_F4 -> 14;
case KeyEvent.VK_F5 -> 15;
case KeyEvent.VK_F6 -> 17;
case KeyEvent.VK_F7 -> 18;
case KeyEvent.VK_F8 -> 19;
case KeyEvent.VK_F9 -> 20;
case KeyEvent.VK_F10 -> 21;
case KeyEvent.VK_F11 -> 23;
case KeyEvent.VK_F12 -> 24;
case KeyEvent.VK_F13 -> 25;
case KeyEvent.VK_F14 -> 26;
case KeyEvent.VK_F15 -> 28;
case KeyEvent.VK_F16 -> 29;
case KeyEvent.VK_F17 -> 31;
case KeyEvent.VK_F18 -> 32;
case KeyEvent.VK_F19 -> 33;
case KeyEvent.VK_F20 -> 34;
default -> -1;
};
if (code == -1) {
return CODE_NONE;
}
try {
// TODO: This doesn't seem to work right, but I'm lost trying to fix it.
return "\033[%d;%d~".formatted(code, modifier).getBytes("ASCII");
}
catch (UnsupportedEncodingException ex) {
throw new AssertionError(ex);
}
}
protected byte[] getAnsiKeyCode(KeyEvent e) {
if (e.getModifiersEx() != 0) {
return getModifiedAnsiKeyCode(e);
}
return switch (e.getKeyCode()) {
case KeyEvent.VK_INSERT -> CODE_INSERT;
// NB. CODE_DELETE is handled in keyTyped
case KeyEvent.VK_HOME -> CODE_HOME;
case KeyEvent.VK_END -> CODE_END;
case KeyEvent.VK_PAGE_UP -> CODE_PAGE_UP;
case KeyEvent.VK_PAGE_DOWN -> CODE_PAGE_DOWN;
case KeyEvent.VK_NUMPAD5 -> CODE_NUMPAD5;
case KeyEvent.VK_UP -> CODE_UP;
case KeyEvent.VK_DOWN -> CODE_DOWN;
case KeyEvent.VK_RIGHT -> CODE_RIGHT;
case KeyEvent.VK_LEFT -> CODE_LEFT;
case KeyEvent.VK_F1 -> CODE_F1;
case KeyEvent.VK_F2 -> CODE_F2;
case KeyEvent.VK_F3 -> CODE_F3;
case KeyEvent.VK_F4 -> CODE_F4;
case KeyEvent.VK_F5 -> CODE_F5;
case KeyEvent.VK_F6 -> CODE_F6;
case KeyEvent.VK_F7 -> CODE_F7;
case KeyEvent.VK_F8 -> CODE_F8;
case KeyEvent.VK_F9 -> CODE_F9;
case KeyEvent.VK_F10 -> CODE_F10;
case KeyEvent.VK_F11 -> CODE_F11;
case KeyEvent.VK_F12 -> CODE_F12;
case KeyEvent.VK_F13 -> CODE_F13;
case KeyEvent.VK_F14 -> CODE_F14;
case KeyEvent.VK_F15 -> CODE_F15;
case KeyEvent.VK_F16 -> CODE_F16;
case KeyEvent.VK_F17 -> CODE_F17;
case KeyEvent.VK_F18 -> CODE_F18;
case KeyEvent.VK_F19 -> CODE_F19;
case KeyEvent.VK_F20 -> CODE_F20;
// F21-F24 are not given on Wikipedia...
default -> CODE_NONE;
};
}
public void keyPressed(KeyEvent e) {
byte[] bytes = getAnsiKeyCode(e);
bb.put(bytes);
generateBytesExc();
}
public void keyTyped(KeyEvent e) {
sendChar(e.getKeyChar());
}
public void mousePressed(MouseEvent e, int row, int col) {
mouseEvent(e, row, col, true);
}
public void mouseReleased(MouseEvent e, int row, int col) {
mouseEvent(e, row, col, false);
}
protected int translateModifiers(InputEvent e) {
int mods = 0;
if (e.isShiftDown()) {
mods += 4;
}
if (e.isMetaDown()) {
mods += 8;
}
if (e.isControlDown()) {
mods += 16;
}
return mods;
}
protected void sendMouseEvent(int buttonsAndModifiers, int row, int col) {
cb.clear();
cb.put("\033[M");
cb.put((char) (' ' + buttonsAndModifiers));
cb.put((char) (' ' + col));
cb.put((char) (' ' + row));
sendCharBuffer();
}
protected void mouseEvent(MouseEvent e, int row, int col, boolean isPress) {
int buttonsAndModifiers = isPress ? switch (e.getButton()) {
case MouseEvent.BUTTON1 -> 0;
case MouseEvent.BUTTON2 -> 1;
case MouseEvent.BUTTON3 -> 2;
default -> throw new AssertionError();
} : 3;
buttonsAndModifiers += translateModifiers(e);
sendMouseEvent(buttonsAndModifiers, row, col);
}
public void mouseWheelMoved(MouseWheelEvent e, int row, int col) {
int buttonsAndModifiers = (e.getWheelRotation() < 0 ? 0 : 1) + 64;
buttonsAndModifiers += translateModifiers(e);
sendMouseEvent(buttonsAndModifiers, row, col);
}
public void focusGained() {
bb.put(CODE_FOCUS_GAINED);
generateBytesExc();
}
public void focusLost() {
bb.put(CODE_FOCUS_LOST);
generateBytesExc();
}
protected void sendCharBuffer() {
cb.flip();
CoderResult result = encoder.encode(cb, bb, true);
cb.compact();
if (result.isError()) {
Msg.error(this, "Error while encoding");
encoder.reset();
cb.clear();
}
generateBytesExc();
}
public void sendChar(char c) {
switch (c) {
case 0x7f:
bb.put(CODE_DELETE);
generateBytesExc();
break;
default:
/**
* If I ever care to support Unicode, I may need to worry about surrogate pairs.
*/
cb.clear();
cb.put(c);
sendCharBuffer();
break;
}
}
protected void generateBytesExc() {
bb.flip();
try {
generateBytes(bb);
}
catch (Throwable t) {
Msg.error(this, "Error generating bytes: " + t, t);
}
finally {
bb.clear();
}
}
public void sendText(CharSequence text) {
for (int i = 0; i < text.length(); i++) {
sendChar(text.charAt(i));
}
}
}

View file

@ -0,0 +1,193 @@
/* ###
* 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.plugin.core.terminal;
import java.awt.datatransfer.*;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.io.IOException;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.apache.commons.lang3.ArrayUtils;
import docking.ActionContext;
import docking.ComponentProvider;
import docking.action.DockingAction;
import docking.action.KeyBindingData;
import docking.dnd.StringTransferable;
import docking.widgets.fieldpanel.support.FieldSelection;
import ghidra.app.services.ClipboardContentProviderService;
import ghidra.app.util.ClipboardType;
import ghidra.util.Msg;
import ghidra.util.task.TaskMonitor;
/**
* The clipboard provider for the terminal plugin.
*
* <p>
* In addition to providing clipboard contents and paste functionality, this customizes the Copy and
* Paste actions. We change the "owner" to be this plugin, so that the action can be configured
* independently of the standard Copy and Paste actions. Then, we re-bind the keys to Ctrl+Shift+C
* and Shift+Shift+V, respectively. This ensures that Ctrl+C will still send an Interrupt (char 3).
* This is the convention followed by just about every XTerm clone.
*/
public class TerminalClipboardProvider implements ClipboardContentProviderService {
protected static final ClipboardType TEXT_TYPE =
new ClipboardType(DataFlavor.stringFlavor, "Text");
protected static final List<ClipboardType> COPY_TYPES = List.of(TEXT_TYPE);
protected final TerminalProvider provider;
protected FieldSelection selection;
protected final Set<ChangeListener> listeners = new CopyOnWriteArraySet<>();
public TerminalClipboardProvider(TerminalProvider provider) {
this.provider = provider;
}
@Override
public ComponentProvider getComponentProvider() {
return provider;
}
@Override
public Transferable copy(TaskMonitor monitor) {
if (selection == null || selection.getNumRanges() != 1) {
return null;
}
String text = provider.panel.getSelectedText(selection.getFieldRange(0));
if (text == null) {
return null;
}
return new StringTransferable(text);
}
@Override
public Transferable copySpecial(ClipboardType copyType, TaskMonitor monitor) {
throw new UnsupportedOperationException();
}
@Override
public boolean paste(Transferable pasteData) {
try {
String text = (String) pasteData.getTransferData(DataFlavor.stringFlavor);
provider.panel.paste(text);
return true;
}
catch (UnsupportedFlavorException | IOException e) {
return false;
}
}
@Override
public List<ClipboardType> getCurrentCopyTypes() {
if (selection == null || selection.getNumRanges() == 0) {
return List.of();
}
return COPY_TYPES;
}
@Override
public boolean isValidContext(ActionContext context) {
return context.getComponentProvider() == provider;
}
@Override
public boolean enableCopy() {
return true;
}
@Override
public boolean enableCopySpecial() {
return false;
}
@Override
public boolean enablePaste() {
return true;
}
@Override
public void lostOwnership(Transferable transferable) {
// Nothing to do
}
@Override
public void addChangeListener(ChangeListener listener) {
listeners.add(listener);
}
@Override
public void removeChangeListener(ChangeListener listener) {
listeners.remove(listener);
}
@Override
public boolean canPaste(DataFlavor[] availableFlavors) {
return -1 != ArrayUtils.indexOf(availableFlavors, DataFlavor.stringFlavor);
}
@Override
public boolean canCopy() {
return selection != null && selection.getNumRanges() == 1;
}
@Override
public boolean canCopySpecial() {
return false;
}
private void notifyStateChanged() {
ChangeEvent event = new ChangeEvent(this);
for (ChangeListener listener : listeners) {
try {
listener.stateChanged(event);
}
catch (Throwable t) {
Msg.showError(this, null, "Error", t.getMessage(), t);
}
}
}
public void selectionChanged(FieldSelection selection) {
this.selection = selection;
notifyStateChanged();
}
@Override
public String getClipboardActionOwner() {
return provider.plugin.getName();
}
@Override
public void customizeClipboardAction(DockingAction action) {
switch (action.getName()) {
case "Copy":
action.setKeyBindingData(new KeyBindingData(KeyEvent.VK_C,
InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK));
break;
case "Paste":
action.setKeyBindingData(new KeyBindingData(KeyEvent.VK_V,
InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK));
break;
}
}
}

View file

@ -0,0 +1,250 @@
/* ###
* 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.plugin.core.terminal;
import java.math.BigInteger;
import java.util.EnumSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import docking.widgets.fieldpanel.support.FieldLocation;
import docking.widgets.fieldpanel.support.FieldRange;
import ghidra.app.plugin.core.terminal.TerminalPanel.FindOptions;
import ghidra.app.plugin.core.terminal.vt.VtLine;
/**
* The algorithm for finding text in the terminal buffer.
*
* <p>
* This is an abstract class, so that text search and regex search are better separated, while the
* common parts need not be duplicated.
*/
public abstract class TerminalFinder {
protected final TerminalLayoutModel model;
protected final FieldLocation cur;
protected final boolean forward;
protected final boolean caseSensitive;
protected final boolean wrap;
protected final boolean wholeWord;
protected final StringBuilder sb = new StringBuilder();
/**
* Create a finder on the given model
*
* @see TerminalPanel#find(String, Set, FieldLocation, boolean)
* @param model the model
* @param cur the start of the current selection, or null
* @param forward true for forward, false for backward
* @param options a set of options, preferably an {@link EnumSet}
*/
protected TerminalFinder(TerminalLayoutModel model, FieldLocation cur, boolean forward,
Set<FindOptions> options) {
this.model = model;
if (cur != null) {
this.cur = cur;
}
else if (forward) {
this.cur = new FieldLocation();
}
else {
BigInteger maxIndex = model.getNumIndexes().subtract(BigInteger.ONE);
int maxChar = model.getLayout(maxIndex).line.length();
this.cur = new FieldLocation(maxIndex, 0, 0, maxChar);
}
this.forward = forward;
this.caseSensitive = options.contains(FindOptions.CASE_SENSITIVE);
this.wrap = options.contains(FindOptions.WRAP);
this.wholeWord = options.contains(FindOptions.WHOLE_WORD);
}
protected void lowerBuf(StringBuilder sb) {
for (int i = 0; i < sb.length(); i++) {
sb.setCharAt(i, Character.toLowerCase(sb.charAt(i)));
}
}
protected boolean isWholeWord(int i, String match) {
if (i > 0 && VtLine.isWordChar(sb.charAt(i - 1))) {
return false;
}
int iAfter = i + match.length();
if (iAfter < sb.length() && VtLine.isWordChar(sb.charAt(iAfter))) {
return false;
}
return true;
}
protected abstract FieldRange findInLine(int start, BigInteger index);
protected boolean continueIndex(BigInteger index, BigInteger end) {
if (forward) {
return index.compareTo(end) <= 0;
}
return index.compareTo(end) >= 0;
}
/**
* Search within the layouts in the given range of indices, inclusive
*
* @param start the first index
* @param end the last index, inclusive
* @param step the step (1 or -1)
* @return the field range, if found, or null
*/
protected FieldRange findInIndices(BigInteger start, BigInteger end, BigInteger step) {
for (BigInteger index = start; continueIndex(index, end); index = index.add(step)) {
TerminalLayout layout = model.getLayout(index);
VtLine line = layout.line;
sb.delete(0, sb.length());
line.gatherText(sb, 0, line.length());
if (!caseSensitive) {
lowerBuf(sb);
}
int s;
if (index.equals(cur.getIndex())) {
s = cur.getCol();
}
else {
s = forward ? 0 : line.length() - 1;
}
FieldRange found = findInLine(s, index);
if (found != null) {
return found;
}
}
return null;
}
/**
* Execute the search
*
* @return the range covering the found term, or null if not found
*/
public FieldRange find() {
BigInteger step = forward ? BigInteger.ONE : BigInteger.ONE.negate();
BigInteger maxIndex = model.getNumIndexes().subtract(BigInteger.ONE);
FieldRange found = findInIndices(cur.getIndex(),
forward ? maxIndex : BigInteger.ZERO, step);
if (found != null) {
return found;
}
if (!wrap) {
return null;
}
return findInIndices(forward ? BigInteger.ZERO : maxIndex,
cur.getIndex(), step);
}
/**
* A finder that searches for exact text, case insensitive by default
*/
public static class TextTerminalFinder extends TerminalFinder {
protected final String text;
/**
* @see TerminalPanel#find(String, Set, FieldLocation, boolean)
*/
public TextTerminalFinder(TerminalLayoutModel model, FieldLocation cur, boolean forward,
String text, Set<FindOptions> options) {
super(model, cur, forward, options);
if (text.isEmpty()) {
throw new IllegalArgumentException("Empty text");
}
this.text = text;
}
@Override
protected FieldRange findInLine(int start, BigInteger index) {
int length = sb.length();
int i = Math.min(start, length - 1);
int step = forward ? 1 : -1;
while (0 <= i && i < length) {
i = forward ? sb.indexOf(text, i) : sb.lastIndexOf(text, i);
if (i == -1) {
return null;
}
if (!wholeWord || isWholeWord(i, text)) {
return new FieldRange(
new FieldLocation(index, 0, 0, i),
new FieldLocation(index, 0, 0, i + text.length()));
}
i += step;
}
return null;
}
}
/**
* A find that searches for regex patterns, case insensitive by default
*/
public static class RegexTerminalFinder extends TerminalFinder {
protected final Pattern pattern;
/**
* @see TerminalPanel#find(String, Set, FieldLocation, boolean)
*/
public RegexTerminalFinder(TerminalLayoutModel model, FieldLocation cur, boolean forward,
String pattern, Set<FindOptions> options) {
super(model, cur, forward, options);
if (pattern.isEmpty()) {
throw new IllegalArgumentException("Empty pattern");
}
this.pattern = Pattern.compile(pattern);
}
@Override
protected FieldRange findInLine(int start, BigInteger index) {
Matcher matcher = pattern.matcher(sb);
int length = sb.length();
if (length == 0) {
return null;
}
start = Math.min(length - 1, start);
if (forward) {
for (int i = start; i < length && matcher.find(i);) {
if (!wholeWord || isWholeWord(i, matcher.group())) {
return new FieldRange(
new FieldLocation(index, 0, 0, matcher.start()),
new FieldLocation(index, 0, 0, matcher.end()));
}
i = matcher.start() + 1;
}
return null;
}
int lastStart = -1;
int lastEnd = -1;
for (int i = 0; i <= start && matcher.find(i);) {
if (!wholeWord || isWholeWord(i, matcher.group())) {
if (matcher.start() > start) {
break;
}
lastStart = matcher.start();
lastEnd = matcher.end();
}
i = matcher.start() + 1;
}
if (lastStart == -1) {
return null;
}
return new FieldRange(
new FieldLocation(index, 0, 0, lastStart),
new FieldLocation(index, 0, 0, lastEnd));
}
}
}

View file

@ -0,0 +1,43 @@
/* ###
* 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.plugin.core.terminal;
import java.awt.FontMetrics;
import docking.widgets.fieldpanel.support.SingleRowLayout;
import ghidra.app.plugin.core.terminal.vt.AnsiColorResolver;
import ghidra.app.plugin.core.terminal.vt.VtLine;
/**
* A layout for a line of text in the terminal.
*
* <p>
* The layout is not terribly complicated, but we must also provide the text field and text element.
* Instead of parceling out the attributed strings into different elements, we hand the entire line
* to a single element, which can then render the text, with its various attributes, straight from
* the model's character buffer. This spares us a good deal of object creation, and allows us to
* re-use the layouts more frequently.
*/
public class TerminalLayout extends SingleRowLayout {
protected final VtLine line;
protected final TerminalTextField field;
public TerminalLayout(VtLine line, FontMetrics metrics, AnsiColorResolver colors) {
super(TerminalTextField.create(line, metrics, colors));
this.line = line;
this.field = (TerminalTextField) getField(0);
}
}

View file

@ -0,0 +1,633 @@
/* ###
* 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.plugin.core.terminal;
import java.awt.Dimension;
import java.awt.FontMetrics;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.*;
import java.util.*;
import docking.DockingWindowManager;
import docking.widgets.fieldpanel.LayoutModel;
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 ghidra.app.plugin.core.terminal.vt.*;
import ghidra.app.plugin.core.terminal.vt.VtCharset.G;
import ghidra.util.*;
/**
* The terminal layout model.
*
* <p>
* This, the buffers, and the parser, comprise the core logic of the terminal emulator. This
* implements the Ghidra layout model, as well as the handler methods of the VT100 parser. Most of
* the commands it dispatches to the current buffer. A few others modify some flags, e.g., the
* handling of mouse events. Another swaps between buffers, etc. This layout model then maps each
* line to a {@link TerminalLayout}. Unlike some other layout models, this does not create a new
* layout whenever a line is mutated. Given the frequency with which the terminal contents change,
* that would generate a decent bit of garbage. The "layout" instead dynamically computes its
* properties from the mutable line object and paints straight from its buffers.
*/
public class TerminalLayoutModel implements LayoutModel, VtHandler {
// Buffers for character decoding
protected final ByteBuffer bb = ByteBuffer.allocate(16);
protected final CharBuffer cb = CharBuffer.allocate(16);
protected final CharsetDecoder decoder;
// States for handling VT-style charsets
protected final Map<VtCharset.G, VtCharset> vtCharsets = new HashMap<>();
protected VtCharset.G curVtCharsetG = VtCharset.G.G0;
protected VtCharset curVtCharset = VtCharset.USASCII;
// A handle to the panel, so that application commands can manipulate it, e.g., titles,
// cursor enablement
protected final TerminalPanel panel;
// Rendering properties
protected FontMetrics metrics;
protected final AnsiColorResolver colors;
protected final ArrayList<LayoutModelListener> listeners = new ArrayList<>();
// Layouts and cache for the model
protected ArrayList<TerminalLayout> layouts = new ArrayList<>();
protected BigInteger numIndexes = BigInteger.ZERO;
protected final Map<VtLine, TerminalLayout> layoutCache = new LinkedHashMap<>() {
protected boolean removeEldestEntry(Map.Entry<VtLine, TerminalLayout> eldest) {
return size() >= bufPrimary.size() + bufAlternate.size();
}
};
// The parser for the actual VT/ANSI control sequences
protected VtParser parser = new VtParser(this);
// Screen buffers, primary, alternate, and current
protected final VtBuffer bufPrimary = new VtBuffer();
protected final VtBuffer bufAlternate = new VtBuffer();
protected VtBuffer buffer = bufPrimary;
// Flags for what's been enabled
protected boolean bracketedPaste;
protected boolean reportMousePress;
protected boolean reportMouseRelease;
protected boolean reportFocus;
private Object lock = new Object();
/**
* Create a model
*
* @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 colors a resolver for ANSI colors
*/
public TerminalLayoutModel(TerminalPanel panel, Charset charset, FontMetrics metrics,
AnsiColorResolver colors) {
this.panel = panel;
this.decoder = charset.newDecoder();
this.metrics = metrics;
this.colors = colors;
bufAlternate.setMaxScrollBack(0);
buildLayouts();
}
@Override
public void handleFullReset() {
bb.clear();
cb.clear();
decoder.reset();
vtCharsets.clear();
curVtCharsetG = VtCharset.G.G0;
curVtCharset = VtCharset.USASCII;
layouts.clear();
layoutCache.clear();
bufPrimary.reset();
bufAlternate.reset();
buffer = bufPrimary;
bracketedPaste = false;
reportMousePress = false;
reportMouseRelease = false;
reportFocus = false;
}
public void processInput(ByteBuffer buffer) {
synchronized (lock) {
parser.process(buffer);
// TODO: Do this less frequently?
buildLayouts();
}
Swing.runIfSwingOrRunLater(() -> {
modelChanged();
panel.placeCursor(true);
});
}
@Override
public Dimension getPreferredViewSize() {
// This assumes font is monospaced.
return new Dimension(buffer.getCols() * metrics.charWidth('M'),
buffer.getRows() * metrics.getHeight());
}
@Override
public BigInteger getNumIndexes() {
return numIndexes;
}
@Override
public TerminalLayout getLayout(BigInteger index) {
synchronized (lock) {
if (BigInteger.ZERO.compareTo(index) <= 0 && index.compareTo(numIndexes) < 0) {
return layouts.get(index.intValue());
}
}
return null;
}
@Override
public BigInteger getIndexBefore(BigInteger index) {
if (BigInteger.ZERO.compareTo(index) < 0) {
return index.subtract(BigInteger.ONE);
}
return null;
}
@Override
public BigInteger getIndexAfter(BigInteger index) {
BigInteger candidate = index.add(BigInteger.ONE);
if (candidate.compareTo(numIndexes) < 0) {
return candidate;
}
return null;
}
protected void addOrSetLayout(int i, TerminalLayout l) {
if (i < layouts.size()) {
layouts.set(i, l);
}
else {
assert i == layouts.size();
layouts.add(l);
}
}
protected TerminalLayout newLayout(VtLine line) {
return new TerminalLayout(line, metrics, colors);
}
protected void buildLayouts() {
int count = buffer.size();
numIndexes = BigInteger.valueOf(count);
buffer.forEachLine(true, (i, y, line) -> {
if (i < layouts.size()) {
TerminalLayout layout = layouts.get(i);
if (layout.line == line) {
return; // Already checked for line.clearDirty()
}
layout = layoutCache.computeIfAbsent(line, this::newLayout);
layouts.set(i, layout);
}
else {
TerminalLayout layout = layoutCache.computeIfAbsent(line, this::newLayout);
layouts.add(layout);
}
});
}
protected void modelChanged() {
for (LayoutModelListener listener : listeners) {
try {
listener.modelSizeChanged(IndexMapper.IDENTITY_MAPPER);
}
catch (Throwable e) {
Msg.showError(this, null, "Error in Listener", "Error in Listener", e);
}
}
}
@Override
public boolean isUniform() {
return true;
}
@Override
public void addLayoutModelListener(LayoutModelListener listener) {
listeners.add(listener);
}
@Override
public void removeLayoutModelListener(LayoutModelListener listener) {
listeners.remove(listener);
}
@Override
public void flushChanges() {
// Nothing to do
}
private static String dumpBuf(ByteBuffer bb) {
byte[] data = new byte[bb.remaining()];
bb.get(bb.position(), data);
return NumericUtilities.convertBytesToString(data, ":");
}
@Override
public void handleChar(byte b) throws Exception {
bb.put(b);
bb.flip();
CoderResult result = decoder.decode(bb, cb, false);
if (result.isError()) {
Msg.error(this, "Error while decoding: " + dumpBuf(bb));
decoder.reset();
bb.clear();
}
else {
bb.compact();
}
cb.flip();
while (cb.hasRemaining()) {
try {
// A little strange using both unicode and vt charsets....
buffer.putChar(curVtCharset.mapChar(cb.get()));
buffer.moveCursorRight(1);
}
catch (Throwable t) {
Msg.error(this, "Error handling character: " + t, t);
}
}
cb.clear();
}
@Override
public void handleBell() {
DockingWindowManager.beep();
}
@Override
public void handleBackSpace() {
buffer.moveCursorLeft(1);
}
@Override
public void handleTab() {
buffer.tab();
}
@Override
public void handleBackwardTab(int n) {
for (int i = 0; i < n; i++) {
buffer.tabBack();
}
}
@Override
public void handleLineFeed() {
buffer.moveCursorDown(1);
}
@Override
public void handleCarriageReturn() {
buffer.carriageReturn();
}
@Override
public void handleSetCharset(G g, VtCharset cs) {
vtCharsets.put(g, cs);
if (curVtCharsetG == g) {
curVtCharset = cs;
}
}
@Override
public void handleAltCharset(boolean alt) {
curVtCharsetG = alt ? VtCharset.G.G1 : VtCharset.G.G0;
curVtCharset = vtCharsets.getOrDefault(curVtCharsetG, VtCharset.USASCII);
}
@Override
public void handleForegroundColor(AnsiColor fg) {
buffer.setAttributes(buffer.getAttributes().fg(fg));
}
@Override
public void handleBackgroundColor(AnsiColor bg) {
buffer.setAttributes(buffer.getAttributes().bg(bg));
}
@Override
public void handleResetAttributes() {
buffer.setAttributes(VtAttributes.DEFAULTS);
}
@Override
public void handleIntensity(Intensity intensity) {
buffer.setAttributes(buffer.getAttributes().intensity(intensity));
}
@Override
public void handleFont(AnsiFont font) {
buffer.setAttributes(buffer.getAttributes().font(font));
}
@Override
public void handleUnderline(Underline underline) {
buffer.setAttributes(buffer.getAttributes().underline(underline));
}
@Override
public void handleBlink(Blink blink) {
buffer.setAttributes(buffer.getAttributes().blink(blink));
}
@Override
public void handleReverseVideo(boolean reverse) {
buffer.setAttributes(buffer.getAttributes().reverseVideo(reverse));
}
@Override
public void handleHidden(boolean hidden) {
buffer.setAttributes(buffer.getAttributes().hidden(hidden));
}
@Override
public void handleStrikeThrough(boolean strikeThrough) {
buffer.setAttributes(buffer.getAttributes().strikeThrough(strikeThrough));
}
@Override
public void handleProportionalSpacing(boolean spacing) {
buffer.setAttributes(buffer.getAttributes().proportionalSpacing(spacing));
}
@Override
public void handleInsertMode(boolean en) {
// Not seen any use this, but it'll probably need doing later.
Msg.trace(this, "TODO: handleInsertMode: " + en);
}
@Override
public void handleApplicationCursorKeys(boolean en) {
// Not sure what this means. Ignore for now.
Msg.trace(this, "TODO: handleApplicationCursorKeys: " + en);
}
@Override
public void handleApplicationKeypad(boolean en) {
// Not sure what this means. Ignore for now.
Msg.trace(this, "TODO: handleApplicationKeypad: " + en);
}
@Override
public void handleAutoWrapMode(boolean en) {
System.err.println("TODO: handleAutoWrapMode: " + en);
}
@Override
public void handleBlinkCursor(boolean blink) {
// Ignore this. FieldPanel seems to support it, but it's inconsistent.
// It's not a necessary feature, anyway.
Msg.trace(this, "TODO: handleBlinkCursor: " + blink);
}
@Override
public void handleShowCursor(boolean show) {
panel.fieldPanel.setCursorOn(show);
}
@Override
public void handleReportMouseEvents(boolean press, boolean release) {
reportMousePress = press;
reportMouseRelease = release;
}
@Override
public void handleReportFocus(boolean report) {
reportFocus = report;
}
@Override
public void handleMetaKey(boolean en) {
Msg.trace(this, "TODO: handleMetaKey: " + en); // Not sure I care
}
@Override
public void handleAltScreenBuffer(boolean alt, boolean clearAlt) {
VtBuffer newBuffer = alt ? bufAlternate : bufPrimary;
if (buffer == newBuffer) {
return;
}
if (clearAlt) {
bufAlternate.erase(Erasure.FULL_DISPLAY);
}
buffer = newBuffer;
}
@Override
public void handleBracketedPasteMode(boolean en) {
this.bracketedPaste = en;
}
@Override
public void handleSaveCursorPos() {
buffer.saveCursorPos();
}
@Override
public void handleRestoreCursorPos() {
buffer.restoreCursorPos();
}
@Override
public void handleMoveCursor(Direction direction, int n) {
switch (direction) {
case UP:
buffer.moveCursorUp(n);
return;
case DOWN:
buffer.moveCursorDown(n);
return;
case FORWARD:
buffer.moveCursorRight(n);
return;
case BACK:
buffer.moveCursorLeft(n);
return;
}
}
@Override
public void handleMoveCursor(int row, int col) {
buffer.moveCursor(row, col);
}
@Override
public void handleMoveCursorRow(int row) {
buffer.moveCursor(row, buffer.getCurX());
}
@Override
public void handleMoveCursorCol(int col) {
buffer.moveCursor(buffer.getCurY(), col);
}
@Override
public void handleReportCursorPos() {
panel.reportCursorPos(buffer.getCurY(), buffer.getCurX());
}
@Override
public void handleErase(Erasure erasure) {
buffer.erase(erasure);
}
@Override
public void handleInsertLines(int n) {
buffer.insertLines(n);
}
@Override
public void handleDeleteLines(int n) {
buffer.deleteLines(n);
}
@Override
public void handleDeleteCharacters(int n) {
buffer.deleteChars(n);
}
@Override
public void handleEraseCharacters(int n) {
buffer.eraseChars(n);
}
@Override
public void handleInsertCharacters(int n) {
buffer.insertChars(n);
}
@Override
public void handleSetScrollRange(Integer start, Integer end) {
buffer.setScrollViewport(start, end);
}
@Override
public void handleScrollViewportDown(int n, boolean intoScrollBack) {
for (int i = 0; i < n; i++) {
buffer.scrollViewportDown(intoScrollBack);
}
}
@Override
public void handleScrollViewportUp(int n) {
for (int i = 0; i < n; i++) {
buffer.scrollViewportUp();
}
}
@Override
public void handleSaveIconTitle() {
// Don't care about "Icon" title
}
@Override
public void handleRestoreIconTitle() {
// Don't care about "Icon" title
}
@Override
public void handleSaveWindowTitle() {
panel.saveTitle();
}
@Override
public void handleRestoreWindowTitle() {
panel.restoreTitle();
}
@Override
public void handleWindowTitle(String title) {
panel.setTitle(title);
}
protected boolean resizeTerminal(int cols, int rows) {
boolean affected;
synchronized (lock) {
affected = buffer.resize(cols, rows);
bufPrimary.resize(cols, rows);
bufAlternate.resize(cols, rows);
}
if (affected) {
Swing.runIfSwingOrRunLater(() -> {
modelChanged();
panel.placeCursor(true);
});
}
return affected;
}
public int getScrollBackSize() {
return buffer.getScrollBackSize();
}
public int getCursorRow() {
return buffer.getCurY();
}
public int getCursorColumn() {
return buffer.getCurX();
}
public int getCols() {
return buffer.getCols();
}
public int getRows() {
return buffer.getRows();
}
public String getSelectedText(FieldRange range) {
synchronized (lock) {
FieldLocation start = range.getStart();
int startRow = start.getIndex().intValueExact();
int startCol = start.getCol();
FieldLocation end = range.getEnd();
int endRow = end.getIndex().intValueExact();
int endCol = end.getCol();
return buffer.getText(startRow, startCol, endRow, endCol, System.lineSeparator());
}
}
public void setFontMetrics(FontMetrics metrics2) {
layouts.clear();
layoutCache.clear();
buildLayouts();
}
}

View file

@ -0,0 +1,41 @@
/* ###
* 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.plugin.core.terminal;
/**
* A listener for various events on a terminal panel
*/
public interface TerminalListener {
/**
* The terminal was resized by the user
*
* <p>
* If applicable and possible, this information should be communicated to the connection
*
* @param cols the number of columns
* @param rows the number of rows
*/
default void resized(int cols, int rows) {
}
/**
* The application requested the window title changed
*
* @param title the requested title
*/
default void retitled(String title) {
}
}

View file

@ -0,0 +1,712 @@
/* ###
* 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.plugin.core.terminal;
import java.awt.*;
import java.awt.event.*;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.*;
import java.util.List;
import javax.swing.JPanel;
import javax.swing.ScrollPaneConstants;
import docking.widgets.EventTrigger;
import docking.widgets.fieldpanel.FieldPanel;
import docking.widgets.fieldpanel.LayoutModel;
import docking.widgets.fieldpanel.field.Field;
import docking.widgets.fieldpanel.listener.*;
import docking.widgets.fieldpanel.support.*;
import docking.widgets.indexedscrollpane.IndexedScrollPane;
import generic.theme.GColor;
import generic.theme.Gui;
import ghidra.app.plugin.core.terminal.TerminalFinder.RegexTerminalFinder;
import ghidra.app.plugin.core.terminal.TerminalFinder.TextTerminalFinder;
import ghidra.app.plugin.core.terminal.vt.*;
import ghidra.app.plugin.core.terminal.vt.VtHandler.*;
import ghidra.app.services.ClipboardService;
import ghidra.util.ColorUtils;
import ghidra.util.Msg;
/**
* A VT100 terminal emulator in a panel.
*
* <p>
* This implementation uses Ghidra's {@link FieldPanel} for its rendering, highlighting, cursor
* positioning, etc. This one follows the same pattern as many other such panels in Ghidra with some
* exceptions. Namely, it removes all key listeners from the field panel to prevent any accidental
* local control of the cursor. A terminal emulator defers that entirely to the application. Key
* strokes are instead sent to the application directly, and it may respond with commands to move
* the actual cursor. This component also implements the {@link AnsiColorResolver}, as it makes the
* most sense to declare the various {@link GColor}s here.
*/
public class TerminalPanel extends JPanel implements FieldLocationListener, FieldSelectionListener,
LayoutListener, AnsiColorResolver {
protected static final int MAX_TITLE_STACK_SIZE = 20;
protected static final String DEFAULT_FONT_ID = "font.plugin.terminal";
protected static final GColor COLOR_BACKGROUND = new GColor("color.bg.plugin.terminal");
protected static final GColor COLOR_FOREGROUND = new GColor("color.fg.plugin.terminal");
protected static final GColor COLOR_CURSOR_FOCUSED =
new GColor("color.cursor.focused.terminal");
protected static final GColor COLOR_CURSOR_UNFOCUSED =
new GColor("color.cursor.unfocused.terminal");
// basic colors
protected static final GColor COLOR_0_BLACK =
new GColor("color.fg.plugin.terminal.normal.black");
protected static final GColor COLOR_1_RED =
new GColor("color.fg.plugin.terminal.normal.red");
protected static final GColor COLOR_2_GREEN =
new GColor("color.fg.plugin.terminal.normal.green");
protected static final GColor COLOR_3_YELLOW =
new GColor("color.fg.plugin.terminal.normal.yellow");
protected static final GColor COLOR_4_BLUE =
new GColor("color.fg.plugin.terminal.normal.blue");
protected static final GColor COLOR_5_MAGENTA =
new GColor("color.fg.plugin.terminal.normal.magenta");
protected static final GColor COLOR_6_CYAN =
new GColor("color.fg.plugin.terminal.normal.cyan");
protected static final GColor COLOR_7_WHITE =
new GColor("color.fg.plugin.terminal.normal.white");
protected static final GColor COLOR_0_BRIGHT_BLACK =
new GColor("color.fg.plugin.terminal.bright.black");
protected static final GColor COLOR_1_BRIGHT_RED =
new GColor("color.fg.plugin.terminal.bright.red");
protected static final GColor COLOR_2_BRIGHT_GREEN =
new GColor("color.fg.plugin.terminal.bright.green");
protected static final GColor COLOR_3_BRIGHT_YELLOW =
new GColor("color.fg.plugin.terminal.bright.yellow");
protected static final GColor COLOR_4_BRIGHT_BLUE =
new GColor("color.fg.plugin.terminal.bright.blue");
protected static final GColor COLOR_5_BRIGHT_MAGENTA =
new GColor("color.fg.plugin.terminal.bright.magenta");
protected static final GColor COLOR_6_BRIGHT_CYAN =
new GColor("color.fg.plugin.terminal.bright.cyan");
protected static final GColor COLOR_7_BRIGHT_WHITE =
new GColor("color.fg.plugin.terminal.bright.white");
protected static final int[] CUBE_STEPS = {
0, 95, 135, 175, 215, 255
};
protected class TerminalFieldPanel extends FieldPanel {
public TerminalFieldPanel(LayoutModel model) {
super(model, "Terminal");
setFieldDescriptionProvider((l, f) -> {
if (f == null) {
return null;
}
// TODO: Adjust, because lines in the history should not be counted
return "line " + (l.getIndex().intValue() + 1) + ": " + f.getText();
});
paintContext.setFocusedCursorColor(COLOR_CURSOR_FOCUSED);
paintContext.setNotFocusedCursorColor(COLOR_CURSOR_UNFOCUSED);
paintContext.setCursorFocused(true);
}
@Override
public void modelSizeChanged(IndexMapper indexMapper) {
// Avoid centering on cursor
setCursorOn(false);
super.modelSizeChanged(indexMapper);
setCursorOn(true);
}
}
protected FontMetrics metrics;
protected final TerminalLayoutModel model;
protected final TerminalFieldPanel fieldPanel;
protected final IndexedScrollPane scroller;
protected boolean fixedSize = false;
protected String title;
protected final Deque<String> titleStack = new LinkedList<>();
protected final TerminalProvider provider;
protected ClipboardService clipboardService;
protected TerminalClipboardProvider clipboardProvider;
protected String selectedText;
protected final ArrayList<TerminalListener> terminalListeners = new ArrayList<>();
protected VtOutput outputCb;
protected final TerminalAwtEventEncoder eventEncoder;
protected final VtResponseEncoder responseEncoder;
protected TerminalPanel(Charset charset, TerminalProvider provider) {
this.provider = provider;
clipboardProvider = new TerminalClipboardProvider(provider);
Gui.registerFont(this, DEFAULT_FONT_ID);
this.metrics = getFontMetrics(getFont());
this.model = new TerminalLayoutModel(this, charset, metrics, this);
this.fieldPanel = new TerminalFieldPanel(model);
fieldPanel.addFieldSelectionListener(this);
fieldPanel.addFieldLocationListener(this);
fieldPanel.addLayoutListener(this);
setBackground(COLOR_BACKGROUND);
// Have to set background before creating scroller;
fieldPanel.setBackgroundColor(COLOR_BACKGROUND);
scroller = new IndexedScrollPane(fieldPanel);
scroller.setBackground(COLOR_BACKGROUND);
scroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
scroller.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
scroller.addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
if (fixedSize) {
return;
}
resizeTerminalToWindow();
}
});
setPreferredSize(new Dimension(600, 400));
setLayout(new BorderLayout());
add(scroller);
eventEncoder = new TerminalAwtEventEncoder(charset) {
@Override
public void generateBytes(ByteBuffer buf) {
if (outputCb != null) {
outputCb.out(buf);
}
}
};
responseEncoder = new VtResponseEncoder(charset) {
@Override
protected void generateBytes(ByteBuffer buf) {
if (outputCb != null) {
outputCb.out(buf);
}
}
};
for (KeyListener r : fieldPanel.getKeyListeners()) {
fieldPanel.removeKeyListener(r);
}
fieldPanel.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (provider.isLocalActionKeyBinding(e)) {
return; // Do not consume, so action can take it
}
eventEncoder.keyPressed(e);
e.consume();
}
@Override
public void keyTyped(KeyEvent e) {
eventEncoder.keyTyped(e);
e.consume();
}
});
fieldPanel.addMouseListener(new MouseListener() {
@Override
public void mousePressed(MouseEvent e) {
/**
* NOTE: According to gdb's docs, it's common for terminals to use SHIFT to override
* application mouse tracking:
*
* https://sourceware.org/gdb/onlinedocs/gdb/TUI-Mouse-Support.html
*/
if (model.reportMousePress && !e.isShiftDown()) {
FieldLocation location = fieldPanel.getLocationForPoint(e.getX(), e.getY());
eventEncoder.mousePressed(e, location.getIndex().intValueExact(),
location.getCol());
e.consume();
}
}
@Override
public void mouseReleased(MouseEvent e) {
if (model.reportMousePress && !e.isShiftDown()) {
FieldLocation location = fieldPanel.getLocationForPoint(e.getX(), e.getY());
eventEncoder.mouseReleased(e, location.getIndex().intValueExact(),
location.getCol());
e.consume();
}
}
@Override
public void mouseClicked(MouseEvent e) {
FieldLocation location = fieldPanel.getLocationForPoint(e.getX(), e.getY());
if (model.reportMousePress && !e.isShiftDown()) {
e.consume();
return;
}
else if (e.getClickCount() == 2 && e.getButton() == 1) {
selectWordAt(location, EventTrigger.GUI_ACTION);
e.consume();
}
else if (e.getButton() == 2) {
String text = getSelectedText();
if (text == null) {
return;
}
paste(text);
}
}
@Override
public void mouseEntered(MouseEvent e) {
}
@Override
public void mouseExited(MouseEvent e) {
}
});
fieldPanel.addMouseMotionListener(new MouseMotionListener() {
@Override
public void mouseDragged(MouseEvent e) {
if (model.reportMousePress && !e.isShiftDown()) {
// TODO: This is not stopping the field selection
e.consume();
return;
}
}
@Override
public void mouseMoved(MouseEvent e) {
}
});
fieldPanel.addMouseWheelListener(new MouseWheelListener() {
@Override
public void mouseWheelMoved(MouseWheelEvent e) {
FieldLocation location = fieldPanel.getLocationForPoint(e.getX(), e.getY());
if (model.reportMousePress && !e.isShiftDown()) {
eventEncoder.mouseWheelMoved(e, location.getIndex().intValueExact(),
location.getCol());
e.consume();
}
}
});
fieldPanel.addFocusListener(new FocusAdapter() {
@Override
public void focusGained(FocusEvent e) {
if (model.reportFocus) {
eventEncoder.focusGained();
}
}
@Override
public void focusLost(FocusEvent e) {
if (model.reportFocus) {
eventEncoder.focusLost();
}
}
});
}
public void addTerminalListener(TerminalListener listener) {
terminalListeners.add(listener);
}
public void removeTerminalListener(TerminalListener listener) {
terminalListeners.remove(listener);
}
protected void notifyTerminalResized(int cols, int rows) {
for (TerminalListener l : terminalListeners) {
try {
l.resized(cols, rows);
}
catch (Throwable t) {
Msg.showError(this, null, "Error", t.getMessage(), t);
}
}
}
protected void notifyTerminalRetitled(String title) {
for (TerminalListener l : terminalListeners) {
try {
l.retitled(title);
}
catch (Throwable t) {
Msg.showError(this, null, "Error", t.getMessage(), t);
}
}
}
@Override
public void setFont(Font font) {
super.setFont(font);
this.metrics = getFontMetrics(font);
if (model != null) {
model.setFontMetrics(this.metrics);
}
}
public TerminalFieldPanel getFieldPanel() {
return fieldPanel;
}
@Override
public void layoutsChanged(List<AnchoredLayout> layouts) {
/**
* Don't just blow away the selection every key stroke; however, don't allow terminal
* changes to modify the selected text without the user knowing. That rule is directly
* implemented here. If the selected text changes, destroy the selection.
*/
if (!Objects.equals(selectedText, getSelectedText())) {
fieldPanel.clearSelection();
}
}
@Override
public void selectionChanged(FieldSelection selection, EventTrigger trigger) {
selectedText = getSelectedText();
clipboardProvider.selectionChanged(selection);
}
@Override
public void fieldLocationChanged(FieldLocation location, Field field, EventTrigger trigger) {
/**
* Prevent the user from doing this. Cursor location is controlled by pty. While we've
* prevented key strokes from causing this, we've not prevented mouse clicks from doing it.
* Next best thing is to just move it back.
*/
if (trigger == EventTrigger.GUI_ACTION) {
placeCursor(false);
}
}
/**
* Select the whole word at the given location.
*
* <p>
* This is used for double-click to select the whole word.
*
* @param location the cursor's location
* @param trigger the cause of the selection
*/
public void selectWordAt(FieldLocation location, EventTrigger trigger) {
BigInteger index = location.getIndex();
TerminalLayout layout = model.getLayout(index);
if (layout == null) {
return;
}
int start = Math.min(location.col, layout.line.findWord(location.col, false));
int end = Math.max(location.col + 1, layout.line.findWord(location.col, true));
FieldSelection sel = new FieldSelection();
sel.addRange(new FieldLocation(index, 0, 0, start), new FieldLocation(index, 0, 0, end));
fieldPanel.setSelection(sel, trigger);
}
/**
* Process the given bytes as application output.
*
* <p>
* In most circumstances, there is a thread that just reads an output stream, usually from a
* pty, and feeds it into this method.
*
* @param buffer the buffer
*/
public void processInput(ByteBuffer buffer) {
model.processInput(buffer);
}
protected Color resolveDefaultColor(WhichGround ground, boolean reverseVideo) {
if (ground == WhichGround.BACKGROUND) {
if (reverseVideo) {
return COLOR_FOREGROUND;
}
return null; // background is already drawn
}
if (reverseVideo) {
return COLOR_BACKGROUND;
}
return COLOR_FOREGROUND;
}
protected Color resolveStandardColor(AnsiStandardColor standard) {
return switch (standard) {
case BLACK -> COLOR_0_BLACK;
case RED -> COLOR_1_RED;
case GREEN -> COLOR_2_GREEN;
case YELLOW -> COLOR_3_YELLOW;
case BLUE -> COLOR_4_BLUE;
case MAGENTA -> COLOR_5_MAGENTA;
case CYAN -> COLOR_6_CYAN;
case WHITE -> COLOR_7_WHITE;
};
}
protected Color resolveIntenseColor(AnsiIntenseColor intense) {
return switch (intense) {
case BLACK -> COLOR_0_BRIGHT_BLACK;
case RED -> COLOR_1_BRIGHT_RED;
case GREEN -> COLOR_2_BRIGHT_GREEN;
case YELLOW -> COLOR_3_BRIGHT_YELLOW;
case BLUE -> COLOR_4_BRIGHT_BLUE;
case MAGENTA -> COLOR_5_BRIGHT_MAGENTA;
case CYAN -> COLOR_6_BRIGHT_CYAN;
case WHITE -> COLOR_7_BRIGHT_WHITE;
};
}
protected Color resolve216Color(Ansi216Color cube) {
return ColorUtils.getColor(CUBE_STEPS[cube.r()], CUBE_STEPS[cube.g()],
CUBE_STEPS[cube.b()]);
}
protected Color resolveGrayscaleColor(AnsiGrayscaleColor gray) {
return ColorUtils.getColor(gray.v() * 10 + 8);
}
protected Color resolve24BitColor(Ansi24BitColor rgb) {
return ColorUtils.getColor(rgb.r(), rgb.g(), rgb.b());
}
@Override
public Color resolveColor(AnsiColor color, WhichGround ground, Intensity intensity,
boolean reverseVideo) {
if (color == AnsiDefaultColor.INSTANCE) {
return resolveDefaultColor(ground, reverseVideo);
}
if (color instanceof AnsiStandardColor standard) {
return resolveStandardColor(standard);
}
if (color instanceof AnsiIntenseColor intense) {
return resolveIntenseColor(intense);
}
if (color instanceof Ansi216Color cube) {
return resolve216Color(cube);
}
if (color instanceof AnsiGrayscaleColor gray) {
return resolveGrayscaleColor(gray);
}
if (color instanceof Ansi24BitColor rgb) {
return resolve24BitColor(rgb);
}
throw new AssertionError();
}
public void setClipboardService(ClipboardService clipboardService) {
if (this.clipboardService == clipboardService) {
return;
}
if (this.clipboardService != null) {
this.clipboardService.deRegisterClipboardContentProvider(clipboardProvider);
}
this.clipboardService = clipboardService;
if (this.clipboardService != null) {
this.clipboardService.registerClipboardContentProvider(clipboardProvider);
}
}
/**
* Set the callback for application input, i.e., terminal output
*
* <p>
* In most circumstances, the bytes are sent to an input stream, usually from a pty.
*
* @param outputCb the callback
*/
public void setOutputCallback(VtOutput outputCb) {
this.outputCb = outputCb;
}
protected void placeCursor(boolean scroll) {
int scrollBack = model.getScrollBackSize();
fieldPanel.setCursorPosition(BigInteger.valueOf(model.getCursorRow() + scrollBack), 0, 0,
model.getCursorColumn());
if (scroll) {
fieldPanel.scrollToCursor();
}
}
protected void saveTitle() {
titleStack.push(title);
if (titleStack.size() > MAX_TITLE_STACK_SIZE) {
titleStack.pollLast();
}
}
protected void restoreTitle() {
notifyTerminalRetitled(title = titleStack.poll());
}
protected void setTitle(String title) {
notifyTerminalRetitled(this.title = title);
}
/**
* Send the cursor's position to the application
*
* @param row the cursor's row
* @param col the cursor's column
*/
public void reportCursorPos(int row, int col) {
responseEncoder.reportCursorPos(row, col);
}
public void dispose() {
if (this.clipboardService != null) {
clipboardService.deRegisterClipboardContentProvider(clipboardProvider);
}
}
/**
* Send the given text to the application, as if typed on the keyboard
*
* <p>
* Note the application may request a mode called "bracketed paste," in which case the text will
* be surrounded by special control sequences, allowing the application to distinguish pastes
* from manual typing. An application may do this so that an Undo could undo the whole paste,
* and not just the last keystroke simulated by the paste.
*
* @param text the text
*/
public void paste(String text) {
if (model.bracketedPaste) {
responseEncoder.reportPasteStart();
}
try {
eventEncoder.sendText(text);
}
finally {
if (model.bracketedPaste) {
responseEncoder.reportPasteEnd();
}
}
}
/**
* Get the text selected by the user
*
* <p>
* If the selection is disjoint, this returns null.
*
* @return the selected text, or null
*/
public String getSelectedText() {
FieldSelection sel = fieldPanel.getSelection();
if (sel == null || sel.getNumRanges() != 1) {
return null;
}
return getSelectedText(sel.getFieldRange(0));
}
/**
* Get the text covered by the given range
*
* @param range the range
* @return the text
*/
public String getSelectedText(FieldRange range) {
return model.getSelectedText(range);
}
/**
* Enumerated options available when searching the terminal's buffer
*/
public enum FindOptions {
/**
* Make the search case sensitive. If this flag is absent, the search defaults to case
* insensitive.
*/
CASE_SENSITIVE,
/**
* Allow the search to wrap.
*/
WRAP,
/**
* Require the result to be a whole word.
*/
WHOLE_WORD,
/**
* Treat the search term as a regular expression instead of literal text.
*/
REGEX
}
/**
* Search the terminal's buffer for the given text.
*
* <p>
* The start location should be given, so that the search can progress to each successive
* result. If no location is given, e.g., because this is the first time the user has searched,
* then a default location will be chosen based on the search direction: the start for forward
* or the end for backward.
*
* @param text the text (or pattern for {@link FindOptions#REGEX})
* @param options the search options
* @param start the starting location, or null for a default
* @param forward true to search forward, false to search backward
* @return the range covering the found term, or null if not found
*/
public FieldRange find(String text, Set<FindOptions> options, FieldLocation start,
boolean forward) {
TerminalFinder finder = options.contains(FindOptions.REGEX)
? new RegexTerminalFinder(model, start, forward, text, options)
: new TextTerminalFinder(model, start, forward, text, options);
return finder.find();
}
protected void resizeTerminalToWindow() {
Rectangle bounds = scroller.getViewportBorderBounds();
int rows = bounds.height / metrics.getHeight();
int cols = bounds.width / metrics.charWidth('M');
resizeTerminal(rows, cols);
}
protected void resizeTerminal(int rows, int cols) {
if (model.resizeTerminal(cols, rows)) {
notifyTerminalResized(model.getCols(), model.getRows());
}
}
/**
* Set the terminal to a fixed size.
*
* <p>
* The terminal will no longer respond to the window resizing, and scrollbars are displayed as
* needed. If the terminal size changes as a result of this call,
* {@link TerminalListener#resized(int, int)} is invoked.
*
* @param rows the number of rows
* @param cols the number of columns
*/
public void setFixedTerminalSize(int rows, int cols) {
this.fixedSize = true;
scroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
scroller.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
resizeTerminal(rows, cols);
}
/**
* Set the terminal to fit the window size.
*
* <p>
* Immediately fit the terminal to the window. It will also respond to the window resizing by
* recalculating the rows and columns and adjusting the buffer's contents to fit. Whenever the
* terminal size changes {@link TerminalListener#resized(int, int)} is invoked. The bottom
* scrollbar is disabled, and the vertical scrollbar is always displayed, to avoid frenetic
* horizontal resizing.
*/
public void setDynamicTerminalSize() {
this.fixedSize = false;
scroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
scroller.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
resizeTerminalToWindow();
}
}

View file

@ -0,0 +1,102 @@
/* ###
* 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.plugin.core.terminal;
import java.io.*;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import ghidra.app.CorePluginPackage;
import ghidra.app.plugin.PluginCategoryNames;
import ghidra.app.plugin.core.terminal.vt.VtOutput;
import ghidra.app.services.*;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.util.Msg;
/**
* The plugin that provides {@link TerminalService}
*/
@PluginInfo(
status = PluginStatus.UNSTABLE,
category = PluginCategoryNames.COMMON,
packageName = CorePluginPackage.NAME,
description = "Provides VT100 Terminal Emulation",
shortDescription = "VT100 Emulator",
servicesProvided = { TerminalService.class })
public class TerminalPlugin extends Plugin implements TerminalService {
protected ClipboardService clipboardService;
protected List<TerminalProvider> providers = new ArrayList<>();
public TerminalPlugin(PluginTool tool) {
super(tool);
clipboardService = tool.getService(ClipboardService.class);
}
public TerminalProvider createProvider(Charset charset, VtOutput outputCb) {
TerminalProvider provider = new TerminalProvider(this, charset);
provider.setOutputCallback(outputCb);
provider.addToTool();
provider.setVisible(true);
providers.add(provider);
provider.setClipboardService(clipboardService);
return provider;
}
@Override
public Terminal createNullTerminal(Charset charset, VtOutput outputCb) {
return new DefaultTerminal(createProvider(charset, outputCb));
}
@Override
public Terminal createWithStreams(Charset charset, InputStream in, OutputStream out) {
WritableByteChannel channel = Channels.newChannel(out);
return new ThreadedTerminal(createProvider(charset, buf -> {
while (buf.hasRemaining()) {
try {
channel.write(buf);
}
catch (IOException e) {
Msg.error(this, "Could not write terminal output", e);
}
}
}), in);
}
@Override
public void serviceAdded(Class<?> interfaceClass, Object service) {
if (interfaceClass == ClipboardService.class) {
clipboardService = (ClipboardService) service;
for (TerminalProvider p : providers) {
p.setClipboardService(clipboardService);
}
}
}
@Override
public void serviceRemoved(Class<?> interfaceClass, Object service) {
if (interfaceClass == ClipboardService.class) {
for (TerminalProvider p : providers) {
p.setClipboardService(null);
}
}
}
}

View file

@ -0,0 +1,309 @@
/* ###
* 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.plugin.core.terminal;
import java.awt.*;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.*;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import org.apache.commons.collections4.IteratorUtils;
import docking.*;
import docking.action.DockingAction;
import docking.action.DockingActionIf;
import docking.action.builder.ActionBuilder;
import docking.widgets.OkDialog;
import docking.widgets.fieldpanel.support.*;
import generic.theme.GIcon;
import ghidra.app.plugin.core.terminal.TerminalPanel.FindOptions;
import ghidra.app.plugin.core.terminal.vt.VtOutput;
import ghidra.app.services.ClipboardService;
import ghidra.framework.plugintool.ComponentProviderAdapter;
/**
* A window holding a VT100 terminal emulator.
*
* <p>
* This also provides UI actions for searching the terminal's contents.
*/
public class TerminalProvider extends ComponentProviderAdapter {
protected class FindDialog extends DialogComponentProvider {
protected final JTextField txtFind = new JTextField(20);
protected final JCheckBox cbCaseSensitive = new JCheckBox("Case sensitive");
protected final JCheckBox cbWrapSearch = new JCheckBox("Wrap search");
protected final JCheckBox cbWholeWord = new JCheckBox("Whole word");
protected final JCheckBox cbRegex = new JCheckBox("Regular expression");
protected final JButton btnFindNext = new JButton("Next");
protected final JButton btnFindPrevious = new JButton("Previous");
protected FindDialog() {
super("Find", false, false, true, false);
populateComponents();
}
protected GridBagConstraints cell(int row, int col, int width, boolean hFill) {
GridBagConstraints constraints = new GridBagConstraints();
constraints.gridx = col;
constraints.gridy = row;
constraints.gridwidth = width;
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.insets = new Insets(row == 0 ? 0 : 5, col == 0 ? 0 : 3, 0, 0);
constraints.weightx = hFill ? 1.0 : 0.0;
return constraints;
}
protected JLabel label(String text) {
JLabel label = new JLabel(text);
label.setHorizontalAlignment(SwingConstants.RIGHT);
return label;
}
protected void populateComponents() {
JPanel panel = new JPanel(new GridBagLayout());
panel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
panel.add(label("Find"), cell(0, 0, 1, false));
panel.add(txtFind, cell(0, 1, 1, true));
panel.add(cbCaseSensitive, cell(2, 0, 2, true));
panel.add(cbWrapSearch, cell(3, 0, 2, true));
panel.add(cbWholeWord, cell(4, 0, 2, true));
panel.add(cbRegex, cell(5, 0, 2, true));
addWorkPanel(panel);
addButton(btnFindNext);
addButton(btnFindPrevious);
addDismissButton();
setDefaultButton(btnFindNext);
txtFind.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
contextChanged();
btnFindNext.setEnabled(isEnabledFindStep(null));
btnFindPrevious.setEnabled(isEnabledFindStep(null));
}
@Override
public void removeUpdate(DocumentEvent e) {
contextChanged();
btnFindNext.setEnabled(isEnabledFindStep(null));
btnFindPrevious.setEnabled(isEnabledFindStep(null));
}
@Override
public void changedUpdate(DocumentEvent e) {
contextChanged();
btnFindNext.setEnabled(isEnabledFindStep(null));
btnFindPrevious.setEnabled(isEnabledFindStep(null));
}
});
btnFindNext.addActionListener(evt -> {
activatedFindNext(null);
});
btnFindPrevious.addActionListener(evt -> {
activatedFindPrevious(null);
});
}
public Set<FindOptions> getOptions() {
EnumSet<FindOptions> opts = EnumSet.noneOf(FindOptions.class);
if (cbCaseSensitive.isSelected()) {
opts.add(FindOptions.CASE_SENSITIVE);
}
if (cbWrapSearch.isSelected()) {
opts.add(FindOptions.WRAP);
}
if (cbWholeWord.isSelected()) {
opts.add(FindOptions.WHOLE_WORD);
}
if (cbRegex.isSelected()) {
opts.add(FindOptions.REGEX);
}
return opts;
}
}
protected final TerminalPlugin plugin;
protected final TerminalPanel panel;
protected final FindDialog findDialog = new FindDialog();
protected DockingAction actionFind;
protected DockingAction actionFindNext;
protected DockingAction actionFindPrevious;
public TerminalProvider(TerminalPlugin plugin, Charset charset) {
super(plugin.getTool(), "Terminal", plugin.getName());
this.plugin = plugin;
this.panel = new TerminalPanel(charset, this);
this.panel.addTerminalListener(new TerminalListener() {
@Override
public void retitled(String title) {
setSubTitle(title);
}
});
createActions();
}
@Override
public JComponent getComponent() {
return panel;
}
public void processInput(ByteBuffer buffer) {
panel.processInput(buffer);
}
public TerminalPanel getTerminalPanel() {
return panel;
}
@Override
public void removeFromTool() {
panel.dispose();
plugin.providers.remove(this);
super.removeFromTool();
}
public void setOutputCallback(VtOutput outputCb) {
panel.setOutputCallback(outputCb);
}
public void addTerminalListener(TerminalListener listener) {
panel.addTerminalListener(listener);
}
public void removeTerminalListener(TerminalListener listener) {
panel.removeTerminalListener(listener);
}
public void setClipboardService(ClipboardService clipboardService) {
panel.setClipboardService(clipboardService);
}
protected void createActions() {
actionFind = new ActionBuilder("Find", plugin.getName())
.menuIcon(new GIcon("icon.search"))
.menuPath(new String[] { "Find" })
.keyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_F,
InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK))
.onAction(this::activatedFind)
.buildAndInstallLocal(this);
actionFindNext = new ActionBuilder("Find Next", plugin.getName())
.menuPath(new String[] { "Find Next" })
.keyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_H,
InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK))
.enabledWhen(this::isEnabledFindStep)
.onAction(this::activatedFindNext)
.buildAndInstallLocal(this);
actionFindPrevious = new ActionBuilder("Find Previous", plugin.getName())
.menuPath(new String[] { "Find Previous" })
.keyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_G,
InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK))
.enabledWhen(this::isEnabledFindStep)
.onAction(this::activatedFindPrevious)
.buildAndInstallLocal(this);
}
protected void activatedFind(ActionContext ctx) {
tool.showDialog(findDialog);
}
protected void doFind(boolean forward) {
FieldSelection sel = panel.getFieldPanel().getSelection();
final FieldLocation start;
if (sel == null || sel.getNumRanges() == 0) {
start = null;
}
else {
FieldLocation s = sel.getFieldRange(0).getStart();
if (forward) {
start = new FieldLocation(s.getIndex(), 0, 0, s.getCol() + 1);
}
else {
/**
* The search algorithm should work such that col == -1 works the same as the end of
* the previous line -- or no result if its the first line.
*/
start = new FieldLocation(s.getIndex(), 0, 0, s.getCol() - 1);
}
}
FieldRange found =
panel.find(findDialog.txtFind.getText(), findDialog.getOptions(), start, forward);
if (found == null) {
OkDialog.showInfo("Find", "String not found");
return;
}
FieldSelection newSel = new FieldSelection();
newSel.addRange(found);
panel.fieldPanel.setSelection(newSel);
panel.fieldPanel.scrollTo(found.getStart());
}
protected boolean isEnabledFindStep(ActionContext ctx) {
return !findDialog.txtFind.getText().isEmpty();
}
protected void activatedFindNext(ActionContext ctx) {
doFind(true);
}
protected void activatedFindPrevious(ActionContext ctx) {
doFind(false);
}
/**
* Check if the given keystroke would activate a local action.
*
* <p>
* Because we usurp control of the keyboard, but we still want local actions accessible via
* keyboard shortcuts, we need a way to check if a local action could take the stroke. In this
* way, we allow local actions to override the terminal, but not tool/global actions.
*
* @param e the event
* @return true if a local action could be activated
*/
protected boolean isLocalActionKeyBinding(KeyEvent e) {
KeyStroke stroke = KeyStroke.getKeyStrokeForEvent(e);
DockingWindowManager wm = DockingWindowManager.getActiveInstance();
for (DockingActionIf action : IteratorUtils.asIterable(wm.getComponentActions(this))) {
if (Objects.equals(stroke, action.getKeyBinding())) {
return true;
}
}
return false;
}
public void setFixedSize(int rows, int cols) {
panel.setFixedTerminalSize(rows, cols);
}
public void setDyanmicSize() {
panel.setDynamicTerminalSize();
}
}

View file

@ -0,0 +1,286 @@
/* ###
* 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.plugin.core.terminal;
import java.awt.*;
import java.util.List;
import javax.swing.JComponent;
import docking.widgets.fieldpanel.field.FieldElement;
import docking.widgets.fieldpanel.field.TextField;
import docking.widgets.fieldpanel.internal.FieldBackgroundColorManager;
import docking.widgets.fieldpanel.internal.PaintContext;
import docking.widgets.fieldpanel.support.*;
import ghidra.app.plugin.core.terminal.vt.*;
/**
* A text field (renderer) for the terminal panel.
*
* <p>
* The purpose of this thing is to hold a single text field element. It is also responsible for
* rendering selections and the cursor. Because the cursor is also supposed to be controlled by the
* application, we do less "validation" and correction of it on our end. If it's past the end of a
* line, so be it.
*/
public class TerminalTextField implements TextField {
protected final int startX;
protected final TerminalTextFieldElement element;
protected final int em;
protected boolean isPrimary;
/**
* Create a text field for the given line.
*
* <p>
* This method will create the sole text field element populating this field.
*
* @param line the line from the {@link VtBuffer} that will be rendered in this field
* @param metrics the font metrics
* @param colors the color resolver
* @return the field
*/
public static TerminalTextField create(VtLine line, FontMetrics metrics,
AnsiColorResolver colors) {
return new TerminalTextField(0, new TerminalTextFieldElement(line, metrics, colors),
metrics);
}
protected TerminalTextField(int startX, TerminalTextFieldElement element, FontMetrics metrics) {
this.startX = startX;
this.element = element;
this.em = metrics.charWidth('M');
}
@Override
public void paint(JComponent c, Graphics g, PaintContext context, Rectangle clip,
FieldBackgroundColorManager colorManager, RowColLocation cursorLoc, int rowHeight) {
if (context.isPrinting()) {
print(g, context);
}
else {
paintSelection(g, colorManager, 0, rowHeight);
paintText(c, g, context);
paintCursor(g, context.getCursorColor(), cursorLoc);
}
}
protected void print(Graphics g, PaintContext context) {
element.paint(null, g, startX, 0);
}
protected void paintText(JComponent c, Graphics g, PaintContext context) {
element.paint(c, g, startX, 0);
}
protected void paintSelection(Graphics g, FieldBackgroundColorManager colorManager, int row,
int rowHeight) {
List<Highlight> selections = colorManager.getSelectionHighlights(row);
if (selections.isEmpty()) {
return;
}
int textLength = element.length();
int endTextPos = findX(textLength);
for (Highlight highlight : selections) {
g.setColor(highlight.getColor());
int startCol = highlight.getStart();
int endCol = highlight.getEnd();
int x1 = findX(startCol);
int x2 = endCol < element.length() ? findX(endCol) : endTextPos;
g.fillRect(startX + x1, -getHeightAbove(), x2 - x1, getHeight());
}
// Padding?
}
/**
* Paint a big cursor, so people can actually see it. Also, don't check column number. The
* cursor is frequently past the end of the text, e.g., after pressing space in vim.
*/
protected void paintCursor(Graphics g, Color cursorColor, RowColLocation cursorLoc) {
if (cursorLoc != null) {
g.setColor(cursorColor);
int x = startX + findX(cursorLoc.col());
g.drawRect(x, -getHeightAbove(), em - 1, getHeight() - 1);
g.drawRect(x + 1, -getHeightAbove() + 1, em - 3, getHeight() - 3);
}
}
protected int findX(int col) {
return em * col;
}
@Override
public int getWidth() {
return element.getStringWidth();
}
@Override
public int getPreferredWidth() {
return element.getStringWidth();
}
@Override
public int getHeight() {
return element.getHeightAbove() + element.getHeightBelow();
}
@Override
public int getHeightAbove() {
return element.getHeightAbove();
}
@Override
public int getHeightBelow() {
return element.getHeightBelow();
}
@Override
public int getStartX() {
return startX;
}
@Override
public boolean contains(int x, int y) {
return (x >= startX) && (x < startX + getWidth()) && (y >= -element.getHeightAbove()) &&
(y < element.getHeightBelow());
}
@Override
public int getNumDataRows() {
return 1;
}
@Override
public int getNumRows() {
return 1;
}
@Override
public int getNumCols(int row) {
return element.getNumCols();
}
@Override
public int getX(int row, int col) {
return startX + findX(col);
}
@Override
public int getY(int row) {
return -getHeightAbove();
}
@Override
public int getRow(int y) {
return 0;
}
@Override
public int getCol(int row, int x) {
int relX = Math.max(0, x - startX);
return element.getMaxCharactersForWidth(relX);
}
@Override
public boolean isValid(int row, int col) {
return row == 0 && 0 <= col && col < getNumCols(0);
}
@Override
public Rectangle getCursorBounds(int row, int col) {
if (row != 0) {
return null;
}
int x = findX(col) + startX;
return new Rectangle(x, -getHeightAbove(), em, getHeight());
}
@Override
public int getScrollableUnitIncrement(int topOfScreen, int direction, int max) {
if ((topOfScreen < -getHeightAbove()) || (topOfScreen > getHeightBelow())) {
return max;
}
if (direction > 0) { // if scrolling down
return getHeightBelow() - topOfScreen;
}
return -getHeightAbove() - topOfScreen;
}
@Override
public void setPrimary(boolean isPrimary) {
this.isPrimary = isPrimary;
}
@Override
public boolean isPrimary() {
return isPrimary;
}
@Override
public void rowHeightChanged(int heightAbove, int heightBelow) {
// Don't care
}
@Override
public String getText() {
return element.getText();
}
@Override
public String getTextWithLineSeparators() {
return element.getText();
}
@Override
public RowColLocation textOffsetToScreenLocation(int textOffset) {
// allow the max position to be just after the last character
return new RowColLocation(0, Math.min(textOffset, element.length()));
}
@Override
public int screenLocationToTextOffset(int row, int col) {
return Math.min(element.length(), col);
}
@Override
public RowColLocation screenToDataLocation(int screenRow, int screenColumn) {
return element.getDataLocationForCharacterIndex(screenColumn);
}
@Override
public RowColLocation dataToScreenLocation(int dataRow, int dataColumn) {
int column = element.getCharacterIndexForDataLocation(dataRow, dataColumn);
if (column < 0) {
return new DefaultRowColLocation(0, element.length());
}
return new RowColLocation(0, column);
}
@Override
public boolean isClipped() {
return false;
}
@Override
public FieldElement getFieldElement(int screenRow, int screenColumn) {
return element.getFieldElement(screenColumn);
}
}

View file

@ -0,0 +1,229 @@
/* ###
* 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.plugin.core.terminal;
import java.awt.*;
import java.awt.geom.AffineTransform;
import javax.swing.JComponent;
import docking.widgets.fieldpanel.field.FieldElement;
import docking.widgets.fieldpanel.support.RowColLocation;
import ghidra.app.plugin.core.terminal.vt.*;
import ghidra.app.plugin.core.terminal.vt.VtHandler.Intensity;
/**
* A text field element for rendering a full line of terminal text
*
* <p>
* {@link TerminalTextFields} 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
* element that renders the {@link VtLine} directly, including the variety of attributes. When the
* line changes, we merely have to re-paint.
*/
public class TerminalTextFieldElement implements FieldElement {
public static final int UNDERLINE_HEIGHT = 1;
protected final VtLine line;
protected final FontMetrics metrics;
protected final AnsiColorResolver colors;
protected final int em;
/**
* Create a text field element
*
* @param line the line of text from the {@link VtBuffer}
* @param metrics the font metrics
* @param colors the color resolver
*/
public TerminalTextFieldElement(VtLine line, FontMetrics metrics, AnsiColorResolver colors) {
this.line = line;
this.metrics = metrics;
this.colors = colors;
this.em = metrics.charWidth('M');
}
@Override
public String getText() {
StringBuilder sb = new StringBuilder();
line.gatherText(sb, 0, line.length());
return sb.toString();
}
@Override
public int length() {
return line.length();
}
/**
* Get the number of columns (total width, not just the used by the line)
*
* @return the column count
*/
public int getNumCols() {
return line.cols();
}
@Override
public int getStringWidth() {
// Assumes monospaced.
return em * length();
}
@Override
public int getHeightAbove() {
return metrics.getMaxAscent() + metrics.getLeading();
}
@Override
public int getHeightBelow() {
return metrics.getMaxDescent();
}
@Override
public char charAt(int index) {
return line.getChar(index);
}
@Override
public Color getColor(int charIndex) {
return line.getCellAttrs(charIndex).resolveForeground(colors);
}
@Override
public FieldElement substring(int start) {
return this; // Used for clipping and wrapping. I don't care.
}
@Override
public FieldElement substring(int start, int end) {
return this; // Used for clipping and wrapping. I don't care.
}
@Override
public FieldElement replaceAll(char[] targets, char replacement) {
throw new UnsupportedOperationException("No wrapping");
}
@Override
public int getMaxCharactersForWidth(int width) {
// Assumes monospaced.
return width / em;
}
@Override
public RowColLocation getDataLocationForCharacterIndex(int characterIndex) {
return new RowColLocation(0, characterIndex);
}
@Override
public int getCharacterIndexForDataLocation(int dataRow, int dataColumn) {
if (dataRow == 0 && dataColumn >= 0 && dataColumn < length()) {
return dataColumn;
}
return -1;
}
protected static class SaveTransform implements AutoCloseable {
private final Graphics2D g;
private final AffineTransform saved;
public SaveTransform(Graphics g) {
this.g = (Graphics2D) g;
this.saved = this.g.getTransform();
}
@Override
public void close() {
this.g.setTransform(saved);
}
}
protected void paintChars(JComponent c, Graphics g, int x, int y, VtAttributes attrs, int start,
int end) {
char[] ch = line.getCharBuffer();
int descent = metrics.getDescent();
int height = metrics.getHeight();
int left = x + start * em;
int width = em * (end - start);
Font font = metrics.getFont();
Color bg = attrs.resolveBackground(colors);
if (bg != null) {
g.setColor(bg);
g.fillRect(left, descent - height, width, height);
}
g.setColor(attrs.resolveForeground(colors));
// NB. I don't really intend to implement blinking.
// TODO: AnsiFont mapping?
if (attrs.intensity() == Intensity.DIM) {
g.setFont(font.deriveFont(Font.PLAIN));
}
else {
// Normal will use bold font, but standard color
g.setFont(font.deriveFont(Font.BOLD));
}
if (!attrs.hidden()) {
switch (attrs.underline()) {
case DOUBLE:
g.fillRect(left, descent - UNDERLINE_HEIGHT * 3, width, UNDERLINE_HEIGHT);
// Yes, fall through
case SINGLE:
g.fillRect(left, descent - UNDERLINE_HEIGHT, width, UNDERLINE_HEIGHT);
case NONE:
}
for (int i = start; i < end; i++) {
/**
* HACK: The default monospaced font selected by Java may not have glyphs for the
* box-drawing characters, so it may choose glyphs from a different font.
* Alternatively, the default monospaced font's box-drawing glyphs are not, in fact,
* 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]);
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);
}
}
else {
g.drawChars(ch, i, 1, x + em * i, 0);
}
}
if (attrs.strikeThrough()) {
g.fillRect(left, height * 2 / 3, width, UNDERLINE_HEIGHT);
}
}
// TODO: What is proportionalSpacing?
}
@Override
public void paint(JComponent c, Graphics g, int x, int y) {
line.forEachRun(
(attrs, start, end) -> paintChars(c, g, x, y, attrs, start, end));
}
@Override
public FieldElement getFieldElement(int column) {
return this;
}
}

View file

@ -0,0 +1,115 @@
/* ###
* 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.plugin.core.terminal;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import ghidra.app.services.TerminalService;
import ghidra.util.Msg;
/**
* A terminal with a background thread and input stream powering its display.
*
* <p>
* The thread eagerly reads the given input stream and pumps it into the given provider. Be careful
* using {@link #injectDisplayOutput(ByteBuffer)}. While it is synchronized, there's no guarantee
* escape codes don't get mixed up. Note that this does not make any effort to connect the
* terminal's keyboard to any output stream.
*
* @see TerminalService#createWithStreams(java.nio.charset.Charset, InputStream, OutputStream)
*/
public class ThreadedTerminal extends DefaultTerminal {
protected final ReadableByteChannel in;
protected final Thread pumpThread = new Thread(this::pump);
protected final ByteBuffer buffer = ByteBuffer.allocate(1024);
protected boolean closed = false;
/**
* Construct a terminal connected to the given input stream
*
* @param provider the provider
* @param in the input stream
*/
public ThreadedTerminal(TerminalProvider provider, InputStream in) {
super(provider);
this.in = Channels.newChannel(in);
this.pumpThread.start();
}
@Override
public void close() {
closed = true;
pumpThread.interrupt();
super.close();
}
@SuppressWarnings("unused") // diagnostic
private void printBuffer() {
byte[] bytes = new byte[buffer.remaining()];
buffer.get(buffer.position(), bytes);
//System.err.println("<< " + NumericUtilities.convertBytesToString(bytes, ":"));
try {
String str = new String(bytes, "US-ASCII");
for (char c : str.toCharArray()) {
if (c == 0x1b) {
System.err.print("\n\\x1b");
}
else if (c < ' ' || c > '\u007f') {
System.err.print("\\x%02x".formatted((int) c));
}
else {
System.err.print(c);
}
}
System.err.println();
}
catch (UnsupportedEncodingException e) {
System.err.println("Couldn't decode");
}
}
protected void pump() {
try {
while (!closed) {
if (-1 == in.read(buffer) || closed) {
return;
}
buffer.flip();
//printBuffer();
synchronized (buffer) {
provider.processInput(buffer);
}
buffer.clear();
}
}
catch (IOException e) {
Msg.error(this, "Console input closed unexpectedly: " + e);
closed = true;
}
}
@Override
public void injectDisplayOutput(ByteBuffer bb) {
synchronized (buffer) {
provider.processInput(bb);
}
}
}

View file

@ -0,0 +1,49 @@
/* ###
* 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.plugin.core.terminal.vt;
import java.awt.Color;
import ghidra.app.plugin.core.terminal.vt.VtHandler.AnsiColor;
import ghidra.app.plugin.core.terminal.vt.VtHandler.Intensity;
/**
* A mechanism for converting an ANSI color specification to an AWT color.
*/
public interface AnsiColorResolver {
/**
* A stupid name for a thing that is either the foreground or the background.
*/
enum WhichGround {
FOREGROUND, BACKGROUND;
}
/**
* Convert a color specification to an AWT color
*
* @param color the ANSI color specification
* @param ground identifies the colors use in the foreground or the background
* @param intensity gives the intensity of the color, really only used when a basic color is
* specified.
* @param reverseVideo identifies whether the foreground and background colors were swapped,
* really only used when the default color is specified.
* @return the AWT color, or null to not draw (usually in the case of the default background
* color)
*/
Color resolveColor(AnsiColor color, WhichGround ground, Intensity intensity,
boolean reverseVideo);
}

View file

@ -0,0 +1,175 @@
/* ###
* 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.plugin.core.terminal.vt;
import java.awt.Color;
import ghidra.app.plugin.core.terminal.vt.AnsiColorResolver.WhichGround;
import ghidra.app.plugin.core.terminal.vt.VtHandler.*;
/**
* A tuple of attributes to apply when rendering terminal text.
*
* <p>
* These are set and collected as the parser and handler deal with various ANSI VT escape codes. As
* characters are placed in the buffer, the current attributes are applied to the corresponding
* cells. The renderer then has to apply the attributes appropriately as it renders each character
* in the buffer.
*/
public record VtAttributes(AnsiColor fg, AnsiColor bg, Intensity intensity,
AnsiFont font, Underline underline, Blink blink, boolean reverseVideo, boolean hidden,
boolean strikeThrough, boolean proportionalSpacing) {
/**
* The default attributes: plain white on black, usually.
*/
public static final VtAttributes DEFAULTS =
new VtAttributes(AnsiDefaultColor.INSTANCE, AnsiDefaultColor.INSTANCE,
Intensity.NORMAL, AnsiFont.NORMAL, Underline.NONE, Blink.NONE, false, false, false,
false);
/**
* Create a copy of this record with the foreground color replaced
*
* @param fg the new foreground color
* @return the new record
*/
public VtAttributes fg(AnsiColor fg) {
return new VtAttributes(fg, bg, intensity, font, underline, blink, reverseVideo,
hidden, strikeThrough, proportionalSpacing);
}
/**
* Create a copy of this record with the background color replaced
*
* @param bg the new background color
* @return the new record
*/
public VtAttributes bg(AnsiColor bg) {
return new VtAttributes(fg, bg, intensity, font, underline, blink, reverseVideo,
hidden, strikeThrough, proportionalSpacing);
}
/**
* Create a copy of this record with the intensity replaced
*
* @param intensity the new intensity
* @return the new record
*/
public VtAttributes intensity(Intensity intensity) {
return new VtAttributes(fg, bg, intensity, font, underline, blink, reverseVideo,
hidden, strikeThrough, proportionalSpacing);
}
/**
* Create a copy of this record with the font replaced
*
* @param font the new font
* @return the new record
*/
public VtAttributes font(AnsiFont font) {
return new VtAttributes(fg, bg, intensity, font, underline, blink, reverseVideo,
hidden, strikeThrough, proportionalSpacing);
}
/**
* Create a copy of this record with the underline replaced
*
* @param underline the new underline
* @return the new record
*/
public VtAttributes underline(Underline underline) {
return new VtAttributes(fg, bg, intensity, font, underline, blink, reverseVideo,
hidden, strikeThrough, proportionalSpacing);
}
/**
* Create a copy of this record with the blink replaced
*
* @param blink the new blink
* @return the new record
*/
public VtAttributes blink(Blink blink) {
return new VtAttributes(fg, bg, intensity, font, underline, blink, reverseVideo,
hidden, strikeThrough, proportionalSpacing);
}
/**
* Create a copy of this record with the reverse-video replaced
*
* @param reverseVideo the new reverse-video
* @return the new record
*/
public VtAttributes reverseVideo(boolean reverseVideo) {
return new VtAttributes(fg, bg, intensity, font, underline, blink, reverseVideo,
hidden, strikeThrough, proportionalSpacing);
}
/**
* Create a copy of this record with the hidden replaced
*
* @param hidden the new hidden
* @return the new record
*/
public VtAttributes hidden(boolean hidden) {
return new VtAttributes(fg, bg, intensity, font, underline, blink, reverseVideo,
hidden, strikeThrough, proportionalSpacing);
}
/**
* Create a copy of this record with the strike-through replaced
*
* @param strikeThrough the new strike-through
* @return the new record
*/
public VtAttributes strikeThrough(boolean strikeThrough) {
return new VtAttributes(fg, bg, intensity, font, underline, blink, reverseVideo,
hidden, strikeThrough, proportionalSpacing);
}
/**
* Create a copy of this record with the proportional-spacing replaced
*
* @param proportionalSpacing the new proportional-spacing
* @return the new record
*/
public VtAttributes proportionalSpacing(boolean proportionalSpacing) {
return new VtAttributes(fg, bg, intensity, font, underline, blink, reverseVideo,
hidden, strikeThrough, proportionalSpacing);
}
/**
* Resolve the foreground color for these attributes
*
* @param colors the color resolver
* @return the color
*/
public Color resolveForeground(AnsiColorResolver colors) {
return colors.resolveColor(reverseVideo ? bg : fg, WhichGround.FOREGROUND, intensity,
reverseVideo);
}
/**
* Resolve the background color for these attributes
*
* @param colors the color resolver
* @return the color, or null to not paint the background
*/
public Color resolveBackground(AnsiColorResolver colors) {
return colors.resolveColor(reverseVideo ? fg : bg, WhichGround.BACKGROUND, Intensity.NORMAL,
reverseVideo);
}
}

View file

@ -0,0 +1,753 @@
/* ###
* 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.plugin.core.terminal.vt;
import java.util.ArrayDeque;
import java.util.ArrayList;
import ghidra.app.plugin.core.terminal.vt.VtHandler.Erasure;
/**
* A buffer for a terminal display and scroll-back
*
* <p>
* 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.
*/
public class VtBuffer {
public static final int DEFAULT_ROWS = 25;
public static final int DEFAULT_COLS = 80;
protected static final int TAB_WIDTH = 8;
protected int rows;
protected int cols;
protected int curX;
protected int curY;
protected int savedX;
protected int savedY;
protected int scrollStart;
protected int scrollEnd; // exclusive
protected int maxScrollBack = 10_000;
protected VtAttributes curAttrs = VtAttributes.DEFAULTS;
protected ArrayDeque<VtLine> scrollBack = new ArrayDeque<>();
protected ArrayList<VtLine> lines = new ArrayList<>();
/**
* Create a new buffer of the default size (25 lines, 80 columns)
*/
public VtBuffer() {
this(DEFAULT_ROWS, DEFAULT_COLS);
}
/**
* Create a new buffer of the given size
*
* @param rows the number of rows
* @param cols the number of columns
*/
public VtBuffer(int rows, int cols) {
this.rows = Math.max(1, rows);
this.cols = Math.max(1, cols);
this.scrollStart = 0;
this.scrollEnd = rows;
while (lines.size() < rows) {
lines.add(new VtLine(cols));
}
}
/**
* Clear the buffer and all state, as if it has just been created
*/
public void reset() {
lines.clear();
while (lines.size() < rows) {
lines.add(new VtLine(cols));
}
curX = 0;
curY = 0;
scrollBack.clear();
}
/**
* Get the number of rows in the display
*
* <p>
* 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.
*
* @return the number of rows
*/
public int getRows() {
return rows;
}
/**
* Get the number of columns in the display
*
* <p>
* 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.
*
* @return the number of columns
*/
public int getCols() {
return cols;
}
/**
* Put the given character at the cursor, and move the cursor forward
*
* <p>
* The cursor's current attributes are applied to the character.
*
* @param c the character to put into the buffer
* @see #setAttributes(VtAttributes)
* @see #getAttributes()
*/
public void putChar(char c) {
if (c == 0) {
return;
}
lines.get(curY).putChar(curX, c, curAttrs);
}
/**
* More the cursor forward to the next tab stop
*/
public void tab() {
int n = TAB_WIDTH + (-curX % TAB_WIDTH);
moveCursorRight(n);
}
/**
* Move the cursor backward to the previous tab stop
*/
public void tabBack() {
if (curX == 0) {
return;
}
int n = (curX - 1) % TAB_WIDTH + 1;
moveCursorLeft(n);
}
/**
* Move the cursor back to the beginning of the line
*
* <p>
* This does <em>not</em> move the cursor down.
*/
public void carriageReturn() {
curX = 0;
}
/**
* Scroll the viewport down a line
*
* <p>
* 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.
*/
public void scrollViewportDown(boolean intoScrollBack) {
if (scrollStart == scrollEnd) {
return;
}
VtLine temp;
if (intoScrollBack && scrollStart == 0 && maxScrollBack > 0) {
temp = scrollBack.size() >= maxScrollBack ? scrollBack.remove() : null;
scrollBack.add(lines.remove(0));
}
else {
temp = lines.remove(scrollStart);
}
if (temp == null) {
temp = new VtLine(cols);
}
else {
temp.reset(cols);
}
lines.add(scrollEnd - 1, temp); // Account for removed line
}
/**
* Scroll the viewport up a line
*
* <p>
* 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.
*/
public void scrollViewportUp() {
VtLine temp = lines.remove(scrollEnd - 1);
temp.reset(cols);
lines.add(scrollStart, temp);
}
/**
* If the cursor is beyond the bottom of the display, scroll the viewport down and move the
* cursor up until the cursor is at the bottom of the display. If applicable, lines at the top
* of the display is shifted into the scroll-back buffer.
*/
public void checkVerticalScroll() {
while (curY >= scrollEnd) {
scrollViewportDown(true);
curY = Math.max(0, curY - 1);
}
}
/**
* Move the cursor up n rows
*
* <p>
* 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)}.
*
* @param n the number of rows to move the cursor up
*/
public void moveCursorUp(int n) {
curY = Math.max(0, curY - n);
}
/**
* Move the cursor down n rows
*
* <p>
* 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)}.
*
* @param n
*/
public void moveCursorDown(int n) {
curY += n;
checkVerticalScroll();
}
/**
* Move the cursor left (backward) n columns
*
* <p>
* If the cursor would move left of the display, it is instead moved to the far right of the
* previous row, unless the cursor is already on the top row, in which case, it will be placed
* in the top-left corner of the display. NOTE: If the cursor is moved to the previous row, no
* heed is given to "leftovers." It doesn't matter how far to the left the cursor would have
* been; it is moved to the far right column and exactly one row up. The value of n must be
* positive, otherwise behavior is undefined. To move the cursor right, use
* {@link #moveCursorRight(int)}.
*
* @param n the number of columns
*/
public void moveCursorLeft(int n) {
if (curX - n >= 0) {
curX -= n;
}
else if (curY > 0) {
curX = cols - 1;
curY--;
}
}
/**
* Move the cursor right (forward) n columns
*
* <p>
* If the cursor would move right of the display, it is instead moved to the far left of the
* next row. If the cursor is already on the bottom row, the viewport is scrolled down a line.
* NOTE: If the cursor is moved to the next row, no heed is given to "leftovers." It doesn't
* matter how far to the right the cursor would have been; it is moved to the far left column
* and exactly one row down. The value of n must be positive, otherwise behavior is undefined.
* To move the cursor left, use {@link #moveCursorLeft(int)}.
*
* @param n the number of columns
*/
public void moveCursorRight(int n) {
curX += n;
if (curX >= cols) {
curX = 0;
curY++;
}
checkVerticalScroll();
}
/**
* Save the current cursor position
*
* <p>
* 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
* {@link #getCurX()} and {@link #getCurY()} to save it externally, is that the buffer will
* adjust the saved position if the buffer is resized via {@link #resize(int, int)}.
*/
public void saveCursorPos() {
savedX = curX;
savedY = curY;
}
/**
* Restore a saved cursor position
*
* <p>
* If there was no previous call to {@link #saveCursorPos()}, the cursor is placed at the
* top-left of the display.
*/
public void restoreCursorPos() {
curX = savedX;
curY = savedY;
}
/**
* Move the cursor to the given row and column
*
* <p>
* The position is clamped to the dimensions of the display. No scrolling will take place if
* {@code col} exceeds the number of rows.
*
* @param row the desired row, 0 up, top to bottom
* @param col the desired column, 0 up, left to right
*/
public void moveCursor(int row, int col) {
this.curX = Math.max(0, Math.min(cols - 1, col));
this.curY = Math.max(0, Math.min(rows - 1, row));
}
/**
* Get the cursor's current attributes
*
* <p>
* Characters put into the buffer via {@link #putChar(char)} are assigned the cursor's current
* attributes at the time they are inserted.
*
* @see #setAttributes(VtAttributes)
* @return the current attributes
*/
public VtAttributes getAttributes() {
return curAttrs;
}
/**
* Set the cursor's current attributes
*
* <p>
* 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.
*
* @param attributes the desired attributes
*/
public void setAttributes(VtAttributes attributes) {
this.curAttrs = attributes == null ? VtAttributes.DEFAULTS : attributes;
}
/**
* Erase (clear) some portion of the display buffer
*
* <p>
* If the current line is erased from start to the cursor, the cursor's attributes are applied
* to the cleared columns.
*
* @param erasure specifies what, relative to the cursor, to erase.
*/
public void erase(Erasure erasure) {
switch (erasure) {
case TO_DISPLAY_END:
for (int y = curY; y < rows; y++) {
VtLine line = lines.get(y);
if (y == curY) {
line.clearToEnd(curX);
}
else {
line.clear();
}
}
return;
case TO_DISPLAY_START:
for (int y = 0; y <= curY; y++) {
VtLine line = lines.get(y);
if (y == curY) {
line.clearToStart(curX, curAttrs);
}
else {
line.clear();
}
}
return;
case FULL_DISPLAY:
for (VtLine line : lines) {
line.clear();
}
return;
case FULL_DISPLAY_AND_SCROLLBACK:
for (VtLine line : lines) {
line.clear();
}
scrollBack.clear();
return;
case TO_LINE_END:
lines.get(curY).clearToEnd(curX);
return;
case TO_LINE_START:
lines.get(curY).clearToStart(curX, curAttrs);
return;
case FULL_LINE:
lines.get(curY).clear();
return;
}
}
/**
* Insert n blank lines at the cursor
*
* <p>
* 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
* above the cursor are affected, including those in the scroll-back buffer.
*
* @param n the number of lines to insert
*/
public void insertLines(int n) {
for (int i = 0; i < n; i++) {
VtLine temp = lines.remove(scrollEnd - 1);
temp.reset(cols);
lines.add(curY, temp);
}
}
/**
* Delete n lines at the cursor
*
* <p>
* 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
* above) the bottom of the viewport. None of the lines above the cursor are affected.
*
* @param n the number of lines to delete
*/
public void deleteLines(int n) {
for (int i = 0; i < n; i++) {
VtLine temp = lines.remove(curY);
temp.reset(cols);
lines.add(scrollEnd - 1, temp); // account for removed index
}
}
/**
* Insert n blank characters at the cursor
*
* <p>
* 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
* be moved or inserted right of the display buffer are effectively deleted. The cursor is
* <em>not</em> moved after this operation.
*
* @param n the number of blanks to insert.
*/
public void insertChars(int n) {
lines.get(curY).insert(curX, n);
}
/**
* Delete n characters at the cursor
*
* <p>
* 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.
*
* @param n the number of characters to delete
*/
public void deleteChars(int n) {
lines.get(curY).delete(curX, curX + n);
}
/**
* Erase n characters at the cursor
*
* <p>
* Characters at (and {@code n-1} to the right) of the cursor are erased, i.e., replaced with
* blanks. No shifting takes place.
*
* @param n the number of characters to erase
*/
public void eraseChars(int n) {
lines.get(curY).erase(curX, curX + n, curAttrs);
}
/**
* Specify the scrolling viewport of the buffer
*
* <p>
* 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
* be updated on calls to {@link #resize(int, int)}. Both parameters are optional, though
* {@code end} should likely only be given if {@code start} is also given. The parameters are
* silently adjusted to ensure that both are within the bounds of the display and so that the
* end is at or below the start. Once set, the cursor should remain within the viewport, or
* otherwise cause the viewport to scroll. Operations that would cause the display to scroll,
* instead cause just the viewport to scroll. Additionally, cursor movement operations are
* clamped to the viewport.
*
* @param start the first line in the viewport, 0 up, top to bottom, inclusive. If omitted, this
* is the top line of the display.
* @param end the last line in the viewport, 0 up, top to bottom, inclusive. If omitted, this is
* the bottom line of the display.
*/
public void setScrollViewport(Integer start, Integer end) {
if (start != null) {
scrollStart = Math.max(0, start);
}
else {
scrollStart = 0;
}
if (end != null) {
// scrollEnd is exclusive
scrollEnd = Math.max(scrollStart + 1, Math.min(rows, end + 1));
}
else {
scrollEnd = rows;
}
}
/**
* Resize the buffer to the given number of rows and columns
*
* <p>
* 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
* decreasing, lines at the top of the display are be shifted into the scroll-back buffer. If
* the row count is increasing, lines at the bottom of the scroll-back buffer are shifted into
* the display buffer. The scroll-back buffer may be culled if the resulting number of lines
* exceeds that scroll-back maximum. The cursor position is adjusted so that, if possible, it
* remains on the same line. (The cursor cannot enter the scroll-back region.) Finally, the
* cursor is clamped into the display region. The saved cursor, if applicable, is similarly
* treated.
*
* @param cols the number of columns
* @param rows the number of rows
* @return true if the buffer was actually resized
*/
public boolean resize(int cols, int rows) {
cols = Math.max(1, cols);
rows = Math.max(1, rows);
if (this.rows == rows && this.cols == cols) {
return false;
}
for (VtLine line : scrollBack) {
line.resize(cols);
}
for (VtLine line : lines) {
line.resize(cols);
}
this.rows = rows;
this.cols = cols;
this.scrollStart = 0;
this.scrollEnd = rows;
while (lines.size() < rows) {
lines.add(0, scrollBack.isEmpty() ? new VtLine(cols) : scrollBack.pollLast());
curY++;
savedY++;
}
while (lines.size() > rows) {
scrollBack.addLast(lines.remove(0));
curY--;
savedY--;
}
while (scrollBack.size() > maxScrollBack) {
scrollBack.pollFirst();
}
curX = Math.min(curX, cols - 1);
savedX = Math.min(savedX, cols - 1);
curY = Math.max(0, Math.min(curY, rows - 1));
savedY = Math.max(0, Math.min(savedY, rows - 1));
return true;
}
/**
* Adjust the maximum number of lines in the scroll-back buffer
*
* <p>
* If the scroll-back buffer exceeds the given maximum, it is immediately culled.
*
* @param maxScrollBack the maximum number of scroll-back lines
*/
public void setMaxScrollBack(int maxScrollBack) {
this.maxScrollBack = maxScrollBack;
while (scrollBack.size() > maxScrollBack) {
scrollBack.pollFirst();
}
}
/**
* A callback for iterating over the lines of the buffer
*/
public interface LineConsumer {
/**
* Process a line of terminal text
*
* @param i the index of the line, optionally including scroll-back, 0 up, top to bottom
* @param y the vertical position of the line. For a scroll-back line, this is -1.
* Otherwise, this counts 0 up, top to bottom.
* @param t the line
* @see VtBuffer#forEachLine(boolean, LineConsumer)
*/
void accept(int i, int y, VtLine t);
}
/**
* Perform an action on each line of terminal text, optionally including the scroll-back buffer.
*
* @param includeScrollBack true to include the scroll-back buffer
* @param action the action
*/
public void forEachLine(boolean includeScrollBack, LineConsumer action) {
int i = 0;
if (includeScrollBack) {
for (VtLine line : scrollBack) {
action.accept(i, -1, line);
i++;
}
}
int y = 0;
for (VtLine line : lines) {
action.accept(i, y, line);
i++;
y++;
}
}
/**
* Get the total number of lines, including scroll-back lines, in the buffer
*
* <p>
* This is equal to {@link #getScrollBackSize()}{@code +}{@link #getRows()}.
*
* @return the number of lines
*/
public int size() {
return scrollBack.size() + rows;
}
/**
* Get the number of lines in the scroll-back buffer
*
* @return the number of lines
*/
public int getScrollBackSize() {
return scrollBack.size();
}
/**
* Get the cursor's column, 0 up, left to right
*
* @return the column
*/
public int getCurX() {
return curX;
}
/**
* Get the cursor's row, 0 up, top to bottom
*
* @return the row
*/
public int getCurY() {
return curY;
}
/**
* This is essentially the loop body for {@link #getText(int, int, int, int, CharSequence)}. It
* is factored into a separate method, because we need to loop over the scroll-back buffer as
* well as the display buffer, and we want the same body.
*/
protected boolean gatherLineText(StringBuilder sb, int startRow, int startCol, int endRow,
int endCol, int i, VtLine line, CharSequence lineSep) {
if (i < startRow) {
return false;
}
if (i == startRow && startRow == endRow) {
line.gatherText(sb, startCol, endCol);
return true;
}
if (i == startRow) {
line.gatherText(sb, startCol, cols);
sb.append(lineSep);
return false;
}
if (i == endRow) {
line.gatherText(sb, 0, endCol);
return true;
}
if (i > endRow) {
return true;
}
line.gatherText(sb, 0, cols);
sb.append(lineSep);
return false;
}
/**
* Get the text between two locations in the buffer
*
* <p>
* 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.
*
* <p>
* 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.
*
* @param startRow the row for the starting location, inclusive
* @param startCol the column for the starting location, inclusive
* @param endRow the row for the ending location, inclusive
* @param endCol the column for the ending location, <em>exclusive</em>
* @param lineSep the line separator
* @return the text
*/
public String getText(int startRow, int startCol, int endRow, int endCol,
CharSequence lineSep) {
StringBuilder buf = new StringBuilder();
int sbSize = scrollBack.size();
if (startRow < sbSize) {
int i = 0;
for (VtLine line : scrollBack) {
if (gatherLineText(buf, startRow, startCol, endRow, endCol, i, line, lineSep)) {
break;
}
i++;
}
}
for (int i = Math.max(sbSize, startRow); i <= endRow; i++) {
VtLine line = lines.get(i - sbSize);
gatherLineText(buf, startRow, startCol, endRow, endCol, i, line, lineSep);
}
return buf.toString();
}
}

View file

@ -0,0 +1,111 @@
/* ###
* 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.plugin.core.terminal.vt;
/**
* A legacy style charset
*
* <p>
* 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.
* 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.
*
* <p>
* It'd be nice to just use UTF-8, but the application would have to agree.
*/
public enum VtCharset {
UK,
USASCII,
FINNISH,
SWEDISH,
GERMAN,
FRENCH_CANADIAN,
FRENCH,
ITALIAN,
SPANISH,
DUTCH,
GREEK,
TURKISH,
PORTUGESE,
HEBREW,
SWISS,
NORWEGIAN_DANISH,
DEC_SPECIAL_LINES {
@Override
public char mapChar(char c) {
switch (c) {
case 'j':
return '\u2518'; // 1pt lower-right corner
case 'k':
return '\u2510'; // 1pt upper-right corner
case 'l':
return '\u250C'; // 1pt upper-left corner
case 'm':
return '\u2514'; // 1pt lower-left corner
case 'q':
return '\u2500'; // 1pt horizontal line
case 'x':
return '\u2502'; // 1pt vertical line
}
return super.mapChar(c);
}
},
DEC_SUPPLEMENTAL,
DEC_TECHNICAL,
DEC_HEBREW,
DEC_GREEK,
DEC_TURKISH,
DEC_SUPPLEMENTAL_GRAPHICS,
DEC_CYRILLIC,
;
/**
* The designation for a charset slot
*
* <p>
* It seems the terminal allows for the selection of 4 alternative charsets, the first of which
* G0 is the default or primary.
*/
public enum G {
G0('('), G1(')'), G2('*'), G3('-');
public final byte b;
/**
* Construct a charset slot designator
*
* @param b the byte in the control sequence that identifies this slot
*/
private G(char b) {
this.b = (byte) b;
}
}
/**
* Map a character, as decoded using US-ASCII, into the actual character for the character set.
*
* @param c the character from US-ASCII.
* @return the mapped character
*/
public char mapChar(char c) {
return c;
}
}

View file

@ -0,0 +1,330 @@
/* ###
* 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.plugin.core.terminal.vt;
/**
* A line of text in the {@link VtBuffer}
*/
public class VtLine {
protected int cols;
protected int len;
protected char[] chars;
private VtAttributes[] cellAttrs;
/**
* Create a line with the given maximum number of characters
*
* @param cols the maximum number of characters
*/
public VtLine(int cols) {
reset(cols);
}
/**
* Get the character in the given column
*
* @param x the column, 0 up
* @return the character
*/
public char getChar(int x) {
return chars[x];
}
/**
* Get the full character buffer
*
* <p>
* This is a reference to the buffer, which is very useful when rendering. Modifying this buffer
* externally is not recommended.
*
* @return the buffer
*/
public char[] getCharBuffer() {
return chars;
}
/**
* Get the attributes for the character in the given column
*
* @param x the column, 0 up
* @return the attributes
*/
public VtAttributes getCellAttrs(int x) {
VtAttributes attrs = cellAttrs[x];
if (attrs == null) {
return VtAttributes.DEFAULTS;
}
return attrs;
}
/**
* Place the given character with attributes into the given column
*
* @param x the column, 0 up
* @param c the character
* @param attrs the attributes
*/
public void putChar(int x, char c, VtAttributes attrs) {
int oldLen = len;
len = Math.max(len, x + 1);
for (int i = oldLen; i < x; i++) {
chars[i] = ' ';
cellAttrs[i] = VtAttributes.DEFAULTS;
}
chars[x] = c;
if (attrs != null) {
cellAttrs[x] = attrs;
}
}
/**
* Resize the line to the given maximum character count
*
* @param cols the maximum number of characters
*/
public void resize(int cols) {
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) {
return;
}
char[] newChars = new char[cols];
VtAttributes[] newCellAttrs = new VtAttributes[cols];
System.arraycopy(chars, 0, newChars, 0, Math.min(cols, chars.length));
System.arraycopy(cellAttrs, 0, newCellAttrs, 0, Math.min(cols, cellAttrs.length));
this.chars = newChars;
this.cellAttrs = newCellAttrs;
}
/**
* Reset the line
*
* @param cols
*/
public void reset(int cols) {
this.cols = cols;
this.len = 0;
if (this.cols != cols || this.chars == null) {
this.chars = new char[cols];
this.cellAttrs = new VtAttributes[cols];
}
}
/**
* Get the length of the line, excluding trailing cleared characters
*
* @return the length
*/
public int length() {
return Math.min(len, cols);
}
/**
* Get the number of columns in the line
*
* @return the column count
*/
public int cols() {
return cols;
}
/**
* Clear the full line
*/
public void clear() {
len = 0;
}
/**
* Clear characters at and after the given column
*
* @param x the column, 0 up
*/
public void clearToEnd(int x) {
len = Math.min(len, x);
}
/**
* Clear characters before and at the given column
*
* @param x the column, 0 up
* @param attrs attributes to apply to the cleared (space) characters
*/
public void clearToStart(int x, VtAttributes attrs) {
if (len <= x) {
len = 0;
return;
}
for (int i = 0; i <= x; i++) {
chars[i] = ' ';
cellAttrs[i] = attrs;
}
}
/**
* Delete characters in the given range, shifting remaining characters to the left
*
* @param start the first column, 0 up
* @param end the last column, exclusive, 0 up
*/
public void delete(int start, int end) {
if (len <= end) {
len = start;
return;
}
int shift = end - start;
len -= shift;
for (int x = start; x < end; x++) {
chars[x] = chars[x + shift];
cellAttrs[x] = cellAttrs[x + shift];
}
}
/**
* Replace characters in the given range with spaces
*
* <p>
* If the last column is erased, this instead clears from the start to the end. The difference
* is subtle, but deals in how the line reports its text contents. The trailing spaces will not
* be included if this call results in the last column being erased.
*
* @param start the first column, 0 up
* @param end the last column, exclusive, 0 up
* @param attrs the attributes to assign the space characters
*/
public void erase(int start, int end, VtAttributes attrs) {
if (len <= end) {
len = start;
return;
}
for (int x = start; x < end; x++) {
chars[x] = ' ';
cellAttrs[x] = attrs;
}
}
/**
* Insert n (space) characters at and after the given column
*
* @param start the column, 0 up
* @param n the number of characters to insert
*/
public void insert(int start, int n) {
// Via experimentation, there is no wrapping.
// Neither of the shifted, nor the inserted characters.
// Additionally, the cursor does not move.
// 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];
cellAttrs[x] = cellAttrs[x - n];
}
for (int x = start; x < end; x++) {
chars[x] = ' ';
}
len = Math.min(cols, len + n);
}
/**
* A callback for a run of contiguous characters having the same attributes
*/
public interface RunConsumer {
/**
* Execute an action on a run
*
* @param attrs the attributes shared by all in the run
* @param start the first column of the run, 0 up
* @param end the last column of the run, exclusive, 0 up
*/
void accept(VtAttributes attrs, int start, int end);
}
/**
* Execute an action on each run of contiguous characters having the same attributes, from left
* to right.
*
* @param action the callback action
*/
public void forEachRun(RunConsumer action) {
int length = length();
if (length == 0) {
action.accept(VtAttributes.DEFAULTS, 0, 0);
}
int first = 0;
VtAttributes attrs = getCellAttrs(0);
for (int x = 1; x < length; x++) {
if (!attrs.equals(getCellAttrs(x))) {
action.accept(attrs, first, x);
first = x;
attrs = getCellAttrs(x);
}
}
action.accept(attrs, first, length);
}
/**
* Append a portion of this line's text to the given string builder
*
* @param sb the destination builder
* @param start the first column, 0 up
* @param end the last column, exclusive, 0 up
*/
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);
}
/**
* Check if the given character is considered part of a word
*
* <p>
* This is used both when selecting words, and when requiring search to find whole words.
*
* @param ch the character
* @return true if the character is part of a word
*/
public static boolean isWordChar(char ch) {
return Character.isLetterOrDigit(ch) || ch == '_' || ch == '-' || ch == '@';
}
/**
* Find the boundaries for the word at the given column
*
* @param x the column, 0 up
* @param forward true to find the end, false to find the beginning
* @return the first column, 0 up, or the last column, exclusive, 0 up
*/
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)) {
continue;
}
if (forward) {
return i;
}
return i + 1;
}
if (forward) {
return len;
}
return 0;
}
}

View file

@ -0,0 +1,34 @@
/* ###
* 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.plugin.core.terminal.vt;
import java.nio.ByteBuffer;
/**
* A callback for bytes generated by the terminal, e.g., to report a key press or reply to a
* request.
*/
public interface VtOutput {
/**
* Handle output from the terminal
*
* <p>
* Most likely these bytes should be sent down an output stream, usually to a pty.
*
* @param buf the buffer of bytes generated
*/
void out(ByteBuffer buf);
}

View file

@ -0,0 +1,123 @@
/* ###
* 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.plugin.core.terminal.vt;
import java.nio.ByteBuffer;
/**
* The parser for a terminal emulator
*
* <p>
* The only real concern of this parser is to separate escape sequences from normal character
* output. All state not related to parsing is handled by a {@link VtHandler}. Most of the logic is
* implemented in the machine state nodes: {@link VtState}.
*/
public class VtParser {
protected final VtHandler handler;
protected VtState state = VtState.CHAR;
protected VtCharset.G csG;
protected ByteBuffer csiParam = ByteBuffer.allocate(100);
protected ByteBuffer csiInter = ByteBuffer.allocate(100);
protected ByteBuffer oscParam = ByteBuffer.allocate(100);
/**
* Construct a parser with the given handler
*
* @param handler the handler
*/
public VtParser(VtHandler handler) {
this.handler = handler;
}
/**
* Create a copy of the CSI buffers, reconstructed as they were in the original stream.
*
* <p>
* This is used to re-process parsed bytes after broken CSI sequence
*
* @param b the character currently being parsed
* @return the copy
*/
protected ByteBuffer copyCsiBuffer(byte b) {
csiParam.flip();
csiInter.flip();
ByteBuffer buf = ByteBuffer.allocate(2 + csiParam.remaining() + csiInter.remaining());
buf.put((byte) '[');
buf.put(csiParam);
buf.put(csiInter);
buf.put(b);
csiParam.clear();
csiInter.clear();
return buf;
}
/**
* Create a copy of the OSC buffers, reconstructed as they were in the original stream.
*
* <p>
* This is used to re-process parsed bytes after a broken OSC sequence
*
* @param b the character currently being parsed
* @return the copy
*/
protected ByteBuffer copyOscBuffer(byte b) {
oscParam.flip();
ByteBuffer buf = ByteBuffer.allocate(2 + oscParam.remaining());
buf.put((byte) ']');
buf.put(oscParam);
buf.put(b);
oscParam.clear();
return buf;
}
/**
* Process the bytes from the given buffer
*
* <p>
* This is likely fed from an input stream, usually of a pty.
*
* @param buf the buffer
*/
public void process(ByteBuffer buf) {
state = doProcess(state, buf);
}
/**
* Process a given byte by delegating to the current state machine node
*
* @param state the node
* @param b the byte
* @return the new state node
*/
protected VtState doProcessByte(VtState state, byte b) {
return state.handleNext(b, this, handler);
}
/**
* Process a given byte buffer, one byte at a time
*
* @param state the initial machine state node
* @param buf the buffer
* @return the resulting machine state node
*/
protected VtState doProcess(VtState state, ByteBuffer buf) {
while (buf.hasRemaining()) {
state = doProcessByte(state, buf.get());
}
return state;
}
}

View file

@ -0,0 +1,64 @@
/* ###
* 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.plugin.core.terminal.vt;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
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 final ByteBuffer bb = ByteBuffer.allocate(16);
protected final Charset charset;
public VtResponseEncoder(Charset charset) {
this.charset = charset;
}
protected abstract void generateBytes(ByteBuffer buf);
public void reportCursorPos(int row, int col) {
bb.put(("\033[" + row + ";" + col + "R").getBytes(charset));
generateBytesExc();
}
protected void generateBytesExc() {
bb.flip();
try {
generateBytes(bb);
}
catch (Throwable t) {
Msg.error(this, "Error generating bytes: " + t, t);
}
finally {
bb.clear();
}
}
public void reportPasteStart() {
bb.put(PASTE_START);
generateBytesExc();
}
public void reportPasteEnd() {
bb.put(PASTE_END);
generateBytesExc();
}
}

View file

@ -0,0 +1,337 @@
/* ###
* 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.plugin.core.terminal.vt;
import ghidra.app.plugin.core.terminal.vt.VtCharset.G;
public enum VtState {
/**
* The initial state, just process output characters until we encounter an {@code ESC}.
*/
CHAR {
@Override
protected VtState handleNext(byte b, VtParser parser, VtHandler handler) {
if (b == 0x1b) {
return ESC;
}
handler.handleCharExc(b);
return CHAR;
}
},
/**
* We have just encountered an {@code ESC}.
*/
ESC {
@Override
protected VtState handleNext(byte b, VtParser parser, VtHandler handler) {
switch (b) {
case '7':
handler.handleSaveCursorPos();
return CHAR;
case '8':
handler.handleRestoreCursorPos();
return CHAR;
case '(':
parser.csG = G.G0;
return CHARSET;
case ')':
parser.csG = G.G1;
return CHARSET;
case '*':
parser.csG = G.G2;
return CHARSET;
case '+':
parser.csG = G.G3;
return CHARSET;
case '[':
return CSI_PARAM;
case ']':
return OSC_PARAM;
case '=':
handler.handleApplicationKeypad(true);
return CHAR;
case '>':
// Normal keypad
handler.handleApplicationKeypad(false);
return CHAR;
case 'D':
handler.handleScrollViewportDown(1, true);
return CHAR;
case 'M':
handler.handleScrollViewportUp(1);
return CHAR;
case 'c':
handler.handleFullReset();
return CHAR;
}
handler.handleCharExc((byte) 0x1b);
handler.handleCharExc(b);
return CHAR;
}
},
/**
* We have encountered {@code ESC} and a charset-selection byte. Now we just need to know the
* charset. Most are one byte, but there are some two-byte codes.
*/
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':
handler.handleSetCharset(parser.csG, VtCharset.UK);
return CHAR;
case 'B':
handler.handleSetCharset(parser.csG, VtCharset.USASCII);
return CHAR;
case 'C':
case '5':
handler.handleSetCharset(parser.csG, VtCharset.FINNISH);
return CHAR;
case 'H':
case '7':
handler.handleSetCharset(parser.csG, VtCharset.SWEDISH);
return CHAR;
case 'K':
handler.handleSetCharset(parser.csG, VtCharset.GERMAN);
return CHAR;
case 'Q':
case '9':
handler.handleSetCharset(parser.csG, VtCharset.FRENCH_CANADIAN);
return CHAR;
case 'R':
case 'f':
handler.handleSetCharset(parser.csG, VtCharset.FRENCH);
return CHAR;
case 'Y':
handler.handleSetCharset(parser.csG, VtCharset.ITALIAN);
return CHAR;
case 'Z':
handler.handleSetCharset(parser.csG, VtCharset.SPANISH);
return CHAR;
case '4':
handler.handleSetCharset(parser.csG, VtCharset.DUTCH);
return CHAR;
case '=':
handler.handleSetCharset(parser.csG, VtCharset.SWISS);
return CHAR;
case '`':
case 'E':
case '6':
handler.handleSetCharset(parser.csG, VtCharset.NORWEGIAN_DANISH);
return CHAR;
case '0':
handler.handleSetCharset(parser.csG, VtCharset.DEC_SPECIAL_LINES);
return CHAR;
case '<':
handler.handleSetCharset(parser.csG, VtCharset.DEC_SUPPLEMENTAL);
return 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);
}
},
/**
* We're selecting a two-byte charset, and we just encountered {@code "}.
*/
CHARSET_QUOTE {
@Override
protected VtState handleNext(byte b, VtParser parser, VtHandler handler) {
switch (b) {
case '>':
handler.handleSetCharset(parser.csG, VtCharset.GREEK);
return CHAR;
case '4':
handler.handleSetCharset(parser.csG, VtCharset.DEC_HEBREW);
return 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);
}
},
/**
* We're selecting a two-byte charset, and we just encountered {@code %}.
*/
CHARSET_PERCENT {
@Override
protected VtState handleNext(byte b, VtParser parser, VtHandler handler) {
switch (b) {
case '2':
handler.handleSetCharset(parser.csG, VtCharset.TURKISH);
return CHAR;
case '6':
handler.handleSetCharset(parser.csG, VtCharset.PORTUGESE);
return CHAR;
case '=':
handler.handleSetCharset(parser.csG, VtCharset.HEBREW);
return CHAR;
case '0':
handler.handleSetCharset(parser.csG, VtCharset.DEC_TURKISH);
return 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);
}
},
/**
* We're selecting a two-byte charset, and we just encountered {@code &}.
*/
CHARSET_AMPERSAND {
@Override
protected VtState handleNext(byte b, VtParser parser, VtHandler handler) {
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);
}
},
/**
* We've encountered {@code CSI}, so now we're parsing parameters, intermediates, or the final
* character.
*/
CSI_PARAM {
@Override
protected VtState handleNext(byte b, VtParser parser, VtHandler handler) {
if (0x30 <= b && b <= 0x3f) {
parser.csiParam.put(b);
return CSI_PARAM;
}
if (0x20 <= b && b <= 0x2f) {
parser.csiInter.put(b);
return CSI_INTER;
}
if (0x40 <= b && b <= 0x7e) {
handleCsi(b, parser, handler);
return CHAR;
}
handler.handleCharExc((byte) 0x1b);
return parser.doProcess(CHAR, parser.copyCsiBuffer(b));
}
},
/**
* We've finished (or skipped) parsing CSI parameters, so now we're parsing intermediates or the
* final character.
*/
CSI_INTER {
@Override
protected VtState handleNext(byte b, VtParser parser, VtHandler handler) {
if (0x20 <= b && b <= 0x2f) {
parser.csiInter.put(b);
return CSI_INTER;
}
if (0x40 <= b && b <= 0x7e) {
handleCsi(b, parser, handler);
return CHAR;
}
handler.handleCharExc((byte) 0x1b);
return parser.doProcess(CHAR, parser.copyCsiBuffer(b));
}
},
/**
* We've encountered {@code OSC}, so now we're parsing parameters until we encounter {@code BEL}
* or {@code ST}.
*/
OSC_PARAM {
@Override
protected VtState handleNext(byte b, VtParser parser, VtHandler handler) {
if (0x20 <= b && b <= 0x7f) {
parser.oscParam.put(b);
return OSC_PARAM;
}
if (b == 0x07) {
handleOsc(parser, handler);
return CHAR;
}
if (b == 0x1b) {
return OSC_ESC;
}
handler.handleCharExc((byte) 0x1b);
return parser.doProcess(CHAR, parser.copyOscBuffer(b));
}
},
/**
* We've encountered {@code ESC} part of , so now we're parsing parameters until we encounter
* {@code BEL} or {@code ST}.
*/
OSC_ESC {
@Override
protected VtState handleNext(byte b, VtParser parser, VtHandler handler) {
if (b == '\\') {
handleOsc(parser, handler);
return CHAR;
}
handler.handleCharExc((byte) 0x1b);
return parser.doProcessByte(OSC_PARAM, b);
}
};
/**
* Handle the given character
*
* @param b the character currently being parsed
* @param parser the parser
* @param handler the handler
* @return the resulting machine state
*/
protected abstract VtState handleNext(byte b, VtParser parser, VtHandler handler);
/**
* Handle a CSI sequence
*
* @param csiFinal the final byte
* @param parser the parser
* @param handler the handler
*/
protected void handleCsi(byte csiFinal, VtParser parser, VtHandler handler) {
parser.csiParam.flip();
parser.csiInter.flip();
handler.handleCsiExc(parser.csiParam, parser.csiInter, csiFinal);
parser.csiParam.clear();
parser.csiInter.clear();
}
/**
* Handle an OSC sequence
*
* @param parser the parser
* @param handler the handler
*/
protected void handleOsc(VtParser parser, VtHandler handler) {
parser.oscParam.flip();
handler.handleOscExc(parser.oscParam);
parser.oscParam.clear();
}
}

View file

@ -23,6 +23,7 @@ import javax.swing.event.ChangeListener;
import docking.ActionContext;
import docking.ComponentProvider;
import docking.action.DockingAction;
import ghidra.app.util.ClipboardType;
import ghidra.util.task.TaskMonitor;
@ -140,4 +141,35 @@ public interface ClipboardContentProviderService {
* @return true if copy special is enabled
*/
public boolean canCopySpecial();
/**
* Provide an alternative action owner.
*
* <p>
* This may be necessary if the key bindings or other user-customizable attributes need to be
* separated from the standard clipboard actions. By default, the clipboard service will create
* actions with a shared owner so that one keybinding, e.g., Ctrl-C, is shared across all Copy
* actions.
*
* @return the alternative owner, or null for the standard owner
* @see #customizeClipboardAction(DockingAction)
*/
default public String getClipboardActionOwner() {
return null;
}
/**
* Customize the given action.
*
* <p>
* This method is called at the end of the action's constructor, which takes placed
* <em>before</em> the action is added to the provider. By default, this method does nothing.
* Likely, you will need to know which action you are customizing. Inspect the action name.
*
* @param action the action
* @see #getClipboardActionOwner()
*/
default void customizeClipboardAction(DockingAction action) {
// Default is don't customize
}
}

View file

@ -1,6 +1,5 @@
/* ###
* 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.
@ -17,6 +16,7 @@
package ghidra.app.services;
public interface ClipboardService {
public void registerClipboardContentProvider( ClipboardContentProviderService service );
public void deRegisterClipboardContentProvider( ClipboardContentProviderService service );
public void registerClipboardContentProvider(ClipboardContentProviderService service);
public void deRegisterClipboardContentProvider(ClipboardContentProviderService service);
}

View file

@ -0,0 +1,78 @@
/* ###
* 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.services;
import java.nio.ByteBuffer;
import ghidra.app.plugin.core.terminal.TerminalListener;
/**
* A handle to a terminal window in the UI.
*/
public interface Terminal extends AutoCloseable {
/**
* Add a listener for terminal events
*
* @param listener the listener
*/
void addTerminalListener(TerminalListener listener);
/**
* Remove a listener for terminal events
*
* @param listener the listener
*/
void removeTerminalListener(TerminalListener listener);
/**
* Process the given buffer as if it were output by the terminal's application.
*
* <p>
* <b>Warning:</b> While implementations may synchronize to ensure the additional buffer is not
* processed at the same time as actual application input, there may not be any effort to ensure
* that the buffer is not injected in the middle of an escape sequence. Even if the injection is
* outside an escape sequence, this may still lead to unexpected behavior, since the injected
* output may be affected by or otherwise interfere with the application's control of the
* terminal's state. Generally, this should only be used for testing, or other cases when the
* caller knows it has exclusive control of the terminal.
*
* @param bb the buffer of bytes to inject
*/
void injectDisplayOutput(ByteBuffer bb);
/**
* @see #injectDisplayOutput(ByteBuffer)
*/
default void injectDisplayOutput(byte[] arr) {
injectDisplayOutput(ByteBuffer.wrap(arr));
}
/**
* Set the terminal size to the given dimensions, as do <em>not</em> resize it to the window.
*
* @param rows the number of rows
* @param cols the number of columns
*/
void setFixedSize(int rows, int cols);
/**
* Fit the terminals dimensions to the containing window.
*/
void setDynamicSize();
@Override
void close();
}

View file

@ -0,0 +1,119 @@
/* ###
* 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.services;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import ghidra.app.plugin.core.terminal.TerminalPlugin;
import ghidra.app.plugin.core.terminal.vt.VtOutput;
import ghidra.framework.plugintool.ServiceInfo;
/**
* A service that provides for the creation and management of DEC VT100 terminal emulators.
*
* <p>
* These are perhaps better described as XTerm clones. It seems the term "VT100" is applied to any
* text display that interprets some number of ANSI escape codes. While the XTerm documentation does
* a decent job of listing which VT version (or Tektronix, or whatever terminal) that introduced or
* specified each code/sequence in the last 6 or so decades, applications don't really seem to care
* about the details. You set {@code TERM=xterm}, and they just use whatever codes the feel like.
* Some make more conservative assumptions than others. For example, there is an escape sequence to
* insert a blank character, shifting the remaining characters in the line to the right. Despite
* using this, Bash (or perhaps Readline) will still re-send the remaining characters, just in case.
* It seems over the years, in an effort to be compatible with as many applications as possible,
* terminal emulators have implemented more and more escape codes, many of which were invented by
* XTerm, and some of which result from mis-reading documentation and/or replicating erroneous
* implementations.
*
* <p>
* Perhaps our interpretation of the history is jaded, and as we learn more, our implementation can
* become more disciplined, but as it stands, our {@link TerminalPlugin} takes the <em>ad hoc</em>
* approach: We've implemented the sequences we need to make it compatible with the applications we
* intend to run, hoping that the resulting feature set will work with many others. It will likely
* need patching to add missing features over its lifetime. We make extensive use of the
* <a href="https://invisible-island.net/xterm/ctlseqs/ctlseqs.html">XTerm control sequence
* documentation</a>, as well as the
* <a href="https://en.wikipedia.org/wiki/ANSI_escape_code">Wikipedia article on ANSI escape
* codes</a>. Where the documentation lacks specificity or otherwise seems incorrect, we experiment
* with a reference implementation to discern and replicate its behavior. The clearest way we know
* to do this is to run the {@code tty} command from the reference terminal to get its
* pseudo-terminal (pty) file name. Then, we use Python from a separate terminal to write test
* sequences to it and/or read sequences from it. We use the {@code sleep} command to prevent Bash
* from reading its own terminal. This same process is applied to test our implementation.
*
* <p>
* The applications we've tested with include, without regard to version:
* <ul>
* <li>{@code bash}</li>
* <li>{@code less}</li>
* <li>{@code vim}</li>
* <li>{@code gdb -tui}</li>
* <li>{@code termmines} (from our Debugger training exercises)</li>
* </ul>
*
* <p>
* Some known issues:
* <ul>
* <li>It seems Java does not provide all the key modifier information, esp., the meta key. Either
* that or Ghidra's intercepting them. Thus, we can't encode those modifiers.</li>
* <li>Many control sequences are not implemented. They're intentionally left to be implemented on
* an as-needed basis.</li>
* <li>We inherit many of the erroneous key encodings, e.g., for F1-F4, present in the reference
* implementation.</li>
* <li>Character sets are incomplete. The box/line drawing set is most important to us as it's used
* by {@code gdb -tui}. Historically, these charsets are used to encode international characters.
* Modern systems (and terminal emulators) support Unicode (though perhaps only UTF-8), but it's not
* obvious how that interacts with the legacy charset switching. It's also likely many applications,
* despite UTF-8 being available, will still use the legacy charset switching, esp., for box
* drawing. Furthermore, because it's tedious work to figure the mapping for every character in a
* charset, we've only cared to implement a portion of the box-drawing charset, and it's sorely
* incomplete.</li>
* </ul>
*/
@ServiceInfo(defaultProvider = TerminalPlugin.class)
public interface TerminalService {
/**
* Create a terminal not connected to any particular application.
*
* <p>
* To display application output, use {@link Terminal#injectDisplayOutput(java.nio.ByteBuffer)}.
* Application input is delivered to the given terminal output callback. If the application is
* connected via streams, esp., those from a pty, consider using
* {@link #createWithStreams(Charset, InputStream, OutputStream)}, instead.
*
* @param charset the character set for the terminal. See note in
* {@link #createWithStreams(Charset, InputStream, OutputStream)}.
* @param outputCb callback for output from the terminal, i.e., the application's input.
* @return the terminal
*/
Terminal createNullTerminal(Charset charset, VtOutput outputCb);
/**
* Create a terminal connected to the application (or pty session) via the given streams.
*
* @param charset the character set for the terminal. <b>NOTE:</b> Only US-ASCII and UTF-8 have
* been tested. So long as the bytes 0x00-0x7f map one-to-one with characters with
* the same code point, it'll probably work. Charsets that require more than one byte
* to decode those characters will almost certainly break things.
* @param in the application's output, i.e., input for the terminal to display.
* @param out the application's input, i.e., output from the terminal's keyboard and mouse.
* @return the terminal
*/
Terminal createWithStreams(Charset charset, InputStream in, OutputStream out);
}

View file

@ -16,6 +16,7 @@
package docking;
import java.awt.event.ActionEvent;
import java.util.List;
import javax.swing.AbstractAction;
import javax.swing.KeyStroke;
@ -24,12 +25,12 @@ import docking.action.DockingActionIf;
import docking.actions.KeyBindingUtils;
/**
* A class that can be used as an interface for using actions associated with keybindings. This
* A class that can be used as an interface for using actions associated with keybindings. This
* class is meant to only by used by internal Ghidra key event processing.
*/
public abstract class DockingKeyBindingAction extends AbstractAction {
private DockingActionIf docakbleAction;
private DockingActionIf dockingAction;
protected final KeyStroke keyStroke;
protected final Tool tool;
@ -37,7 +38,7 @@ public abstract class DockingKeyBindingAction extends AbstractAction {
public DockingKeyBindingAction(Tool tool, DockingActionIf action, KeyStroke keyStroke) {
super(KeyBindingUtils.parseKeyStroke(keyStroke));
this.tool = tool;
this.docakbleAction = action;
this.dockingAction = action;
this.keyStroke = keyStroke;
}
@ -63,7 +64,7 @@ public abstract class DockingKeyBindingAction extends AbstractAction {
ComponentProvider provider = tool.getActiveComponentProvider();
ActionContext context = getLocalContext(provider);
context.setSourceObject(e.getSource());
docakbleAction.actionPerformed(context);
dockingAction.actionPerformed(context);
}
protected ActionContext getLocalContext(ComponentProvider localProvider) {
@ -78,4 +79,8 @@ public abstract class DockingKeyBindingAction extends AbstractAction {
return new DefaultActionContext(localProvider, null);
}
public List<DockingActionIf> getActions() {
return List.of(dockingAction);
}
}

View file

@ -705,11 +705,13 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
placeholderManager.removeComponent(provider);
}
//==================================================================================================
// Package-level Action Methods
//==================================================================================================
Iterator<DockingActionIf> getComponentActions(ComponentProvider provider) {
/**
* Get the local actions installed on the given provider
*
* @param provider the provider
* @return an iterator over the actions
*/
public Iterator<DockingActionIf> getComponentActions(ComponentProvider provider) {
ComponentPlaceholder placeholder = getActivePlaceholder(provider);
if (placeholder != null) {
return placeholder.getActions();
@ -719,6 +721,10 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
return emptyList.iterator();
}
//==================================================================================================
// Package-level Action Methods
//==================================================================================================
void removeProviderAction(ComponentProvider provider, DockingActionIf action) {
ComponentPlaceholder placeholder = getActivePlaceholder(provider);
if (placeholder != null) {

View file

@ -318,6 +318,7 @@ public class MultipleKeyAction extends DockingKeyBindingAction {
return dwm.getActiveWindow();
}
@Override
public List<DockingActionIf> getActions() {
List<DockingActionIf> list = new ArrayList<>(actions.size());
for (ActionData actionData : actions) {

View file

@ -57,7 +57,7 @@ public class IndexedScrollPane extends JPanel implements IndexScrollListener {
}
/**
* Sets this scroll pane to never show scroll bars. This is useful when you want a container
* Sets this scroll pane to never show scroll bars. This is useful when you want a container
* whose view is always as big as the component in this scroll pane.
*/
public void setNeverScroll(boolean b) {
@ -67,6 +67,20 @@ public class IndexedScrollPane extends JPanel implements IndexScrollListener {
useViewSizeAsPreferredSize = b;
}
/**
* @see JScrollPane#setVerticalScrollBarPolicy(int)
*/
public void setVerticalScrollBarPolicy(int policy) {
scrollPane.setVerticalScrollBarPolicy(policy);
}
/**
* @see JScrollPane#setHorizontalScrollBarPolicy(int)
*/
public void setHorizontalScrollBarPolicy(int policy) {
scrollPane.setHorizontalScrollBarPolicy(policy);
}
private ViewToIndexMapper createIndexMapper() {
if (neverScroll) {
return new PreMappedViewToIndexMapper(scrollable);
@ -215,6 +229,7 @@ public class IndexedScrollPane extends JPanel implements IndexScrollListener {
return false;
}
@Override
public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation,
int direction) {
@ -296,8 +311,8 @@ public class IndexedScrollPane extends JPanel implements IndexScrollListener {
/**
* Sets whether the scroll wheel triggers scrolling <b>when over the scroll pane</b> of this
* class. When disabled, scrolling will still work when over the component inside of
* this class, but not when over the scroll bar.
* class. When disabled, scrolling will still work when over the component inside of this class,
* but not when over the scroll bar.
*
* @param enabled true to enable
*/

View file

@ -322,7 +322,7 @@ public abstract class ThemeManager {
FontValue font = currentValues.getFont(id);
if (font == null) {
error("No color value registered for: '" + id + "'");
error("No font value registered for: '" + id + "'");
return DEFAULT_FONT;
}
return font.get(currentValues);

View file

View file

@ -0,0 +1,30 @@
/* ###
* 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.
*/
import org.gradle.plugins.ide.eclipse.model.Container;
apply from: "$rootProject.projectDir/gradle/distributableGhidraModule.gradle"
apply from: "$rootProject.projectDir/gradle/javaProject.gradle"
apply from: "$rootProject.projectDir/gradle/jacocoProject.gradle"
apply from: "$rootProject.projectDir/gradle/javaTestProject.gradle"
apply from: "$rootProject.projectDir/gradle/javadoc.gradle"
apply plugin: 'eclipse'
eclipse.project.name = 'Framework Pty'
dependencies {
api project(':Framework-Debugging')
api "com.jcraft:jsch:0.1.55"
}

View file

@ -0,0 +1,4 @@
##VERSION: 2.0
##MODULE IP: Apache License 2.0
Module.manifest||GHIDRA||||END|
data/gui.palette.theme.properties||GHIDRA||||END|

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty;
package ghidra.pty;
import java.io.IOException;

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty;
package ghidra.pty;
import java.io.IOException;
import java.util.*;

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty;
package ghidra.pty;
import java.io.InputStream;
import java.io.OutputStream;

View file

@ -13,14 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty;
package ghidra.pty;
import java.io.IOException;
import agent.gdb.pty.linux.LinuxPtyFactory;
import agent.gdb.pty.macos.MacosPtyFactory;
import agent.gdb.pty.windows.ConPtyFactory;
import ghidra.framework.OperatingSystem;
import ghidra.pty.linux.LinuxPtyFactory;
import ghidra.pty.macos.MacosPtyFactory;
import ghidra.pty.windows.ConPtyFactory;
/**
* A mechanism for opening pseudo-terminals

View file

@ -13,10 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty;
package ghidra.pty;
/**
* The parent (UNIX "master") end of a pseudo-terminal
*/
public interface PtyParent extends PtyEndpoint {
void setWindowSize(int cols, int rows);
}

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty;
package ghidra.pty;
/**
* A session led by the child pty

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty.linux;
package ghidra.pty.linux;
import com.sun.jna.LastErrorException;
import com.sun.jna.Native;

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty.linux;
package ghidra.pty.linux;
import java.io.IOException;
import java.io.InputStream;

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty.linux;
package ghidra.pty.linux;
import java.io.IOException;
import java.io.OutputStream;

View file

@ -13,14 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty.linux;
package ghidra.pty.linux;
import java.io.IOException;
import com.sun.jna.*;
import com.sun.jna.ptr.IntByReference;
import agent.gdb.pty.Pty;
import ghidra.pty.Pty;
import ghidra.util.Msg;
public class LinuxPty implements Pty {

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty.linux;
package ghidra.pty.linux;
import java.io.*;
import java.net.URL;
@ -21,10 +21,10 @@ import java.net.URLDecoder;
import java.nio.file.Paths;
import java.util.*;
import agent.gdb.pty.PtyChild;
import agent.gdb.pty.PtySession;
import agent.gdb.pty.linux.PosixC.Termios;
import agent.gdb.pty.local.LocalProcessPtySession;
import ghidra.pty.PtyChild;
import ghidra.pty.PtySession;
import ghidra.pty.linux.PosixC.Termios;
import ghidra.pty.local.LocalProcessPtySession;
import ghidra.util.Msg;
public class LinuxPtyChild extends LinuxPtyEndpoint implements PtyChild {

View file

@ -13,12 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty.linux;
package ghidra.pty.linux;
import java.io.InputStream;
import java.io.OutputStream;
import agent.gdb.pty.PtyEndpoint;
import ghidra.pty.PtyEndpoint;
public class LinuxPtyEndpoint implements PtyEndpoint {
protected final int fd;

View file

@ -13,12 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty.linux;
package ghidra.pty.linux;
import java.io.IOException;
import agent.gdb.pty.Pty;
import agent.gdb.pty.PtyFactory;
import ghidra.pty.Pty;
import ghidra.pty.PtyFactory;
public class LinuxPtyFactory implements PtyFactory {
@Override

View file

@ -13,12 +13,26 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty.linux;
package ghidra.pty.linux;
import agent.gdb.pty.PtyParent;
import ghidra.pty.PtyParent;
import ghidra.pty.linux.PosixC.Winsize;
public class LinuxPtyParent extends LinuxPtyEndpoint implements PtyParent {
LinuxPtyParent(int fd) {
super(fd);
}
@Override
public void setWindowSize(int cols, int rows) {
if (cols > 0xffff || rows > 0xffff) {
throw new IllegalArgumentException(
"Dimensions limited to unsigned shorts. Got cols=" + cols + ",rows=" + rows);
}
Winsize.ByReference ws = new Winsize.ByReference();
ws.ws_col = (short) cols;
ws.ws_row = (short) rows;
ws.write();
PosixC.INSTANCE.ioctl(fd, Winsize.TIOCSWINSZ, ws.getPointer());
}
}

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty.linux;
package ghidra.pty.linux;
import java.util.List;

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty.linux;
package ghidra.pty.linux;
import com.sun.jna.*;
import com.sun.jna.Structure.FieldOrder;
@ -26,6 +26,19 @@ import com.sun.jna.Structure.FieldOrder;
*/
public interface PosixC extends Library {
@FieldOrder({ "ws_row", "ws_col", "ws_xpixel", "ws_ypixel" })
class Winsize extends Structure {
public static final int TIOCSWINSZ = 0x5414; // This may actually be Linux-specific
public short ws_row;
public short ws_col;
public short ws_xpixel; // Unused
public short ws_ypixel; // Unused
public static class ByReference extends Winsize implements Structure.ByReference {
}
}
@FieldOrder({ "c_iflag", "c_oflag", "c_cflag", "c_lflag", "c_line", "c_cc", "c_ispeed",
"c_ospeed" })
class Termios extends Structure {
@ -96,6 +109,11 @@ public interface PosixC extends Library {
return Err.checkLt0(BARE.execv(path, argv));
}
@Override
public int ioctl(int fd, int cmd, Pointer... args) {
return Err.checkLt0(BARE.ioctl(fd, cmd, args));
}
@Override
public int tcgetattr(int fd, Termios.ByReference termios_p) {
return Err.checkLt0(BARE.tcgetattr(fd, termios_p));
@ -123,6 +141,8 @@ public interface PosixC extends Library {
int execv(String path, String[] argv);
int ioctl(int fd, int cmd, Pointer... args);
int tcgetattr(int fd, Termios.ByReference termios_p);
int tcsetattr(int fd, int optional_actions, Termios.ByReference termios_p);

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty.linux;
package ghidra.pty.linux;
import com.sun.jna.*;
import com.sun.jna.ptr.IntByReference;

View file

@ -13,9 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty.local;
package ghidra.pty.local;
import agent.gdb.pty.PtySession;
import ghidra.pty.PtySession;
import ghidra.util.Msg;
/**

View file

@ -13,15 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty.local;
package ghidra.pty.local;
import com.sun.jna.LastErrorException;
import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.platform.win32.WinBase;
import com.sun.jna.ptr.IntByReference;
import agent.gdb.pty.PtySession;
import agent.gdb.pty.windows.Handle;
import ghidra.pty.PtySession;
import ghidra.pty.windows.Handle;
import ghidra.util.Msg;
public class LocalWindowsNativeProcessPtySession implements PtySession {

View file

@ -13,13 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty.macos;
package ghidra.pty.macos;
import java.io.IOException;
import agent.gdb.pty.Pty;
import agent.gdb.pty.PtyFactory;
import agent.gdb.pty.linux.LinuxPty;
import ghidra.pty.Pty;
import ghidra.pty.PtyFactory;
import ghidra.pty.linux.LinuxPty;
public class MacosPtyFactory implements PtyFactory {
@Override

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty.ssh;
package ghidra.pty.ssh;
import java.io.IOException;
import java.util.Objects;
@ -26,9 +26,9 @@ import org.apache.commons.text.StringEscapeUtils;
import com.jcraft.jsch.*;
import com.jcraft.jsch.ConfigRepository.Config;
import agent.gdb.pty.PtyFactory;
import docking.DockingWindowManager;
import docking.widgets.PasswordDialog;
import ghidra.pty.PtyFactory;
import ghidra.util.Msg;
import ghidra.util.StringUtilities;

View file

@ -13,34 +13,41 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty.ssh;
package ghidra.pty.ssh;
import java.io.*;
import com.jcraft.jsch.*;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSchException;
import agent.gdb.pty.*;
import ghidra.pty.*;
public class SshPty implements Pty {
private final ChannelExec channel;
private final OutputStream out;
private final InputStream in;
private final SshPtyParent parent;
private final SshPtyChild child;
public SshPty(ChannelExec channel) throws JSchException, IOException {
this.channel = channel;
out = channel.getOutputStream();
in = channel.getInputStream();
parent = new SshPtyParent(channel, out, in);
child = new SshPtyChild(channel, out, in);
}
@Override
public PtyParent getParent() {
return new SshPtyParent(out, in);
return parent;
}
@Override
public PtyChild getChild() {
return new SshPtyChild(channel, out, in);
return child;
}
@Override

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty.ssh;
package ghidra.pty.ssh;
import java.io.*;
import java.util.*;
@ -22,18 +22,15 @@ import java.util.stream.Collectors;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSchException;
import agent.gdb.pty.PtyChild;
import ghidra.dbg.util.ShellUtils;
import ghidra.pty.PtyChild;
import ghidra.util.Msg;
public class SshPtyChild extends SshPtyEndpoint implements PtyChild {
private final ChannelExec channel;
private String name;
public SshPtyChild(ChannelExec channel, OutputStream outputStream, InputStream inputStream) {
super(outputStream, inputStream);
this.channel = channel;
super(channel, outputStream, inputStream);
}
private String sttyString(Collection<TermMode> mode) {

View file

@ -13,18 +13,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty.ssh;
package ghidra.pty.ssh;
import java.io.InputStream;
import java.io.OutputStream;
import agent.gdb.pty.PtyEndpoint;
import com.jcraft.jsch.ChannelExec;
import ghidra.pty.PtyEndpoint;
public class SshPtyEndpoint implements PtyEndpoint {
protected final ChannelExec channel;
protected final OutputStream outputStream;
protected final InputStream inputStream;
public SshPtyEndpoint(OutputStream outputStream, InputStream inputStream) {
public SshPtyEndpoint(ChannelExec channel, OutputStream outputStream, InputStream inputStream) {
this.channel = channel;
this.outputStream = outputStream;
this.inputStream = inputStream;
}

View file

@ -13,15 +13,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty.ssh;
package ghidra.pty.ssh;
import java.io.InputStream;
import java.io.OutputStream;
import agent.gdb.pty.PtyParent;
import com.jcraft.jsch.ChannelExec;
import ghidra.pty.PtyParent;
public class SshPtyParent extends SshPtyEndpoint implements PtyParent {
public SshPtyParent(OutputStream outputStream, InputStream inputStream) {
super(outputStream, inputStream);
public SshPtyParent(ChannelExec channel, OutputStream outputStream, InputStream inputStream) {
super(channel, outputStream, inputStream);
}
@Override
public void setWindowSize(int cols, int rows) {
channel.setPtySize(cols, rows, 0, 0);
}
}

View file

@ -13,11 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty.ssh;
package ghidra.pty.ssh;
import com.jcraft.jsch.Channel;
import agent.gdb.pty.PtySession;
import ghidra.pty.PtySession;
public class SshPtySession implements PtySession {

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty.windows;
package ghidra.pty.windows;
import java.io.*;
import java.nio.ByteBuffer;

View file

@ -13,18 +13,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty.windows;
package ghidra.pty.windows;
import java.io.IOException;
import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.platform.win32.WinDef.DWORD;
import com.sun.jna.platform.win32.WinNT.HANDLEByReference;
import com.sun.jna.platform.win32.COM.COMUtils;
import agent.gdb.pty.*;
import agent.gdb.pty.windows.jna.ConsoleApiNative;
import agent.gdb.pty.windows.jna.ConsoleApiNative.COORD;
import ghidra.pty.*;
import ghidra.pty.windows.jna.ConsoleApiNative;
import ghidra.pty.windows.jna.ConsoleApiNative.COORD;
import com.sun.jna.platform.win32.COM.COMUtils;
public class ConPty implements Pty {
static final DWORD DW_ZERO = new DWORD(0);

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty.windows;
package ghidra.pty.windows;
import java.io.IOException;
import java.util.*;
@ -25,11 +25,11 @@ import com.sun.jna.platform.win32.WinBase.PROCESS_INFORMATION;
import com.sun.jna.platform.win32.WinDef.*;
import com.sun.jna.platform.win32.WinNT.HANDLE;
import agent.gdb.pty.PtyChild;
import agent.gdb.pty.local.LocalWindowsNativeProcessPtySession;
import agent.gdb.pty.windows.jna.ConsoleApiNative;
import agent.gdb.pty.windows.jna.ConsoleApiNative.STARTUPINFOEX;
import ghidra.dbg.util.ShellUtils;
import ghidra.pty.PtyChild;
import ghidra.pty.local.LocalWindowsNativeProcessPtySession;
import ghidra.pty.windows.jna.ConsoleApiNative;
import ghidra.pty.windows.jna.ConsoleApiNative.STARTUPINFOEX;
public class ConPtyChild extends ConPtyEndpoint implements PtyChild {
private final Handle pseudoConsoleHandle;

View file

@ -13,12 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty.windows;
package ghidra.pty.windows;
import java.io.InputStream;
import java.io.OutputStream;
import agent.gdb.pty.PtyEndpoint;
import ghidra.pty.PtyEndpoint;
public class ConPtyEndpoint implements PtyEndpoint {
protected InputStream inputStream;

View file

@ -13,12 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty.windows;
package ghidra.pty.windows;
import java.io.IOException;
import agent.gdb.pty.Pty;
import agent.gdb.pty.PtyFactory;
import ghidra.pty.Pty;
import ghidra.pty.PtyFactory;
public class ConPtyFactory implements PtyFactory {
@Override

View file

@ -13,12 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty.windows;
package ghidra.pty.windows;
import agent.gdb.pty.PtyParent;
import ghidra.pty.PtyParent;
import ghidra.util.Msg;
public class ConPtyParent extends ConPtyEndpoint implements PtyParent {
public ConPtyParent(Handle writeHandle, Handle readHandle) {
super(writeHandle, readHandle);
}
@Override
public void setWindowSize(int rows, int cols) {
Msg.error(this, "Pty window size not implemented on Windows");
}
}

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty.windows;
package ghidra.pty.windows;
import java.lang.ref.Cleaner;

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty.windows;
package ghidra.pty.windows;
import java.io.IOException;
import java.io.InputStream;

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty.windows;
package ghidra.pty.windows;
import java.io.IOException;
import java.io.OutputStream;

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty.windows;
package ghidra.pty.windows;
import com.sun.jna.LastErrorException;
import com.sun.jna.platform.win32.Kernel32;

View file

@ -13,11 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty.windows;
package ghidra.pty.windows;
import com.sun.jna.platform.win32.WinNT.HANDLE;
import agent.gdb.pty.windows.jna.ConsoleApiNative;
import ghidra.pty.windows.jna.ConsoleApiNative;
public class PseudoConsoleHandle extends Handle {

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty.windows.jna;
package ghidra.pty.windows.jna;
import java.util.List;

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty;
package ghidra.pty;
import static org.junit.Assert.assertEquals;

View file

@ -13,9 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty.linux;
package ghidra.pty.linux;
import static org.junit.Assert.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
import java.io.*;
@ -24,11 +25,11 @@ import java.util.*;
import org.junit.Before;
import org.junit.Test;
import agent.gdb.pty.AbstractPtyTest;
import agent.gdb.pty.PtyChild.Echo;
import agent.gdb.pty.PtySession;
import ghidra.dbg.testutil.DummyProc;
import ghidra.framework.OperatingSystem;
import ghidra.pty.AbstractPtyTest;
import ghidra.pty.PtyChild.Echo;
import ghidra.pty.PtySession;
public class LinuxPtyTest extends AbstractPtyTest {
@Before

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty.ssh;
package ghidra.pty.ssh;
import static org.junit.Assert.assertEquals;
import static org.junit.Assume.assumeFalse;
@ -23,9 +23,9 @@ import java.io.*;
import org.junit.Before;
import org.junit.Test;
import agent.gdb.pty.PtyChild.Echo;
import agent.gdb.pty.PtySession;
import ghidra.app.script.AskDialog;
import ghidra.pty.PtyChild.Echo;
import ghidra.pty.PtySession;
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
import ghidra.util.SystemUtilities;
import ghidra.util.exception.CancelledException;

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty.windows;
package ghidra.pty.windows;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
@ -27,9 +27,9 @@ import org.junit.Test;
import com.sun.jna.LastErrorException;
import agent.gdb.pty.*;
import ghidra.dbg.testutil.DummyProc;
import ghidra.framework.OperatingSystem;
import ghidra.pty.*;
public class ConPtyTest extends AbstractPtyTest {

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.pty.windows;
package ghidra.pty.windows;
import java.io.*;

View file

@ -0,0 +1,381 @@
/* ###
* 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.plugin.core.terminal;
import static org.junit.Assert.assertEquals;
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import org.junit.Test;
import docking.widgets.OkDialog;
import docking.widgets.fieldpanel.support.*;
import ghidra.app.plugin.core.clipboard.ClipboardPlugin;
import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest;
import ghidra.app.services.*;
import ghidra.framework.OperatingSystem;
import ghidra.pty.*;
import ghidra.util.SystemUtilities;
public class TerminalProviderTest extends AbstractGhidraHeadedDebuggerGUITest {
protected static byte[] ascii(String str) {
try {
return str.getBytes("US-ASCII");
}
catch (UnsupportedEncodingException e) {
throw new AssertionError(e);
}
}
protected static final byte[] TEST_CONTENTS = ascii("""
term Term\r
noterm\r
""");
TerminalService terminalService;
ClipboardService clipboardService;
@Test
@SuppressWarnings("resource")
public void testBash() throws Exception {
assumeFalse(SystemUtilities.isInTestingBatchMode());
assumeFalse(OperatingSystem.CURRENT_OPERATING_SYSTEM == OperatingSystem.WINDOWS);
terminalService = addPlugin(tool, TerminalPlugin.class);
clipboardService = addPlugin(tool, ClipboardPlugin.class);
PtyFactory factory = PtyFactory.local();
try (Pty pty = factory.openpty()) {
Map<String, String> env = new HashMap<>(System.getenv());
env.put("TERM", "xterm-256color");
PtySession session = pty.getChild().session(new String[] { "/usr/bin/bash" }, env);
PtyParent parent = pty.getParent();
try (Terminal term = terminalService.createWithStreams(Charset.forName("US-ASCII"),
parent.getInputStream(), parent.getOutputStream())) {
term.addTerminalListener(new TerminalListener() {
@Override
public void resized(int cols, int rows) {
parent.setWindowSize(cols, rows);
}
});
session.waitExited();
}
}
}
@Test
@SuppressWarnings("resource")
public void testCmd() throws Exception {
assumeFalse(SystemUtilities.isInTestingBatchMode());
assumeTrue(OperatingSystem.CURRENT_OPERATING_SYSTEM == OperatingSystem.WINDOWS);
terminalService = addPlugin(tool, TerminalPlugin.class);
clipboardService = addPlugin(tool, ClipboardPlugin.class);
PtyFactory factory = PtyFactory.local();
try (Pty pty = factory.openpty()) {
Map<String, String> env = new HashMap<>(System.getenv());
PtySession session =
pty.getChild().session(new String[] { "C:\\Windows\\cmd.exe" }, env);
PtyParent parent = pty.getParent();
try (Terminal term = terminalService.createWithStreams(Charset.forName("US-ASCII"),
parent.getInputStream(), parent.getOutputStream())) {
term.addTerminalListener(new TerminalListener() {
@Override
public void resized(int cols, int rows) {
parent.setWindowSize(cols, rows);
}
});
session.waitExited();
}
}
}
protected void assertSingleSelection(int row, int colStart, int colEnd, FieldSelection sel) {
assertEquals(1, sel.getNumRanges());
FieldRange range = sel.getFieldRange(0);
assertEquals(new FieldLocation(row, 0, 0, colStart), range.getStart());
assertEquals(new FieldLocation(row, 0, 0, colEnd), range.getEnd());
}
@Test
@SuppressWarnings("resource")
public void testFindSimple() throws Exception {
terminalService = addPlugin(tool, TerminalPlugin.class);
try (DefaultTerminal term = (DefaultTerminal) terminalService
.createNullTerminal(Charset.forName("US-ASCII"), buf -> {
})) {
term.setFixedSize(25, 80);
term.injectDisplayOutput(TEST_CONTENTS);
term.provider.findDialog.txtFind.setText("term");
performAction(term.provider.actionFindNext, false);
waitForPass(() -> assertSingleSelection(0, 0, 4,
term.provider.panel.fieldPanel.getSelection()));
performAction(term.provider.actionFindNext, false);
waitForPass(() -> assertSingleSelection(0, 5, 9,
term.provider.panel.fieldPanel.getSelection()));
performAction(term.provider.actionFindNext, false);
waitForPass(() -> assertSingleSelection(1, 2, 6,
term.provider.panel.fieldPanel.getSelection()));
performAction(term.provider.actionFindNext, false);
OkDialog dialog = waitForInfoDialog();
assertEquals("String not found", dialog.getMessage());
dialog.close();
}
}
@Test
@SuppressWarnings("resource")
public void testFindCaseSensitive() throws Exception {
terminalService = addPlugin(tool, TerminalPlugin.class);
try (DefaultTerminal term = (DefaultTerminal) terminalService
.createNullTerminal(Charset.forName("US-ASCII"), buf -> {
})) {
term.setFixedSize(25, 80);
term.injectDisplayOutput(TEST_CONTENTS);
term.provider.findDialog.txtFind.setText("term");
term.provider.findDialog.cbCaseSensitive.setSelected(true);
performAction(term.provider.actionFindNext, false);
waitForPass(() -> assertSingleSelection(0, 0, 4,
term.provider.panel.fieldPanel.getSelection()));
performAction(term.provider.actionFindNext, false);
waitForPass(() -> assertSingleSelection(1, 2, 6,
term.provider.panel.fieldPanel.getSelection()));
performAction(term.provider.actionFindNext, false);
OkDialog dialog = waitForInfoDialog();
assertEquals("String not found", dialog.getMessage());
dialog.close();
}
}
@Test
@SuppressWarnings("resource")
public void testFindWrap() throws Exception {
terminalService = addPlugin(tool, TerminalPlugin.class);
try (DefaultTerminal term = (DefaultTerminal) terminalService
.createNullTerminal(Charset.forName("US-ASCII"), buf -> {
})) {
term.setFixedSize(25, 80);
term.injectDisplayOutput(TEST_CONTENTS);
term.provider.findDialog.txtFind.setText("term");
term.provider.findDialog.cbWrapSearch.setSelected(true);
performAction(term.provider.actionFindNext, false);
waitForPass(() -> assertSingleSelection(0, 0, 4,
term.provider.panel.fieldPanel.getSelection()));
performAction(term.provider.actionFindNext, false);
waitForPass(() -> assertSingleSelection(0, 5, 9,
term.provider.panel.fieldPanel.getSelection()));
performAction(term.provider.actionFindNext, false);
waitForPass(() -> assertSingleSelection(1, 2, 6,
term.provider.panel.fieldPanel.getSelection()));
performAction(term.provider.actionFindNext, false);
waitForPass(() -> assertSingleSelection(0, 0, 4,
term.provider.panel.fieldPanel.getSelection()));
}
}
@Test
@SuppressWarnings("resource")
public void testFindWholeWord() throws Exception {
terminalService = addPlugin(tool, TerminalPlugin.class);
try (DefaultTerminal term = (DefaultTerminal) terminalService
.createNullTerminal(Charset.forName("US-ASCII"), buf -> {
})) {
term.setFixedSize(25, 80);
term.injectDisplayOutput(TEST_CONTENTS);
term.provider.findDialog.txtFind.setText("term");
term.provider.findDialog.cbWholeWord.setSelected(true);
performAction(term.provider.actionFindNext, false);
waitForPass(() -> assertSingleSelection(0, 0, 4,
term.provider.panel.fieldPanel.getSelection()));
performAction(term.provider.actionFindNext, false);
waitForPass(() -> assertSingleSelection(0, 5, 9,
term.provider.panel.fieldPanel.getSelection()));
performAction(term.provider.actionFindNext, false);
OkDialog dialog = waitForInfoDialog();
assertEquals("String not found", dialog.getMessage());
dialog.close();
}
}
@Test
@SuppressWarnings("resource")
public void testFindRegex() throws Exception {
terminalService = addPlugin(tool, TerminalPlugin.class);
try (DefaultTerminal term = (DefaultTerminal) terminalService
.createNullTerminal(Charset.forName("US-ASCII"), buf -> {
})) {
term.setFixedSize(25, 80);
term.injectDisplayOutput(TEST_CONTENTS);
term.provider.findDialog.txtFind.setText("o?term");
term.provider.findDialog.cbRegex.setSelected(true);
performAction(term.provider.actionFindNext, false);
waitForPass(() -> assertSingleSelection(0, 0, 4,
term.provider.panel.fieldPanel.getSelection()));
performAction(term.provider.actionFindNext, false);
waitForPass(() -> assertSingleSelection(0, 5, 9,
term.provider.panel.fieldPanel.getSelection()));
performAction(term.provider.actionFindNext, false);
waitForPass(() -> assertSingleSelection(1, 1, 6,
term.provider.panel.fieldPanel.getSelection()));
// NB. the o is optional, so it finds a subrange of the previous result
performAction(term.provider.actionFindNext, false);
waitForPass(() -> assertSingleSelection(1, 2, 6,
term.provider.panel.fieldPanel.getSelection()));
performAction(term.provider.actionFindNext, false);
OkDialog dialog = waitForInfoDialog();
assertEquals("String not found", dialog.getMessage());
dialog.close();
}
}
@Test
@SuppressWarnings("resource")
public void testFindPrevious() throws Exception {
terminalService = addPlugin(tool, TerminalPlugin.class);
try (DefaultTerminal term = (DefaultTerminal) terminalService
.createNullTerminal(Charset.forName("US-ASCII"), buf -> {
})) {
term.setFixedSize(25, 80);
term.injectDisplayOutput(TEST_CONTENTS);
term.provider.findDialog.txtFind.setText("term");
performAction(term.provider.actionFindPrevious, false);
waitForPass(() -> assertSingleSelection(1, 2, 6,
term.provider.panel.fieldPanel.getSelection()));
performAction(term.provider.actionFindPrevious, false);
waitForPass(() -> assertSingleSelection(0, 5, 9,
term.provider.panel.fieldPanel.getSelection()));
performAction(term.provider.actionFindPrevious, false);
waitForPass(() -> assertSingleSelection(0, 0, 4,
term.provider.panel.fieldPanel.getSelection()));
performAction(term.provider.actionFindPrevious, false);
OkDialog dialog = waitForInfoDialog();
assertEquals("String not found", dialog.getMessage());
dialog.close();
}
}
@Test
@SuppressWarnings("resource")
public void testFindPreviousWrap() throws Exception {
terminalService = addPlugin(tool, TerminalPlugin.class);
try (DefaultTerminal term = (DefaultTerminal) terminalService
.createNullTerminal(Charset.forName("US-ASCII"), buf -> {
})) {
term.setFixedSize(25, 80);
term.injectDisplayOutput(TEST_CONTENTS);
term.provider.findDialog.txtFind.setText("term");
term.provider.findDialog.cbWrapSearch.setSelected(true);
performAction(term.provider.actionFindPrevious, false);
waitForPass(() -> assertSingleSelection(1, 2, 6,
term.provider.panel.fieldPanel.getSelection()));
performAction(term.provider.actionFindPrevious, false);
waitForPass(() -> assertSingleSelection(0, 5, 9,
term.provider.panel.fieldPanel.getSelection()));
performAction(term.provider.actionFindPrevious, false);
waitForPass(() -> assertSingleSelection(0, 0, 4,
term.provider.panel.fieldPanel.getSelection()));
performAction(term.provider.actionFindPrevious, false);
waitForPass(() -> assertSingleSelection(1, 2, 6,
term.provider.panel.fieldPanel.getSelection()));
}
}
@Test
@SuppressWarnings("resource")
public void testFindPreviousRegex() throws Exception {
terminalService = addPlugin(tool, TerminalPlugin.class);
try (DefaultTerminal term = (DefaultTerminal) terminalService
.createNullTerminal(Charset.forName("US-ASCII"), buf -> {
})) {
term.setFixedSize(25, 80);
term.injectDisplayOutput(TEST_CONTENTS);
term.provider.findDialog.txtFind.setText("o?term");
term.provider.findDialog.cbRegex.setSelected(true);
// NB. the o is optional, so it finds a subrange of the next result
performAction(term.provider.actionFindPrevious, false);
waitForPass(() -> assertSingleSelection(1, 2, 6,
term.provider.panel.fieldPanel.getSelection()));
performAction(term.provider.actionFindPrevious, false);
waitForPass(() -> assertSingleSelection(1, 1, 6,
term.provider.panel.fieldPanel.getSelection()));
performAction(term.provider.actionFindPrevious, false);
waitForPass(() -> assertSingleSelection(0, 5, 9,
term.provider.panel.fieldPanel.getSelection()));
performAction(term.provider.actionFindPrevious, false);
waitForPass(() -> assertSingleSelection(0, 0, 4,
term.provider.panel.fieldPanel.getSelection()));
performAction(term.provider.actionFindPrevious, false);
OkDialog dialog = waitForInfoDialog();
assertEquals("String not found", dialog.getMessage());
dialog.close();
}
}
}