mirror of
https://github.com/NationalSecurityAgency/ghidra
synced 2024-09-18 01:31:53 +00:00
GP-1977: Introduce Terminal Service and Plugin
This commit is contained in:
parent
bafded084e
commit
482341f6b1
|
@ -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')
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -25,6 +25,7 @@ apply plugin: 'eclipse'
|
|||
eclipse.project.name = 'Debug Debugger-rmi-trace'
|
||||
|
||||
dependencies {
|
||||
api project(':Pty')
|
||||
api project(':Debugger')
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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);
|
||||
|
|
0
Ghidra/Framework/Pty/Module.manifest
Normal file
0
Ghidra/Framework/Pty/Module.manifest
Normal file
30
Ghidra/Framework/Pty/build.gradle
Normal file
30
Ghidra/Framework/Pty/build.gradle
Normal 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"
|
||||
}
|
4
Ghidra/Framework/Pty/certification.manifest
Normal file
4
Ghidra/Framework/Pty/certification.manifest
Normal 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|
|
|
@ -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;
|
||||
|
|
@ -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.*;
|
|
@ -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;
|
|
@ -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
|
|
@ -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);
|
||||
}
|
|
@ -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
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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 {
|
|
@ -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 {
|
|
@ -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;
|
|
@ -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
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
@ -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);
|
|
@ -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;
|
|
@ -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;
|
||||
|
||||
/**
|
|
@ -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 {
|
|
@ -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
|
|
@ -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;
|
||||
|
|
@ -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
|
|
@ -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) {
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
@ -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;
|
|
@ -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);
|
|
@ -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;
|
|
@ -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;
|
|
@ -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
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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 {
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
|
@ -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;
|
|
@ -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 {
|
||||
|
|
@ -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.*;
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue