mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-04 18:29:37 +02:00
GP-1977: Introduce Terminal Service and Plugin
This commit is contained in:
parent
bafded084e
commit
482341f6b1
98 changed files with 7972 additions and 141 deletions
|
@ -29,7 +29,7 @@ dependencies {
|
||||||
api project(':Framework-AsyncComm')
|
api project(':Framework-AsyncComm')
|
||||||
api project(':Framework-Debugging')
|
api project(':Framework-Debugging')
|
||||||
api project(':Debugger-gadp')
|
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-AsyncComm', configuration: 'testArtifacts')
|
||||||
testImplementation project(path: ':Framework-Debugging', 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.manager.GdbManager;
|
||||||
import agent.gdb.model.impl.GdbModelImpl;
|
import agent.gdb.model.impl.GdbModelImpl;
|
||||||
import agent.gdb.pty.PtyFactory;
|
|
||||||
import ghidra.dbg.DebuggerModelFactory;
|
import ghidra.dbg.DebuggerModelFactory;
|
||||||
import ghidra.dbg.DebuggerObjectModel;
|
import ghidra.dbg.DebuggerObjectModel;
|
||||||
import ghidra.dbg.util.ConfigurableFactory.FactoryDescription;
|
import ghidra.dbg.util.ConfigurableFactory.FactoryDescription;
|
||||||
import ghidra.dbg.util.ShellUtils;
|
import ghidra.dbg.util.ShellUtils;
|
||||||
import ghidra.program.model.listing.Program;
|
import ghidra.program.model.listing.Program;
|
||||||
|
import ghidra.pty.PtyFactory;
|
||||||
|
|
||||||
@FactoryDescription(
|
@FactoryDescription(
|
||||||
brief = "gdb",
|
brief = "gdb",
|
||||||
|
|
|
@ -19,12 +19,12 @@ import java.util.List;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
import agent.gdb.model.impl.GdbModelImpl;
|
import agent.gdb.model.impl.GdbModelImpl;
|
||||||
import agent.gdb.pty.ssh.GhidraSshPtyFactory;
|
|
||||||
import ghidra.dbg.DebuggerModelFactory;
|
import ghidra.dbg.DebuggerModelFactory;
|
||||||
import ghidra.dbg.DebuggerObjectModel;
|
import ghidra.dbg.DebuggerObjectModel;
|
||||||
import ghidra.dbg.util.ShellUtils;
|
import ghidra.dbg.util.ShellUtils;
|
||||||
import ghidra.dbg.util.ConfigurableFactory.FactoryDescription;
|
import ghidra.dbg.util.ConfigurableFactory.FactoryDescription;
|
||||||
import ghidra.program.model.listing.Program;
|
import ghidra.program.model.listing.Program;
|
||||||
|
import ghidra.pty.ssh.GhidraSshPtyFactory;
|
||||||
|
|
||||||
@FactoryDescription(
|
@FactoryDescription(
|
||||||
brief = "gdb via SSH",
|
brief = "gdb via SSH",
|
||||||
|
|
|
@ -21,8 +21,8 @@ import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
import agent.gdb.gadp.GdbGadpServer;
|
import agent.gdb.gadp.GdbGadpServer;
|
||||||
import agent.gdb.model.impl.GdbModelImpl;
|
import agent.gdb.model.impl.GdbModelImpl;
|
||||||
import agent.gdb.pty.PtyFactory;
|
|
||||||
import ghidra.dbg.gadp.server.AbstractGadpServer;
|
import ghidra.dbg.gadp.server.AbstractGadpServer;
|
||||||
|
import ghidra.pty.PtyFactory;
|
||||||
|
|
||||||
public class GdbGadpServerImpl implements GdbGadpServer {
|
public class GdbGadpServerImpl implements GdbGadpServer {
|
||||||
public class GadpSide extends AbstractGadpServer {
|
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.GdbBreakpointInfo;
|
||||||
import agent.gdb.manager.breakpoint.GdbBreakpointInsertions;
|
import agent.gdb.manager.breakpoint.GdbBreakpointInsertions;
|
||||||
import agent.gdb.manager.impl.GdbManagerImpl;
|
import agent.gdb.manager.impl.GdbManagerImpl;
|
||||||
import agent.gdb.pty.PtyFactory;
|
import ghidra.pty.PtyFactory;
|
||||||
import agent.gdb.pty.linux.LinuxPty;
|
import ghidra.pty.linux.LinuxPty;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The controlling side of a GDB session, using GDB/MI, usually via a pseudo-terminal
|
* 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;
|
||||||
import agent.gdb.manager.parsing.GdbMiParser.GdbMiFieldList;
|
import agent.gdb.manager.parsing.GdbMiParser.GdbMiFieldList;
|
||||||
import agent.gdb.manager.parsing.GdbParsingUtils.GdbParseError;
|
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.GhidraApplicationLayout;
|
||||||
import ghidra.async.*;
|
import ghidra.async.*;
|
||||||
import ghidra.async.AsyncLock.Hold;
|
import ghidra.async.AsyncLock.Hold;
|
||||||
|
@ -51,6 +48,9 @@ import ghidra.dbg.util.HandlerMap;
|
||||||
import ghidra.dbg.util.PrefixMap;
|
import ghidra.dbg.util.PrefixMap;
|
||||||
import ghidra.framework.OperatingSystem;
|
import ghidra.framework.OperatingSystem;
|
||||||
import ghidra.lifecycle.Internal;
|
import ghidra.lifecycle.Internal;
|
||||||
|
import ghidra.pty.*;
|
||||||
|
import ghidra.pty.PtyChild.Echo;
|
||||||
|
import ghidra.pty.windows.AnsiBufferedInputStream;
|
||||||
import ghidra.util.Msg;
|
import ghidra.util.Msg;
|
||||||
import ghidra.util.SystemUtilities;
|
import ghidra.util.SystemUtilities;
|
||||||
import ghidra.util.datastruct.ListenerSet;
|
import ghidra.util.datastruct.ListenerSet;
|
||||||
|
|
|
@ -24,7 +24,6 @@ import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||||
|
|
||||||
import agent.gdb.manager.*;
|
import agent.gdb.manager.*;
|
||||||
import agent.gdb.manager.impl.cmd.GdbCommandError;
|
import agent.gdb.manager.impl.cmd.GdbCommandError;
|
||||||
import agent.gdb.pty.PtyFactory;
|
|
||||||
import ghidra.async.AsyncUtils;
|
import ghidra.async.AsyncUtils;
|
||||||
import ghidra.dbg.DebuggerModelClosedReason;
|
import ghidra.dbg.DebuggerModelClosedReason;
|
||||||
import ghidra.dbg.agent.AbstractDebuggerObjectModel;
|
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.AnnotatedSchemaContext;
|
||||||
import ghidra.dbg.target.schema.TargetObjectSchema;
|
import ghidra.dbg.target.schema.TargetObjectSchema;
|
||||||
import ghidra.program.model.address.*;
|
import ghidra.program.model.address.*;
|
||||||
|
import ghidra.pty.PtyFactory;
|
||||||
|
|
||||||
public class GdbModelImpl extends AbstractDebuggerObjectModel {
|
public class GdbModelImpl extends AbstractDebuggerObjectModel {
|
||||||
// TODO: Need some minimal memory modeling per architecture on the model/agent side.
|
// 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.*;
|
||||||
import agent.gdb.manager.GdbManager.StepCmd;
|
import agent.gdb.manager.GdbManager.StepCmd;
|
||||||
import agent.gdb.manager.breakpoint.GdbBreakpointInfo;
|
import agent.gdb.manager.breakpoint.GdbBreakpointInfo;
|
||||||
import agent.gdb.pty.PtyFactory;
|
|
||||||
import agent.gdb.pty.linux.LinuxPtyFactory;
|
|
||||||
import generic.ULongSpan;
|
import generic.ULongSpan;
|
||||||
import generic.ULongSpan.ULongSpanSet;
|
import generic.ULongSpan.ULongSpanSet;
|
||||||
import ghidra.async.AsyncReference;
|
import ghidra.async.AsyncReference;
|
||||||
import ghidra.dbg.testutil.DummyProc;
|
import ghidra.dbg.testutil.DummyProc;
|
||||||
|
import ghidra.pty.PtyFactory;
|
||||||
|
import ghidra.pty.linux.LinuxPtyFactory;
|
||||||
import ghidra.test.AbstractGhidraHeadlessIntegrationTest;
|
import ghidra.test.AbstractGhidraHeadlessIntegrationTest;
|
||||||
import ghidra.util.Msg;
|
import ghidra.util.Msg;
|
||||||
import ghidra.util.SystemUtilities;
|
import ghidra.util.SystemUtilities;
|
||||||
|
|
|
@ -22,8 +22,8 @@ import java.util.concurrent.CompletableFuture;
|
||||||
import org.junit.Ignore;
|
import org.junit.Ignore;
|
||||||
|
|
||||||
import agent.gdb.manager.GdbManager;
|
import agent.gdb.manager.GdbManager;
|
||||||
import agent.gdb.pty.PtySession;
|
import ghidra.pty.PtySession;
|
||||||
import agent.gdb.pty.linux.LinuxPty;
|
import ghidra.pty.linux.LinuxPty;
|
||||||
import ghidra.util.Msg;
|
import ghidra.util.Msg;
|
||||||
|
|
||||||
@Ignore("Need compatible GDB version for CI")
|
@Ignore("Need compatible GDB version for CI")
|
||||||
|
|
|
@ -21,8 +21,8 @@ import java.util.concurrent.CompletableFuture;
|
||||||
import org.junit.Ignore;
|
import org.junit.Ignore;
|
||||||
|
|
||||||
import agent.gdb.manager.GdbManager;
|
import agent.gdb.manager.GdbManager;
|
||||||
import agent.gdb.pty.PtyFactory;
|
import ghidra.pty.PtyFactory;
|
||||||
import agent.gdb.pty.windows.ConPtyFactory;
|
import ghidra.pty.windows.ConPtyFactory;
|
||||||
|
|
||||||
@Ignore("Need compatible version on CI")
|
@Ignore("Need compatible version on CI")
|
||||||
public class SpawnedWindowsMi2GdbManagerTest extends AbstractGdbManagerTest {
|
public class SpawnedWindowsMi2GdbManagerTest extends AbstractGdbManagerTest {
|
||||||
|
|
|
@ -18,9 +18,9 @@ package agent.gdb.model.ssh;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import agent.gdb.GdbOverSshDebuggerModelFactory;
|
import agent.gdb.GdbOverSshDebuggerModelFactory;
|
||||||
import agent.gdb.pty.ssh.SshPtyTest;
|
|
||||||
import ghidra.dbg.DebuggerModelFactory;
|
import ghidra.dbg.DebuggerModelFactory;
|
||||||
import ghidra.dbg.test.AbstractModelHost;
|
import ghidra.dbg.test.AbstractModelHost;
|
||||||
|
import ghidra.pty.ssh.SshPtyTest;
|
||||||
import ghidra.util.exception.CancelledException;
|
import ghidra.util.exception.CancelledException;
|
||||||
|
|
||||||
public class SshGdbModelHost extends AbstractModelHost {
|
public class SshGdbModelHost extends AbstractModelHost {
|
||||||
|
|
|
@ -18,9 +18,9 @@ package agent.gdb.model.ssh;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import agent.gdb.GdbOverSshDebuggerModelFactory;
|
import agent.gdb.GdbOverSshDebuggerModelFactory;
|
||||||
import agent.gdb.pty.ssh.SshPtyTest;
|
|
||||||
import ghidra.dbg.DebuggerModelFactory;
|
import ghidra.dbg.DebuggerModelFactory;
|
||||||
import ghidra.dbg.test.AbstractModelHost;
|
import ghidra.dbg.test.AbstractModelHost;
|
||||||
|
import ghidra.pty.ssh.SshPtyTest;
|
||||||
import ghidra.util.exception.CancelledException;
|
import ghidra.util.exception.CancelledException;
|
||||||
|
|
||||||
public class SshJoinGdbModelHost extends AbstractModelHost {
|
public class SshJoinGdbModelHost extends AbstractModelHost {
|
||||||
|
|
|
@ -25,6 +25,7 @@ apply plugin: 'eclipse'
|
||||||
eclipse.project.name = 'Debug Debugger-rmi-trace'
|
eclipse.project.name = 'Debug Debugger-rmi-trace'
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
api project(':Pty')
|
||||||
api project(':Debugger')
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.7 = color.palette.cyan
|
||||||
color.fg.plugin.interpreter.renderer.color.intense.8 = color.palette.white
|
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.locationreferences.highlight = color.palette.lightcornflowerblue
|
||||||
|
|
||||||
color.bg.plugin.myprogramchangesdisplay.markers.changes.unsaved = color.palette.darkgray
|
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.tabs.list = SansSerif-BOLD-9
|
||||||
font.plugin.tips = Dialog-PLAIN-12
|
font.plugin.tips = Dialog-PLAIN-12
|
||||||
font.plugin.tips.label = font.plugin.tips[BOLD]
|
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
|
@Override
|
||||||
public void lostOwnership(Clipboard clipboard, Transferable contents) {
|
public void lostOwnership(Clipboard clipboard, Transferable contents) {
|
||||||
|
@ -353,11 +354,19 @@ public class ClipboardPlugin extends ProgramPlugin implements ClipboardOwner, Cl
|
||||||
// maker interface
|
// 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 class CopyAction extends DockingAction implements ICopy {
|
||||||
private final ClipboardContentProviderService clipboardService;
|
private final ClipboardContentProviderService clipboardService;
|
||||||
|
|
||||||
private CopyAction(ClipboardContentProviderService clipboardService) {
|
private CopyAction(ClipboardContentProviderService clipboardService) {
|
||||||
super("Copy", ClipboardPlugin.this.getName());
|
super("Copy", getActionOwner(clipboardService));
|
||||||
this.clipboardService = clipboardService;
|
this.clipboardService = clipboardService;
|
||||||
|
|
||||||
setPopupMenuData(new MenuData(new String[] { "Copy" }, "Clipboard"));
|
setPopupMenuData(new MenuData(new String[] { "Copy" }, "Clipboard"));
|
||||||
|
@ -365,6 +374,7 @@ public class ClipboardPlugin extends ProgramPlugin implements ClipboardOwner, Cl
|
||||||
"Clipboard"));
|
"Clipboard"));
|
||||||
setKeyBindingData(new KeyBindingData(KeyEvent.VK_C, InputEvent.CTRL_DOWN_MASK));
|
setKeyBindingData(new KeyBindingData(KeyEvent.VK_C, InputEvent.CTRL_DOWN_MASK));
|
||||||
setHelpLocation(new HelpLocation("ClipboardPlugin", "Copy"));
|
setHelpLocation(new HelpLocation("ClipboardPlugin", "Copy"));
|
||||||
|
clipboardService.customizeClipboardAction(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -390,7 +400,7 @@ public class ClipboardPlugin extends ProgramPlugin implements ClipboardOwner, Cl
|
||||||
private final ClipboardContentProviderService clipboardService;
|
private final ClipboardContentProviderService clipboardService;
|
||||||
|
|
||||||
private PasteAction(ClipboardContentProviderService clipboardService) {
|
private PasteAction(ClipboardContentProviderService clipboardService) {
|
||||||
super("Paste", ClipboardPlugin.this.getName());
|
super("Paste", ClipboardPlugin.this.getActionOwner(clipboardService));
|
||||||
this.clipboardService = clipboardService;
|
this.clipboardService = clipboardService;
|
||||||
|
|
||||||
setPopupMenuData(new MenuData(new String[] { "Paste" }, "Clipboard"));
|
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"));
|
new ToolBarData(new GIcon("icon.plugin.clipboard.paste"), "Clipboard"));
|
||||||
setKeyBindingData(new KeyBindingData(KeyEvent.VK_V, InputEvent.CTRL_DOWN_MASK));
|
setKeyBindingData(new KeyBindingData(KeyEvent.VK_V, InputEvent.CTRL_DOWN_MASK));
|
||||||
setHelpLocation(new HelpLocation("ClipboardPlugin", "Paste"));
|
setHelpLocation(new HelpLocation("ClipboardPlugin", "Paste"));
|
||||||
|
clipboardService.customizeClipboardAction(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -426,12 +437,13 @@ public class ClipboardPlugin extends ProgramPlugin implements ClipboardOwner, Cl
|
||||||
private final ClipboardContentProviderService clipboardService;
|
private final ClipboardContentProviderService clipboardService;
|
||||||
|
|
||||||
private CopySpecialAction(ClipboardContentProviderService clipboardService) {
|
private CopySpecialAction(ClipboardContentProviderService clipboardService) {
|
||||||
super("Copy Special", ClipboardPlugin.this.getName());
|
super("Copy Special", ClipboardPlugin.this.getActionOwner(clipboardService));
|
||||||
this.clipboardService = clipboardService;
|
this.clipboardService = clipboardService;
|
||||||
|
|
||||||
setPopupMenuData(new MenuData(new String[] { "Copy Special..." }, "Clipboard"));
|
setPopupMenuData(new MenuData(new String[] { "Copy Special..." }, "Clipboard"));
|
||||||
setEnabled(false);
|
setEnabled(false);
|
||||||
setHelpLocation(new HelpLocation("ClipboardPlugin", "Copy_Special"));
|
setHelpLocation(new HelpLocation("ClipboardPlugin", "Copy_Special"));
|
||||||
|
clipboardService.customizeClipboardAction(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -457,12 +469,13 @@ public class ClipboardPlugin extends ProgramPlugin implements ClipboardOwner, Cl
|
||||||
private final ClipboardContentProviderService clipboardService;
|
private final ClipboardContentProviderService clipboardService;
|
||||||
|
|
||||||
private CopySpecialAgainAction(ClipboardContentProviderService clipboardService) {
|
private CopySpecialAgainAction(ClipboardContentProviderService clipboardService) {
|
||||||
super("Copy Special Again", ClipboardPlugin.this.getName());
|
super("Copy Special Again", ClipboardPlugin.this.getActionOwner(clipboardService));
|
||||||
this.clipboardService = clipboardService;
|
this.clipboardService = clipboardService;
|
||||||
|
|
||||||
setPopupMenuData(new MenuData(new String[] { "Copy Special Again" }, "Clipboard"));
|
setPopupMenuData(new MenuData(new String[] { "Copy Special Again" }, "Clipboard"));
|
||||||
setEnabled(false);
|
setEnabled(false);
|
||||||
setHelpLocation(new HelpLocation("ClipboardPlugin", "Copy_Special"));
|
setHelpLocation(new HelpLocation("ClipboardPlugin", "Copy_Special"));
|
||||||
|
clipboardService.customizeClipboardAction(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -36,9 +36,6 @@ import ghidra.util.ColorUtils;
|
||||||
public class AnsiRenderer {
|
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
|
* 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.
|
* 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.ActionContext;
|
||||||
import docking.ComponentProvider;
|
import docking.ComponentProvider;
|
||||||
|
import docking.action.DockingAction;
|
||||||
import ghidra.app.util.ClipboardType;
|
import ghidra.app.util.ClipboardType;
|
||||||
import ghidra.util.task.TaskMonitor;
|
import ghidra.util.task.TaskMonitor;
|
||||||
|
|
||||||
|
@ -140,4 +141,35 @@ public interface ClipboardContentProviderService {
|
||||||
* @return true if copy special is enabled
|
* @return true if copy special is enabled
|
||||||
*/
|
*/
|
||||||
public boolean canCopySpecial();
|
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
|
* IP: GHIDRA
|
||||||
* REVIEWED: YES
|
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -17,6 +16,7 @@
|
||||||
package ghidra.app.services;
|
package ghidra.app.services;
|
||||||
|
|
||||||
public interface ClipboardService {
|
public interface ClipboardService {
|
||||||
public void registerClipboardContentProvider( ClipboardContentProviderService service );
|
public void registerClipboardContentProvider(ClipboardContentProviderService service);
|
||||||
public void deRegisterClipboardContentProvider( 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;
|
package docking;
|
||||||
|
|
||||||
import java.awt.event.ActionEvent;
|
import java.awt.event.ActionEvent;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import javax.swing.AbstractAction;
|
import javax.swing.AbstractAction;
|
||||||
import javax.swing.KeyStroke;
|
import javax.swing.KeyStroke;
|
||||||
|
@ -29,7 +30,7 @@ import docking.actions.KeyBindingUtils;
|
||||||
*/
|
*/
|
||||||
public abstract class DockingKeyBindingAction extends AbstractAction {
|
public abstract class DockingKeyBindingAction extends AbstractAction {
|
||||||
|
|
||||||
private DockingActionIf docakbleAction;
|
private DockingActionIf dockingAction;
|
||||||
|
|
||||||
protected final KeyStroke keyStroke;
|
protected final KeyStroke keyStroke;
|
||||||
protected final Tool tool;
|
protected final Tool tool;
|
||||||
|
@ -37,7 +38,7 @@ public abstract class DockingKeyBindingAction extends AbstractAction {
|
||||||
public DockingKeyBindingAction(Tool tool, DockingActionIf action, KeyStroke keyStroke) {
|
public DockingKeyBindingAction(Tool tool, DockingActionIf action, KeyStroke keyStroke) {
|
||||||
super(KeyBindingUtils.parseKeyStroke(keyStroke));
|
super(KeyBindingUtils.parseKeyStroke(keyStroke));
|
||||||
this.tool = tool;
|
this.tool = tool;
|
||||||
this.docakbleAction = action;
|
this.dockingAction = action;
|
||||||
this.keyStroke = keyStroke;
|
this.keyStroke = keyStroke;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,7 +64,7 @@ public abstract class DockingKeyBindingAction extends AbstractAction {
|
||||||
ComponentProvider provider = tool.getActiveComponentProvider();
|
ComponentProvider provider = tool.getActiveComponentProvider();
|
||||||
ActionContext context = getLocalContext(provider);
|
ActionContext context = getLocalContext(provider);
|
||||||
context.setSourceObject(e.getSource());
|
context.setSourceObject(e.getSource());
|
||||||
docakbleAction.actionPerformed(context);
|
dockingAction.actionPerformed(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected ActionContext getLocalContext(ComponentProvider localProvider) {
|
protected ActionContext getLocalContext(ComponentProvider localProvider) {
|
||||||
|
@ -78,4 +79,8 @@ public abstract class DockingKeyBindingAction extends AbstractAction {
|
||||||
|
|
||||||
return new DefaultActionContext(localProvider, null);
|
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);
|
placeholderManager.removeComponent(provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
//==================================================================================================
|
/**
|
||||||
// Package-level Action Methods
|
* Get the local actions installed on the given provider
|
||||||
//==================================================================================================
|
*
|
||||||
|
* @param provider the provider
|
||||||
Iterator<DockingActionIf> getComponentActions(ComponentProvider provider) {
|
* @return an iterator over the actions
|
||||||
|
*/
|
||||||
|
public Iterator<DockingActionIf> getComponentActions(ComponentProvider provider) {
|
||||||
ComponentPlaceholder placeholder = getActivePlaceholder(provider);
|
ComponentPlaceholder placeholder = getActivePlaceholder(provider);
|
||||||
if (placeholder != null) {
|
if (placeholder != null) {
|
||||||
return placeholder.getActions();
|
return placeholder.getActions();
|
||||||
|
@ -719,6 +721,10 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
||||||
return emptyList.iterator();
|
return emptyList.iterator();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//==================================================================================================
|
||||||
|
// Package-level Action Methods
|
||||||
|
//==================================================================================================
|
||||||
|
|
||||||
void removeProviderAction(ComponentProvider provider, DockingActionIf action) {
|
void removeProviderAction(ComponentProvider provider, DockingActionIf action) {
|
||||||
ComponentPlaceholder placeholder = getActivePlaceholder(provider);
|
ComponentPlaceholder placeholder = getActivePlaceholder(provider);
|
||||||
if (placeholder != null) {
|
if (placeholder != null) {
|
||||||
|
|
|
@ -318,6 +318,7 @@ public class MultipleKeyAction extends DockingKeyBindingAction {
|
||||||
return dwm.getActiveWindow();
|
return dwm.getActiveWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public List<DockingActionIf> getActions() {
|
public List<DockingActionIf> getActions() {
|
||||||
List<DockingActionIf> list = new ArrayList<>(actions.size());
|
List<DockingActionIf> list = new ArrayList<>(actions.size());
|
||||||
for (ActionData actionData : actions) {
|
for (ActionData actionData : actions) {
|
||||||
|
|
|
@ -67,6 +67,20 @@ public class IndexedScrollPane extends JPanel implements IndexScrollListener {
|
||||||
useViewSizeAsPreferredSize = b;
|
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() {
|
private ViewToIndexMapper createIndexMapper() {
|
||||||
if (neverScroll) {
|
if (neverScroll) {
|
||||||
return new PreMappedViewToIndexMapper(scrollable);
|
return new PreMappedViewToIndexMapper(scrollable);
|
||||||
|
@ -215,6 +229,7 @@ public class IndexedScrollPane extends JPanel implements IndexScrollListener {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation,
|
public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation,
|
||||||
int direction) {
|
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
|
* 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
|
* class. When disabled, scrolling will still work when over the component inside of this class,
|
||||||
* this class, but not when over the scroll bar.
|
* but not when over the scroll bar.
|
||||||
*
|
*
|
||||||
* @param enabled true to enable
|
* @param enabled true to enable
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -322,7 +322,7 @@ public abstract class ThemeManager {
|
||||||
FontValue font = currentValues.getFont(id);
|
FontValue font = currentValues.getFont(id);
|
||||||
|
|
||||||
if (font == null) {
|
if (font == null) {
|
||||||
error("No color value registered for: '" + id + "'");
|
error("No font value registered for: '" + id + "'");
|
||||||
return DEFAULT_FONT;
|
return DEFAULT_FONT;
|
||||||
}
|
}
|
||||||
return font.get(currentValues);
|
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
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.pty;
|
package ghidra.pty;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.pty;
|
package ghidra.pty;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.*;
|
import java.util.*;
|
|
@ -13,7 +13,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.pty;
|
package ghidra.pty;
|
||||||
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
|
@ -13,14 +13,14 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.pty;
|
package ghidra.pty;
|
||||||
|
|
||||||
import java.io.IOException;
|
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.framework.OperatingSystem;
|
||||||
|
import ghidra.pty.linux.LinuxPtyFactory;
|
||||||
|
import ghidra.pty.macos.MacosPtyFactory;
|
||||||
|
import ghidra.pty.windows.ConPtyFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A mechanism for opening pseudo-terminals
|
* A mechanism for opening pseudo-terminals
|
|
@ -13,10 +13,11 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.pty;
|
package ghidra.pty;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The parent (UNIX "master") end of a pseudo-terminal
|
* The parent (UNIX "master") end of a pseudo-terminal
|
||||||
*/
|
*/
|
||||||
public interface PtyParent extends PtyEndpoint {
|
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
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.pty;
|
package ghidra.pty;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A session led by the child pty
|
* A session led by the child pty
|
|
@ -13,7 +13,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.pty.linux;
|
package ghidra.pty.linux;
|
||||||
|
|
||||||
import com.sun.jna.LastErrorException;
|
import com.sun.jna.LastErrorException;
|
||||||
import com.sun.jna.Native;
|
import com.sun.jna.Native;
|
|
@ -13,7 +13,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.pty.linux;
|
package ghidra.pty.linux;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
|
@ -13,7 +13,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.pty.linux;
|
package ghidra.pty.linux;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
|
@ -13,14 +13,14 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.pty.linux;
|
package ghidra.pty.linux;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import com.sun.jna.*;
|
import com.sun.jna.*;
|
||||||
import com.sun.jna.ptr.IntByReference;
|
import com.sun.jna.ptr.IntByReference;
|
||||||
|
|
||||||
import agent.gdb.pty.Pty;
|
import ghidra.pty.Pty;
|
||||||
import ghidra.util.Msg;
|
import ghidra.util.Msg;
|
||||||
|
|
||||||
public class LinuxPty implements Pty {
|
public class LinuxPty implements Pty {
|
|
@ -13,7 +13,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.pty.linux;
|
package ghidra.pty.linux;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
@ -21,10 +21,10 @@ import java.net.URLDecoder;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
import agent.gdb.pty.PtyChild;
|
import ghidra.pty.PtyChild;
|
||||||
import agent.gdb.pty.PtySession;
|
import ghidra.pty.PtySession;
|
||||||
import agent.gdb.pty.linux.PosixC.Termios;
|
import ghidra.pty.linux.PosixC.Termios;
|
||||||
import agent.gdb.pty.local.LocalProcessPtySession;
|
import ghidra.pty.local.LocalProcessPtySession;
|
||||||
import ghidra.util.Msg;
|
import ghidra.util.Msg;
|
||||||
|
|
||||||
public class LinuxPtyChild extends LinuxPtyEndpoint implements PtyChild {
|
public class LinuxPtyChild extends LinuxPtyEndpoint implements PtyChild {
|
|
@ -13,12 +13,12 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.pty.linux;
|
package ghidra.pty.linux;
|
||||||
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
|
||||||
import agent.gdb.pty.PtyEndpoint;
|
import ghidra.pty.PtyEndpoint;
|
||||||
|
|
||||||
public class LinuxPtyEndpoint implements PtyEndpoint {
|
public class LinuxPtyEndpoint implements PtyEndpoint {
|
||||||
protected final int fd;
|
protected final int fd;
|
|
@ -13,12 +13,12 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.pty.linux;
|
package ghidra.pty.linux;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import agent.gdb.pty.Pty;
|
import ghidra.pty.Pty;
|
||||||
import agent.gdb.pty.PtyFactory;
|
import ghidra.pty.PtyFactory;
|
||||||
|
|
||||||
public class LinuxPtyFactory implements PtyFactory {
|
public class LinuxPtyFactory implements PtyFactory {
|
||||||
@Override
|
@Override
|
|
@ -13,12 +13,26 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* 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 {
|
public class LinuxPtyParent extends LinuxPtyEndpoint implements PtyParent {
|
||||||
LinuxPtyParent(int fd) {
|
LinuxPtyParent(int fd) {
|
||||||
super(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
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.pty.linux;
|
package ghidra.pty.linux;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.pty.linux;
|
package ghidra.pty.linux;
|
||||||
|
|
||||||
import com.sun.jna.*;
|
import com.sun.jna.*;
|
||||||
import com.sun.jna.Structure.FieldOrder;
|
import com.sun.jna.Structure.FieldOrder;
|
||||||
|
@ -26,6 +26,19 @@ import com.sun.jna.Structure.FieldOrder;
|
||||||
*/
|
*/
|
||||||
public interface PosixC extends Library {
|
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",
|
@FieldOrder({ "c_iflag", "c_oflag", "c_cflag", "c_lflag", "c_line", "c_cc", "c_ispeed",
|
||||||
"c_ospeed" })
|
"c_ospeed" })
|
||||||
class Termios extends Structure {
|
class Termios extends Structure {
|
||||||
|
@ -96,6 +109,11 @@ public interface PosixC extends Library {
|
||||||
return Err.checkLt0(BARE.execv(path, argv));
|
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
|
@Override
|
||||||
public int tcgetattr(int fd, Termios.ByReference termios_p) {
|
public int tcgetattr(int fd, Termios.ByReference termios_p) {
|
||||||
return Err.checkLt0(BARE.tcgetattr(fd, 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 execv(String path, String[] argv);
|
||||||
|
|
||||||
|
int ioctl(int fd, int cmd, Pointer... args);
|
||||||
|
|
||||||
int tcgetattr(int fd, Termios.ByReference termios_p);
|
int tcgetattr(int fd, Termios.ByReference termios_p);
|
||||||
|
|
||||||
int tcsetattr(int fd, int optional_actions, 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
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.pty.linux;
|
package ghidra.pty.linux;
|
||||||
|
|
||||||
import com.sun.jna.*;
|
import com.sun.jna.*;
|
||||||
import com.sun.jna.ptr.IntByReference;
|
import com.sun.jna.ptr.IntByReference;
|
|
@ -13,9 +13,9 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* 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;
|
import ghidra.util.Msg;
|
||||||
|
|
||||||
/**
|
/**
|
|
@ -13,15 +13,15 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.pty.local;
|
package ghidra.pty.local;
|
||||||
|
|
||||||
import com.sun.jna.LastErrorException;
|
import com.sun.jna.LastErrorException;
|
||||||
import com.sun.jna.platform.win32.Kernel32;
|
import com.sun.jna.platform.win32.Kernel32;
|
||||||
import com.sun.jna.platform.win32.WinBase;
|
import com.sun.jna.platform.win32.WinBase;
|
||||||
import com.sun.jna.ptr.IntByReference;
|
import com.sun.jna.ptr.IntByReference;
|
||||||
|
|
||||||
import agent.gdb.pty.PtySession;
|
import ghidra.pty.PtySession;
|
||||||
import agent.gdb.pty.windows.Handle;
|
import ghidra.pty.windows.Handle;
|
||||||
import ghidra.util.Msg;
|
import ghidra.util.Msg;
|
||||||
|
|
||||||
public class LocalWindowsNativeProcessPtySession implements PtySession {
|
public class LocalWindowsNativeProcessPtySession implements PtySession {
|
|
@ -13,13 +13,13 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.pty.macos;
|
package ghidra.pty.macos;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import agent.gdb.pty.Pty;
|
import ghidra.pty.Pty;
|
||||||
import agent.gdb.pty.PtyFactory;
|
import ghidra.pty.PtyFactory;
|
||||||
import agent.gdb.pty.linux.LinuxPty;
|
import ghidra.pty.linux.LinuxPty;
|
||||||
|
|
||||||
public class MacosPtyFactory implements PtyFactory {
|
public class MacosPtyFactory implements PtyFactory {
|
||||||
@Override
|
@Override
|
|
@ -13,7 +13,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.pty.ssh;
|
package ghidra.pty.ssh;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
@ -26,9 +26,9 @@ import org.apache.commons.text.StringEscapeUtils;
|
||||||
import com.jcraft.jsch.*;
|
import com.jcraft.jsch.*;
|
||||||
import com.jcraft.jsch.ConfigRepository.Config;
|
import com.jcraft.jsch.ConfigRepository.Config;
|
||||||
|
|
||||||
import agent.gdb.pty.PtyFactory;
|
|
||||||
import docking.DockingWindowManager;
|
import docking.DockingWindowManager;
|
||||||
import docking.widgets.PasswordDialog;
|
import docking.widgets.PasswordDialog;
|
||||||
|
import ghidra.pty.PtyFactory;
|
||||||
import ghidra.util.Msg;
|
import ghidra.util.Msg;
|
||||||
import ghidra.util.StringUtilities;
|
import ghidra.util.StringUtilities;
|
||||||
|
|
|
@ -13,34 +13,41 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.pty.ssh;
|
package ghidra.pty.ssh;
|
||||||
|
|
||||||
import java.io.*;
|
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 {
|
public class SshPty implements Pty {
|
||||||
private final ChannelExec channel;
|
private final ChannelExec channel;
|
||||||
private final OutputStream out;
|
private final OutputStream out;
|
||||||
private final InputStream in;
|
private final InputStream in;
|
||||||
|
|
||||||
|
private final SshPtyParent parent;
|
||||||
|
private final SshPtyChild child;
|
||||||
|
|
||||||
public SshPty(ChannelExec channel) throws JSchException, IOException {
|
public SshPty(ChannelExec channel) throws JSchException, IOException {
|
||||||
this.channel = channel;
|
this.channel = channel;
|
||||||
|
|
||||||
out = channel.getOutputStream();
|
out = channel.getOutputStream();
|
||||||
in = channel.getInputStream();
|
in = channel.getInputStream();
|
||||||
|
|
||||||
|
parent = new SshPtyParent(channel, out, in);
|
||||||
|
child = new SshPtyChild(channel, out, in);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PtyParent getParent() {
|
public PtyParent getParent() {
|
||||||
return new SshPtyParent(out, in);
|
return parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PtyChild getChild() {
|
public PtyChild getChild() {
|
||||||
return new SshPtyChild(channel, out, in);
|
return child;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
|
@ -13,7 +13,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.pty.ssh;
|
package ghidra.pty.ssh;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
@ -22,18 +22,15 @@ import java.util.stream.Collectors;
|
||||||
import com.jcraft.jsch.ChannelExec;
|
import com.jcraft.jsch.ChannelExec;
|
||||||
import com.jcraft.jsch.JSchException;
|
import com.jcraft.jsch.JSchException;
|
||||||
|
|
||||||
import agent.gdb.pty.PtyChild;
|
|
||||||
import ghidra.dbg.util.ShellUtils;
|
import ghidra.dbg.util.ShellUtils;
|
||||||
|
import ghidra.pty.PtyChild;
|
||||||
import ghidra.util.Msg;
|
import ghidra.util.Msg;
|
||||||
|
|
||||||
public class SshPtyChild extends SshPtyEndpoint implements PtyChild {
|
public class SshPtyChild extends SshPtyEndpoint implements PtyChild {
|
||||||
private final ChannelExec channel;
|
|
||||||
|
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
public SshPtyChild(ChannelExec channel, OutputStream outputStream, InputStream inputStream) {
|
public SshPtyChild(ChannelExec channel, OutputStream outputStream, InputStream inputStream) {
|
||||||
super(outputStream, inputStream);
|
super(channel, outputStream, inputStream);
|
||||||
this.channel = channel;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String sttyString(Collection<TermMode> mode) {
|
private String sttyString(Collection<TermMode> mode) {
|
|
@ -13,18 +13,22 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.pty.ssh;
|
package ghidra.pty.ssh;
|
||||||
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
|
||||||
import agent.gdb.pty.PtyEndpoint;
|
import com.jcraft.jsch.ChannelExec;
|
||||||
|
|
||||||
|
import ghidra.pty.PtyEndpoint;
|
||||||
|
|
||||||
public class SshPtyEndpoint implements PtyEndpoint {
|
public class SshPtyEndpoint implements PtyEndpoint {
|
||||||
|
protected final ChannelExec channel;
|
||||||
protected final OutputStream outputStream;
|
protected final OutputStream outputStream;
|
||||||
protected final InputStream inputStream;
|
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.outputStream = outputStream;
|
||||||
this.inputStream = inputStream;
|
this.inputStream = inputStream;
|
||||||
}
|
}
|
|
@ -13,15 +13,22 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.pty.ssh;
|
package ghidra.pty.ssh;
|
||||||
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
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 class SshPtyParent extends SshPtyEndpoint implements PtyParent {
|
||||||
public SshPtyParent(OutputStream outputStream, InputStream inputStream) {
|
public SshPtyParent(ChannelExec channel, OutputStream outputStream, InputStream inputStream) {
|
||||||
super(outputStream, 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
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.pty.ssh;
|
package ghidra.pty.ssh;
|
||||||
|
|
||||||
import com.jcraft.jsch.Channel;
|
import com.jcraft.jsch.Channel;
|
||||||
|
|
||||||
import agent.gdb.pty.PtySession;
|
import ghidra.pty.PtySession;
|
||||||
|
|
||||||
public class SshPtySession implements PtySession {
|
public class SshPtySession implements PtySession {
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.pty.windows;
|
package ghidra.pty.windows;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
|
@ -13,18 +13,19 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.pty.windows;
|
package ghidra.pty.windows;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import com.sun.jna.platform.win32.Kernel32;
|
import com.sun.jna.platform.win32.Kernel32;
|
||||||
import com.sun.jna.platform.win32.WinDef.DWORD;
|
import com.sun.jna.platform.win32.WinDef.DWORD;
|
||||||
import com.sun.jna.platform.win32.WinNT.HANDLEByReference;
|
import com.sun.jna.platform.win32.WinNT.HANDLEByReference;
|
||||||
import com.sun.jna.platform.win32.COM.COMUtils;
|
|
||||||
|
|
||||||
import agent.gdb.pty.*;
|
import ghidra.pty.*;
|
||||||
import agent.gdb.pty.windows.jna.ConsoleApiNative;
|
import ghidra.pty.windows.jna.ConsoleApiNative;
|
||||||
import agent.gdb.pty.windows.jna.ConsoleApiNative.COORD;
|
import ghidra.pty.windows.jna.ConsoleApiNative.COORD;
|
||||||
|
|
||||||
|
import com.sun.jna.platform.win32.COM.COMUtils;
|
||||||
|
|
||||||
public class ConPty implements Pty {
|
public class ConPty implements Pty {
|
||||||
static final DWORD DW_ZERO = new DWORD(0);
|
static final DWORD DW_ZERO = new DWORD(0);
|
|
@ -13,7 +13,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.pty.windows;
|
package ghidra.pty.windows;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.*;
|
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.WinDef.*;
|
||||||
import com.sun.jna.platform.win32.WinNT.HANDLE;
|
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.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 {
|
public class ConPtyChild extends ConPtyEndpoint implements PtyChild {
|
||||||
private final Handle pseudoConsoleHandle;
|
private final Handle pseudoConsoleHandle;
|
|
@ -13,12 +13,12 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.pty.windows;
|
package ghidra.pty.windows;
|
||||||
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
|
||||||
import agent.gdb.pty.PtyEndpoint;
|
import ghidra.pty.PtyEndpoint;
|
||||||
|
|
||||||
public class ConPtyEndpoint implements PtyEndpoint {
|
public class ConPtyEndpoint implements PtyEndpoint {
|
||||||
protected InputStream inputStream;
|
protected InputStream inputStream;
|
|
@ -13,12 +13,12 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.pty.windows;
|
package ghidra.pty.windows;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import agent.gdb.pty.Pty;
|
import ghidra.pty.Pty;
|
||||||
import agent.gdb.pty.PtyFactory;
|
import ghidra.pty.PtyFactory;
|
||||||
|
|
||||||
public class ConPtyFactory implements PtyFactory {
|
public class ConPtyFactory implements PtyFactory {
|
||||||
@Override
|
@Override
|
|
@ -13,12 +13,18 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* 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 class ConPtyParent extends ConPtyEndpoint implements PtyParent {
|
||||||
public ConPtyParent(Handle writeHandle, Handle readHandle) {
|
public ConPtyParent(Handle writeHandle, Handle readHandle) {
|
||||||
super(writeHandle, 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
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.pty.windows;
|
package ghidra.pty.windows;
|
||||||
|
|
||||||
import java.lang.ref.Cleaner;
|
import java.lang.ref.Cleaner;
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.pty.windows;
|
package ghidra.pty.windows;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
|
@ -13,7 +13,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.pty.windows;
|
package ghidra.pty.windows;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
|
@ -13,7 +13,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.pty.windows;
|
package ghidra.pty.windows;
|
||||||
|
|
||||||
import com.sun.jna.LastErrorException;
|
import com.sun.jna.LastErrorException;
|
||||||
import com.sun.jna.platform.win32.Kernel32;
|
import com.sun.jna.platform.win32.Kernel32;
|
|
@ -13,11 +13,11 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.pty.windows;
|
package ghidra.pty.windows;
|
||||||
|
|
||||||
import com.sun.jna.platform.win32.WinNT.HANDLE;
|
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 {
|
public class PseudoConsoleHandle extends Handle {
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.pty.windows.jna;
|
package ghidra.pty.windows.jna;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.pty;
|
package ghidra.pty;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
|
@ -13,9 +13,10 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* 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 static org.junit.Assume.assumeTrue;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
|
@ -24,11 +25,11 @@ import java.util.*;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
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.dbg.testutil.DummyProc;
|
||||||
import ghidra.framework.OperatingSystem;
|
import ghidra.framework.OperatingSystem;
|
||||||
|
import ghidra.pty.AbstractPtyTest;
|
||||||
|
import ghidra.pty.PtyChild.Echo;
|
||||||
|
import ghidra.pty.PtySession;
|
||||||
|
|
||||||
public class LinuxPtyTest extends AbstractPtyTest {
|
public class LinuxPtyTest extends AbstractPtyTest {
|
||||||
@Before
|
@Before
|
|
@ -13,7 +13,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.pty.ssh;
|
package ghidra.pty.ssh;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assume.assumeFalse;
|
import static org.junit.Assume.assumeFalse;
|
||||||
|
@ -23,9 +23,9 @@ import java.io.*;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import agent.gdb.pty.PtyChild.Echo;
|
|
||||||
import agent.gdb.pty.PtySession;
|
|
||||||
import ghidra.app.script.AskDialog;
|
import ghidra.app.script.AskDialog;
|
||||||
|
import ghidra.pty.PtyChild.Echo;
|
||||||
|
import ghidra.pty.PtySession;
|
||||||
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
|
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
|
||||||
import ghidra.util.SystemUtilities;
|
import ghidra.util.SystemUtilities;
|
||||||
import ghidra.util.exception.CancelledException;
|
import ghidra.util.exception.CancelledException;
|
|
@ -13,7 +13,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.pty.windows;
|
package ghidra.pty.windows;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
|
@ -27,9 +27,9 @@ import org.junit.Test;
|
||||||
|
|
||||||
import com.sun.jna.LastErrorException;
|
import com.sun.jna.LastErrorException;
|
||||||
|
|
||||||
import agent.gdb.pty.*;
|
|
||||||
import ghidra.dbg.testutil.DummyProc;
|
import ghidra.dbg.testutil.DummyProc;
|
||||||
import ghidra.framework.OperatingSystem;
|
import ghidra.framework.OperatingSystem;
|
||||||
|
import ghidra.pty.*;
|
||||||
|
|
||||||
public class ConPtyTest extends AbstractPtyTest {
|
public class ConPtyTest extends AbstractPtyTest {
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package agent.gdb.pty.windows;
|
package ghidra.pty.windows;
|
||||||
|
|
||||||
import java.io.*;
|
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…
Add table
Add a link
Reference in a new issue