3275 lines
97 KiB
Java
3275 lines
97 KiB
Java
/*
|
|
* Copyright (C) 2007 The Android Open Source Project
|
|
*
|
|
* 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 com.android.term;
|
|
|
|
import java.io.FileDescriptor;
|
|
import java.io.FileInputStream;
|
|
import java.io.FileOutputStream;
|
|
import java.io.IOException;
|
|
import java.util.ArrayList;
|
|
|
|
import android.app.Activity;
|
|
import android.app.AlertDialog;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.content.SharedPreferences;
|
|
import android.content.res.Configuration;
|
|
import android.content.res.Resources;
|
|
import android.content.res.TypedArray;
|
|
import android.graphics.Bitmap;
|
|
import android.graphics.BitmapFactory;
|
|
import android.graphics.Canvas;
|
|
import android.graphics.ColorMatrixColorFilter;
|
|
import android.graphics.Paint;
|
|
import android.graphics.PorterDuff;
|
|
import android.graphics.PorterDuffXfermode;
|
|
import android.graphics.Rect;
|
|
import android.graphics.Typeface;
|
|
import android.net.Uri;
|
|
import android.os.Bundle;
|
|
import android.os.Handler;
|
|
import android.os.Message;
|
|
import android.preference.PreferenceManager;
|
|
import android.util.AttributeSet;
|
|
import android.util.DisplayMetrics;
|
|
import android.util.Log;
|
|
import android.view.GestureDetector;
|
|
import android.view.KeyEvent;
|
|
import android.view.Menu;
|
|
import android.view.MenuItem;
|
|
import android.view.MotionEvent;
|
|
import android.view.View;
|
|
import android.view.inputmethod.BaseInputConnection;
|
|
import android.view.inputmethod.CompletionInfo;
|
|
import android.view.inputmethod.EditorInfo;
|
|
import android.view.inputmethod.ExtractedText;
|
|
import android.view.inputmethod.ExtractedTextRequest;
|
|
import android.view.inputmethod.InputConnection;
|
|
|
|
/**
|
|
* A terminal emulator activity.
|
|
*/
|
|
|
|
public class Term extends Activity {
|
|
/**
|
|
* Set to true to add debugging code and logging.
|
|
*/
|
|
public static final boolean DEBUG = false;
|
|
|
|
/**
|
|
* Set to true to log each character received from the remote process to the
|
|
* android log, which makes it easier to debug some kinds of problems with
|
|
* emulating escape sequences and control codes.
|
|
*/
|
|
public static final boolean LOG_CHARACTERS_FLAG = DEBUG && false;
|
|
|
|
/**
|
|
* Set to true to log unknown escape sequences.
|
|
*/
|
|
public static final boolean LOG_UNKNOWN_ESCAPE_SEQUENCES = DEBUG && false;
|
|
|
|
/**
|
|
* The tag we use when logging, so that our messages can be distinguished
|
|
* from other messages in the log. Public because it's used by several
|
|
* classes.
|
|
*/
|
|
public static final String LOG_TAG = "Term";
|
|
|
|
/**
|
|
* Our main view. Displays the emulated terminal screen.
|
|
*/
|
|
private EmulatorView mEmulatorView;
|
|
|
|
/**
|
|
* The pseudo-teletype (pty) file descriptor that we use to communicate with
|
|
* another process, typically a shell.
|
|
*/
|
|
private FileDescriptor mTermFd;
|
|
|
|
/**
|
|
* Used to send data to the remote process.
|
|
*/
|
|
private FileOutputStream mTermOut;
|
|
|
|
/**
|
|
* A key listener that tracks the modifier keys and allows the full ASCII
|
|
* character set to be entered.
|
|
*/
|
|
private TermKeyListener mKeyListener;
|
|
|
|
/**
|
|
* The name of our emulator view in the view resource.
|
|
*/
|
|
private static final int EMULATOR_VIEW = R.id.emulatorView;
|
|
|
|
private int mFontSize = 9;
|
|
private int mColorId = 2;
|
|
private int mControlKeyId = 0;
|
|
|
|
private static final String FONTSIZE_KEY = "fontsize";
|
|
private static final String COLOR_KEY = "color";
|
|
private static final String CONTROLKEY_KEY = "controlkey";
|
|
private static final String SHELL_KEY = "shell";
|
|
private static final String INITIALCOMMAND_KEY = "initialcommand";
|
|
|
|
public static final int WHITE = 0xffffffff;
|
|
public static final int BLACK = 0xff000000;
|
|
public static final int BLUE = 0xff344ebd;
|
|
|
|
private static final int[][] COLOR_SCHEMES = {
|
|
{BLACK, WHITE}, {WHITE, BLACK}, {WHITE, BLUE}};
|
|
|
|
private static final int[] CONTROL_KEY_SCHEMES = {
|
|
KeyEvent.KEYCODE_DPAD_CENTER,
|
|
KeyEvent.KEYCODE_AT,
|
|
KeyEvent.KEYCODE_ALT_LEFT,
|
|
KeyEvent.KEYCODE_ALT_RIGHT
|
|
};
|
|
private static final String[] CONTROL_KEY_NAME = {
|
|
"Ball", "@", "Left-Alt", "Right-Alt"
|
|
};
|
|
|
|
private int mControlKeyCode;
|
|
|
|
private final static String DEFAULT_SHELL = "/system/bin/sh -";
|
|
private String mShell;
|
|
|
|
private final static String DEFAULT_INITIAL_COMMAND =
|
|
"export PATH=/data/local/bin:$PATH";
|
|
private String mInitialCommand;
|
|
|
|
private SharedPreferences mPrefs;
|
|
|
|
@Override
|
|
public void onCreate(Bundle icicle) {
|
|
super.onCreate(icicle);
|
|
Log.e(Term.LOG_TAG, "onCreate");
|
|
mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
|
|
readPrefs();
|
|
|
|
setContentView(R.layout.term_activity);
|
|
|
|
mEmulatorView = (EmulatorView) findViewById(EMULATOR_VIEW);
|
|
|
|
startListening();
|
|
|
|
mKeyListener = new TermKeyListener();
|
|
|
|
mEmulatorView.setFocusable(true);
|
|
mEmulatorView.setFocusableInTouchMode(true);
|
|
mEmulatorView.requestFocus();
|
|
mEmulatorView.register(mKeyListener);
|
|
|
|
updatePrefs();
|
|
}
|
|
|
|
@Override
|
|
public void onDestroy() {
|
|
super.onDestroy();
|
|
if (mTermFd != null) {
|
|
Exec.close(mTermFd);
|
|
mTermFd = null;
|
|
}
|
|
}
|
|
|
|
private void startListening() {
|
|
int[] processId = new int[1];
|
|
|
|
createSubprocess(processId);
|
|
final int procId = processId[0];
|
|
|
|
final Handler handler = new Handler() {
|
|
@Override
|
|
public void handleMessage(Message msg) {
|
|
}
|
|
};
|
|
|
|
Runnable watchForDeath = new Runnable() {
|
|
|
|
public void run() {
|
|
Log.i(Term.LOG_TAG, "waiting for: " + procId);
|
|
int result = Exec.waitFor(procId);
|
|
Log.i(Term.LOG_TAG, "Subprocess exited: " + result);
|
|
handler.sendEmptyMessage(result);
|
|
}
|
|
|
|
};
|
|
Thread watcher = new Thread(watchForDeath);
|
|
watcher.start();
|
|
|
|
mTermOut = new FileOutputStream(mTermFd);
|
|
|
|
mEmulatorView.initialize(mTermFd, mTermOut);
|
|
|
|
sendInitialCommand();
|
|
}
|
|
|
|
private void sendInitialCommand() {
|
|
String initialCommand = mInitialCommand;
|
|
if (initialCommand == null || initialCommand.equals("")) {
|
|
initialCommand = DEFAULT_INITIAL_COMMAND;
|
|
}
|
|
if (initialCommand.length() > 0) {
|
|
write(initialCommand + '\r');
|
|
}
|
|
}
|
|
|
|
private void restart() {
|
|
startActivity(getIntent());
|
|
finish();
|
|
}
|
|
|
|
private void write(String data) {
|
|
try {
|
|
mTermOut.write(data.getBytes());
|
|
mTermOut.flush();
|
|
} catch (IOException e) {
|
|
// Ignore exception
|
|
// We don't really care if the receiver isn't listening.
|
|
// We just make a best effort to answer the query.
|
|
}
|
|
}
|
|
|
|
private void createSubprocess(int[] processId) {
|
|
String shell = mShell;
|
|
if (shell == null || shell.equals("")) {
|
|
shell = DEFAULT_SHELL;
|
|
}
|
|
ArrayList<String> args = parse(shell);
|
|
String arg0 = args.get(0);
|
|
String arg1 = null;
|
|
String arg2 = null;
|
|
if (args.size() >= 2) {
|
|
arg1 = args.get(1);
|
|
}
|
|
if (args.size() >= 3) {
|
|
arg2 = args.get(2);
|
|
}
|
|
mTermFd = Exec.createSubprocess(arg0, arg1, arg2, processId);
|
|
}
|
|
|
|
private ArrayList<String> parse(String cmd) {
|
|
final int PLAIN = 0;
|
|
final int WHITESPACE = 1;
|
|
final int INQUOTE = 2;
|
|
int state = WHITESPACE;
|
|
ArrayList<String> result = new ArrayList<String>();
|
|
int cmdLen = cmd.length();
|
|
StringBuilder builder = new StringBuilder();
|
|
for (int i = 0; i < cmdLen; i++) {
|
|
char c = cmd.charAt(i);
|
|
if (state == PLAIN) {
|
|
if (Character.isWhitespace(c)) {
|
|
result.add(builder.toString());
|
|
builder.delete(0,builder.length());
|
|
state = WHITESPACE;
|
|
} else if (c == '"') {
|
|
state = INQUOTE;
|
|
} else {
|
|
builder.append(c);
|
|
}
|
|
} else if (state == WHITESPACE) {
|
|
if (Character.isWhitespace(c)) {
|
|
// do nothing
|
|
} else if (c == '"') {
|
|
state = INQUOTE;
|
|
} else {
|
|
state = PLAIN;
|
|
builder.append(c);
|
|
}
|
|
} else if (state == INQUOTE) {
|
|
if (c == '\\') {
|
|
if (i + 1 < cmdLen) {
|
|
i += 1;
|
|
builder.append(cmd.charAt(i));
|
|
}
|
|
} else if (c == '"') {
|
|
state = PLAIN;
|
|
} else {
|
|
builder.append(c);
|
|
}
|
|
}
|
|
}
|
|
if (builder.length() > 0) {
|
|
result.add(builder.toString());
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private void readPrefs() {
|
|
mFontSize = readIntPref(FONTSIZE_KEY, mFontSize, 20);
|
|
mColorId = readIntPref(COLOR_KEY, mColorId, COLOR_SCHEMES.length - 1);
|
|
mControlKeyId = readIntPref(CONTROLKEY_KEY, mControlKeyId,
|
|
CONTROL_KEY_SCHEMES.length - 1);
|
|
{
|
|
String newShell = readStringPref(SHELL_KEY, mShell);
|
|
if ((newShell == null) || ! newShell.equals(mShell)) {
|
|
if (mShell != null) {
|
|
Log.i(Term.LOG_TAG, "New shell set. Restarting.");
|
|
restart();
|
|
}
|
|
mShell = newShell;
|
|
}
|
|
}
|
|
{
|
|
String newInitialCommand = readStringPref(INITIALCOMMAND_KEY,
|
|
mInitialCommand);
|
|
if ((newInitialCommand == null)
|
|
|| ! newInitialCommand.equals(mInitialCommand)) {
|
|
if (mInitialCommand != null) {
|
|
Log.i(Term.LOG_TAG, "New initial command set. Restarting.");
|
|
restart();
|
|
}
|
|
mInitialCommand = newInitialCommand;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void updatePrefs() {
|
|
DisplayMetrics metrics = new DisplayMetrics();
|
|
getWindowManager().getDefaultDisplay().getMetrics(metrics);
|
|
mEmulatorView.setTextSize((int) (mFontSize * metrics.density));
|
|
setColors();
|
|
mControlKeyCode = CONTROL_KEY_SCHEMES[mControlKeyId];
|
|
}
|
|
|
|
private int readIntPref(String key, int defaultValue, int maxValue) {
|
|
int val;
|
|
try {
|
|
val = Integer.parseInt(
|
|
mPrefs.getString(key, Integer.toString(defaultValue)));
|
|
} catch (NumberFormatException e) {
|
|
val = defaultValue;
|
|
}
|
|
val = Math.max(0, Math.min(val, maxValue));
|
|
return val;
|
|
}
|
|
|
|
private String readStringPref(String key, String defaultValue) {
|
|
return mPrefs.getString(key, defaultValue);
|
|
}
|
|
|
|
@Override
|
|
public void onResume() {
|
|
super.onResume();
|
|
readPrefs();
|
|
updatePrefs();
|
|
}
|
|
|
|
@Override
|
|
public void onConfigurationChanged(Configuration newConfig) {
|
|
super.onConfigurationChanged(newConfig);
|
|
|
|
mEmulatorView.updateSize();
|
|
}
|
|
|
|
@Override
|
|
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
|
if (handleControlKey(keyCode, true)) {
|
|
return true;
|
|
} else if (isSystemKey(keyCode, event)) {
|
|
// Don't intercept the system keys
|
|
return super.onKeyDown(keyCode, event);
|
|
} else if (handleDPad(keyCode, true)) {
|
|
return true;
|
|
}
|
|
|
|
// Translate the keyCode into an ASCII character.
|
|
int letter = mKeyListener.keyDown(keyCode, event);
|
|
|
|
if (letter >= 0) {
|
|
try {
|
|
mTermOut.write(letter);
|
|
} catch (IOException e) {
|
|
// Ignore I/O exceptions
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
|
if (handleControlKey(keyCode, false)) {
|
|
return true;
|
|
} else if (isSystemKey(keyCode, event)) {
|
|
// Don't intercept the system keys
|
|
return super.onKeyUp(keyCode, event);
|
|
} else if (handleDPad(keyCode, false)) {
|
|
return true;
|
|
}
|
|
|
|
mKeyListener.keyUp(keyCode);
|
|
return true;
|
|
}
|
|
|
|
private boolean handleControlKey(int keyCode, boolean down) {
|
|
if (keyCode == mControlKeyCode) {
|
|
mKeyListener.handleControlKey(down);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Handle dpad left-right-up-down events. Don't handle
|
|
* dpad-center, that's our control key.
|
|
* @param keyCode
|
|
* @param down
|
|
*/
|
|
private boolean handleDPad(int keyCode, boolean down) {
|
|
if (keyCode < KeyEvent.KEYCODE_DPAD_UP ||
|
|
keyCode > KeyEvent.KEYCODE_DPAD_CENTER) {
|
|
return false;
|
|
}
|
|
|
|
if (down) {
|
|
try {
|
|
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
|
|
mTermOut.write('\r');
|
|
} else {
|
|
char code;
|
|
switch (keyCode) {
|
|
case KeyEvent.KEYCODE_DPAD_UP:
|
|
code = 'A';
|
|
break;
|
|
case KeyEvent.KEYCODE_DPAD_DOWN:
|
|
code = 'B';
|
|
break;
|
|
case KeyEvent.KEYCODE_DPAD_LEFT:
|
|
code = 'D';
|
|
break;
|
|
default:
|
|
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
|
code = 'C';
|
|
break;
|
|
}
|
|
mTermOut.write(27); // ESC
|
|
if (mEmulatorView.getKeypadApplicationMode()) {
|
|
mTermOut.write('O');
|
|
} else {
|
|
mTermOut.write('[');
|
|
}
|
|
mTermOut.write(code);
|
|
}
|
|
} catch (IOException e) {
|
|
// Ignore
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private boolean isSystemKey(int keyCode, KeyEvent event) {
|
|
return event.isSystem();
|
|
}
|
|
|
|
@Override
|
|
public boolean onCreateOptionsMenu(Menu menu) {
|
|
getMenuInflater().inflate(R.menu.main, menu);
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean onOptionsItemSelected(MenuItem item) {
|
|
int id = item.getItemId();
|
|
if (id == R.id.menu_preferences) {
|
|
doPreferences();
|
|
} else if (id == R.id.menu_reset) {
|
|
doResetTerminal();
|
|
} else if (id == R.id.menu_send_email) {
|
|
doEmailTranscript();
|
|
} else if (id == R.id.menu_special_keys) {
|
|
doDocumentKeys();
|
|
}
|
|
return super.onOptionsItemSelected(item);
|
|
}
|
|
|
|
private void doPreferences() {
|
|
startActivity(new Intent(this, TermPreferences.class));
|
|
}
|
|
|
|
private void setColors() {
|
|
int[] scheme = COLOR_SCHEMES[mColorId];
|
|
mEmulatorView.setColors(scheme[0], scheme[1]);
|
|
}
|
|
|
|
private void doResetTerminal() {
|
|
restart();
|
|
}
|
|
|
|
private void doEmailTranscript() {
|
|
// Don't really want to supply an address, but
|
|
// currently it's required, otherwise we get an
|
|
// exception.
|
|
String addr = "user@example.com";
|
|
Intent intent =
|
|
new Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:"
|
|
+ addr));
|
|
|
|
intent.putExtra("body", mEmulatorView.getTranscriptText());
|
|
startActivity(intent);
|
|
}
|
|
|
|
private void doDocumentKeys() {
|
|
String controlKey = CONTROL_KEY_NAME[mControlKeyId];
|
|
new AlertDialog.Builder(this).
|
|
setTitle("Press " + controlKey + " and Key").
|
|
setMessage(controlKey + " Space ==> Control-@ (NUL)\n"
|
|
+ controlKey + " A..Z ==> Control-A..Z\n"
|
|
+ controlKey + " 1 ==> Control-[ (ESC)\n"
|
|
+ controlKey + " 5 ==> Control-_\n"
|
|
+ controlKey + " . ==> Control-\\\n"
|
|
+ controlKey + " 0 ==> Control-]\n"
|
|
+ controlKey + " 6 ==> Control-^").
|
|
show();
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* An abstract screen interface. A terminal screen stores lines of text. (The
|
|
* reason to abstract it is to allow different implementations, and to hide
|
|
* implementation details from clients.)
|
|
*/
|
|
interface Screen {
|
|
|
|
/**
|
|
* Set line wrap flag for a given row. Affects how lines are logically
|
|
* wrapped when changing screen size or converting to a transcript.
|
|
*/
|
|
void setLineWrap(int row);
|
|
|
|
/**
|
|
* Store byte b into the screen at location (x, y)
|
|
*
|
|
* @param x X coordinate (also known as column)
|
|
* @param y Y coordinate (also known as row)
|
|
* @param b ASCII character to store
|
|
* @param foreColor the foreground color
|
|
* @param backColor the background color
|
|
*/
|
|
void set(int x, int y, byte b, int foreColor, int backColor);
|
|
|
|
/**
|
|
* Scroll the screen down one line. To scroll the whole screen of a 24 line
|
|
* screen, the arguments would be (0, 24).
|
|
*
|
|
* @param topMargin First line that is scrolled.
|
|
* @param bottomMargin One line after the last line that is scrolled.
|
|
*/
|
|
void scroll(int topMargin, int bottomMargin, int foreColor, int backColor);
|
|
|
|
/**
|
|
* Block copy characters from one position in the screen to another. The two
|
|
* positions can overlap. All characters of the source and destination must
|
|
* be within the bounds of the screen, or else an InvalidParemeterException
|
|
* will be thrown.
|
|
*
|
|
* @param sx source X coordinate
|
|
* @param sy source Y coordinate
|
|
* @param w width
|
|
* @param h height
|
|
* @param dx destination X coordinate
|
|
* @param dy destination Y coordinate
|
|
*/
|
|
void blockCopy(int sx, int sy, int w, int h, int dx, int dy);
|
|
|
|
/**
|
|
* Block set characters. All characters must be within the bounds of the
|
|
* screen, or else and InvalidParemeterException will be thrown. Typically
|
|
* this is called with a "val" argument of 32 to clear a block of
|
|
* characters.
|
|
*
|
|
* @param sx source X
|
|
* @param sy source Y
|
|
* @param w width
|
|
* @param h height
|
|
* @param val value to set.
|
|
* @param foreColor the foreground color
|
|
* @param backColor the background color
|
|
*/
|
|
void blockSet(int sx, int sy, int w, int h, int val, int foreColor, int
|
|
backColor);
|
|
|
|
/**
|
|
* Get the contents of the transcript buffer as a text string.
|
|
*
|
|
* @return the contents of the transcript buffer.
|
|
*/
|
|
String getTranscriptText();
|
|
|
|
/**
|
|
* Resize the screen
|
|
* @param columns
|
|
* @param rows
|
|
*/
|
|
void resize(int columns, int rows, int foreColor, int backColor);
|
|
}
|
|
|
|
|
|
/**
|
|
* A TranscriptScreen is a screen that remembers data that's been scrolled. The
|
|
* old data is stored in a ring buffer to minimize the amount of copying that
|
|
* needs to be done. The transcript does its own drawing, to avoid having to
|
|
* expose its internal data structures.
|
|
*/
|
|
class TranscriptScreen implements Screen {
|
|
|
|
/**
|
|
* The width of the transcript, in characters. Fixed at initialization.
|
|
*/
|
|
private int mColumns;
|
|
|
|
/**
|
|
* The total number of rows in the transcript and the screen. Fixed at
|
|
* initialization.
|
|
*/
|
|
private int mTotalRows;
|
|
|
|
/**
|
|
* The number of rows in the active portion of the transcript. Doesn't
|
|
* include the screen.
|
|
*/
|
|
private int mActiveTranscriptRows;
|
|
|
|
/**
|
|
* Which row is currently the topmost line of the transcript. Used to
|
|
* implement a circular buffer.
|
|
*/
|
|
private int mHead;
|
|
|
|
/**
|
|
* The number of active rows, includes both the transcript and the screen.
|
|
*/
|
|
private int mActiveRows;
|
|
|
|
/**
|
|
* The number of rows in the screen.
|
|
*/
|
|
private int mScreenRows;
|
|
|
|
/**
|
|
* The data for both the screen and the transcript. The first mScreenRows *
|
|
* mLineWidth characters are the screen, the rest are the transcript.
|
|
* The low byte encodes the ASCII character, the high byte encodes the
|
|
* foreground and background colors, plus underline and bold.
|
|
*/
|
|
private char[] mData;
|
|
|
|
/**
|
|
* The data's stored as color-encoded chars, but the drawing routines require chars, so we
|
|
* need a temporary buffer to hold a row's worth of characters.
|
|
*/
|
|
private char[] mRowBuffer;
|
|
|
|
/**
|
|
* Flags that keep track of whether the current line logically wraps to the
|
|
* next line. This is used when resizing the screen and when copying to the
|
|
* clipboard or an email attachment
|
|
*/
|
|
|
|
private boolean[] mLineWrap;
|
|
|
|
/**
|
|
* Create a transcript screen.
|
|
*
|
|
* @param columns the width of the screen in characters.
|
|
* @param totalRows the height of the entire text area, in rows of text.
|
|
* @param screenRows the height of just the screen, not including the
|
|
* transcript that holds lines that have scrolled off the top of the
|
|
* screen.
|
|
*/
|
|
public TranscriptScreen(int columns, int totalRows, int screenRows,
|
|
int foreColor, int backColor) {
|
|
init(columns, totalRows, screenRows, foreColor, backColor);
|
|
}
|
|
|
|
private void init(int columns, int totalRows, int screenRows, int foreColor, int backColor) {
|
|
mColumns = columns;
|
|
mTotalRows = totalRows;
|
|
mActiveTranscriptRows = 0;
|
|
mHead = 0;
|
|
mActiveRows = screenRows;
|
|
mScreenRows = screenRows;
|
|
int totalSize = columns * totalRows;
|
|
mData = new char[totalSize];
|
|
blockSet(0, 0, mColumns, mScreenRows, ' ', foreColor, backColor);
|
|
mRowBuffer = new char[columns];
|
|
mLineWrap = new boolean[totalRows];
|
|
consistencyCheck();
|
|
}
|
|
|
|
/**
|
|
* Convert a row value from the public external coordinate system to our
|
|
* internal private coordinate system. External coordinate system:
|
|
* -mActiveTranscriptRows to mScreenRows-1, with the screen being
|
|
* 0..mScreenRows-1 Internal coordinate system: 0..mScreenRows-1 rows of
|
|
* mData are the visible rows. mScreenRows..mActiveRows - 1 are the
|
|
* transcript, stored as a circular buffer.
|
|
*
|
|
* @param row a row in the external coordinate system.
|
|
* @return The row corresponding to the input argument in the private
|
|
* coordinate system.
|
|
*/
|
|
private int externalToInternalRow(int row) {
|
|
if (row < -mActiveTranscriptRows || row >= mScreenRows) {
|
|
throw new IllegalArgumentException();
|
|
}
|
|
if (row >= 0) {
|
|
return row; // This is a visible row.
|
|
}
|
|
return mScreenRows
|
|
+ ((mHead + mActiveTranscriptRows + row) % mActiveTranscriptRows);
|
|
}
|
|
|
|
private int getOffset(int externalLine) {
|
|
return externalToInternalRow(externalLine) * mColumns;
|
|
}
|
|
|
|
private int getOffset(int x, int y) {
|
|
return getOffset(y) + x;
|
|
}
|
|
|
|
public void setLineWrap(int row) {
|
|
mLineWrap[externalToInternalRow(row)] = true;
|
|
}
|
|
|
|
/**
|
|
* Store byte b into the screen at location (x, y)
|
|
*
|
|
* @param x X coordinate (also known as column)
|
|
* @param y Y coordinate (also known as row)
|
|
* @param b ASCII character to store
|
|
* @param foreColor the foreground color
|
|
* @param backColor the background color
|
|
*/
|
|
public void set(int x, int y, byte b, int foreColor, int backColor) {
|
|
mData[getOffset(x, y)] = encode(b, foreColor, backColor);
|
|
}
|
|
|
|
private char encode(int b, int foreColor, int backColor) {
|
|
return (char) ((foreColor << 12) | (backColor << 8) | b);
|
|
}
|
|
|
|
/**
|
|
* Scroll the screen down one line. To scroll the whole screen of a 24 line
|
|
* screen, the arguments would be (0, 24).
|
|
*
|
|
* @param topMargin First line that is scrolled.
|
|
* @param bottomMargin One line after the last line that is scrolled.
|
|
*/
|
|
public void scroll(int topMargin, int bottomMargin, int foreColor,
|
|
int backColor) {
|
|
if (topMargin > bottomMargin - 2 || topMargin > mScreenRows - 2
|
|
|| bottomMargin > mScreenRows) {
|
|
throw new IllegalArgumentException();
|
|
}
|
|
|
|
// Adjust the transcript so that the last line of the transcript
|
|
// is ready to receive the newly scrolled data
|
|
consistencyCheck();
|
|
int expansionRows = Math.min(1, mTotalRows - mActiveRows);
|
|
int rollRows = 1 - expansionRows;
|
|
mActiveRows += expansionRows;
|
|
mActiveTranscriptRows += expansionRows;
|
|
if (mActiveTranscriptRows > 0) {
|
|
mHead = (mHead + rollRows) % mActiveTranscriptRows;
|
|
}
|
|
consistencyCheck();
|
|
|
|
// Block move the scroll line to the transcript
|
|
int topOffset = getOffset(topMargin);
|
|
int destOffset = getOffset(-1);
|
|
System.arraycopy(mData, topOffset, mData, destOffset, mColumns);
|
|
|
|
int topLine = externalToInternalRow(topMargin);
|
|
int destLine = externalToInternalRow(-1);
|
|
System.arraycopy(mLineWrap, topLine, mLineWrap, destLine, 1);
|
|
|
|
// Block move the scrolled data up
|
|
int numScrollChars = (bottomMargin - topMargin - 1) * mColumns;
|
|
System.arraycopy(mData, topOffset + mColumns, mData, topOffset,
|
|
numScrollChars);
|
|
int numScrollLines = (bottomMargin - topMargin - 1);
|
|
System.arraycopy(mLineWrap, topLine + 1, mLineWrap, topLine,
|
|
numScrollLines);
|
|
|
|
// Erase the bottom line of the scroll region
|
|
blockSet(0, bottomMargin - 1, mColumns, 1, ' ', foreColor, backColor);
|
|
mLineWrap[externalToInternalRow(bottomMargin-1)] = false;
|
|
}
|
|
|
|
private void consistencyCheck() {
|
|
checkPositive(mColumns);
|
|
checkPositive(mTotalRows);
|
|
checkRange(0, mActiveTranscriptRows, mTotalRows);
|
|
if (mActiveTranscriptRows == 0) {
|
|
checkEqual(mHead, 0);
|
|
} else {
|
|
checkRange(0, mHead, mActiveTranscriptRows-1);
|
|
}
|
|
checkEqual(mScreenRows + mActiveTranscriptRows, mActiveRows);
|
|
checkRange(0, mScreenRows, mTotalRows);
|
|
|
|
checkEqual(mTotalRows, mLineWrap.length);
|
|
checkEqual(mTotalRows*mColumns, mData.length);
|
|
checkEqual(mColumns, mRowBuffer.length);
|
|
}
|
|
|
|
private void checkPositive(int n) {
|
|
if (n < 0) {
|
|
throw new IllegalArgumentException("checkPositive " + n);
|
|
}
|
|
}
|
|
|
|
private void checkRange(int a, int b, int c) {
|
|
if (a > b || b > c) {
|
|
throw new IllegalArgumentException("checkRange " + a + " <= " + b + " <= " + c);
|
|
}
|
|
}
|
|
|
|
private void checkEqual(int a, int b) {
|
|
if (a != b) {
|
|
throw new IllegalArgumentException("checkEqual " + a + " == " + b);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Block copy characters from one position in the screen to another. The two
|
|
* positions can overlap. All characters of the source and destination must
|
|
* be within the bounds of the screen, or else an InvalidParemeterException
|
|
* will be thrown.
|
|
*
|
|
* @param sx source X coordinate
|
|
* @param sy source Y coordinate
|
|
* @param w width
|
|
* @param h height
|
|
* @param dx destination X coordinate
|
|
* @param dy destination Y coordinate
|
|
*/
|
|
public void blockCopy(int sx, int sy, int w, int h, int dx, int dy) {
|
|
if (sx < 0 || sx + w > mColumns || sy < 0 || sy + h > mScreenRows
|
|
|| dx < 0 || dx + w > mColumns || dy < 0
|
|
|| dy + h > mScreenRows) {
|
|
throw new IllegalArgumentException();
|
|
}
|
|
if (sy <= dy) {
|
|
// Move in increasing order
|
|
for (int y = 0; y < h; y++) {
|
|
int srcOffset = getOffset(sx, sy + y);
|
|
int dstOffset = getOffset(dx, dy + y);
|
|
System.arraycopy(mData, srcOffset, mData, dstOffset, w);
|
|
}
|
|
} else {
|
|
// Move in decreasing order
|
|
for (int y = 0; y < h; y++) {
|
|
int y2 = h - (y + 1);
|
|
int srcOffset = getOffset(sx, sy + y2);
|
|
int dstOffset = getOffset(dx, dy + y2);
|
|
System.arraycopy(mData, srcOffset, mData, dstOffset, w);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Block set characters. All characters must be within the bounds of the
|
|
* screen, or else and InvalidParemeterException will be thrown. Typically
|
|
* this is called with a "val" argument of 32 to clear a block of
|
|
* characters.
|
|
*
|
|
* @param sx source X
|
|
* @param sy source Y
|
|
* @param w width
|
|
* @param h height
|
|
* @param val value to set.
|
|
*/
|
|
public void blockSet(int sx, int sy, int w, int h, int val,
|
|
int foreColor, int backColor) {
|
|
if (sx < 0 || sx + w > mColumns || sy < 0 || sy + h > mScreenRows) {
|
|
throw new IllegalArgumentException();
|
|
}
|
|
char[] data = mData;
|
|
char encodedVal = encode(val, foreColor, backColor);
|
|
for (int y = 0; y < h; y++) {
|
|
int offset = getOffset(sx, sy + y);
|
|
for (int x = 0; x < w; x++) {
|
|
data[offset + x] = encodedVal;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Draw a row of text. Out-of-bounds rows are blank, not errors.
|
|
*
|
|
* @param row The row of text to draw.
|
|
* @param canvas The canvas to draw to.
|
|
* @param x The x coordinate origin of the drawing
|
|
* @param y The y coordinate origin of the drawing
|
|
* @param renderer The renderer to use to draw the text
|
|
* @param cx the cursor X coordinate, -1 means don't draw it
|
|
*/
|
|
public final void drawText(int row, Canvas canvas, float x, float y,
|
|
TextRenderer renderer, int cx) {
|
|
|
|
// Out-of-bounds rows are blank.
|
|
if (row < -mActiveTranscriptRows || row >= mScreenRows) {
|
|
return;
|
|
}
|
|
|
|
// Copy the data from the byte array to a char array so they can
|
|
// be drawn.
|
|
|
|
int offset = getOffset(row);
|
|
char[] rowBuffer = mRowBuffer;
|
|
char[] data = mData;
|
|
int columns = mColumns;
|
|
int lastColors = 0;
|
|
int lastRunStart = -1;
|
|
final int CURSOR_MASK = 0x10000;
|
|
for (int i = 0; i < columns; i++) {
|
|
char c = data[offset + i];
|
|
int colors = (char) (c & 0xff00);
|
|
if (cx == i) {
|
|
// Set cursor background color:
|
|
colors |= CURSOR_MASK;
|
|
}
|
|
rowBuffer[i] = (char) (c & 0x00ff);
|
|
if (colors != lastColors) {
|
|
if (lastRunStart >= 0) {
|
|
renderer.drawTextRun(canvas, x, y, lastRunStart, rowBuffer,
|
|
lastRunStart, i - lastRunStart,
|
|
(lastColors & CURSOR_MASK) != 0,
|
|
0xf & (lastColors >> 12), 0xf & (lastColors >> 8));
|
|
}
|
|
lastColors = colors;
|
|
lastRunStart = i;
|
|
}
|
|
}
|
|
if (lastRunStart >= 0) {
|
|
renderer.drawTextRun(canvas, x, y, lastRunStart, rowBuffer,
|
|
lastRunStart, columns - lastRunStart,
|
|
(lastColors & CURSOR_MASK) != 0,
|
|
0xf & (lastColors >> 12), 0xf & (lastColors >> 8));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the count of active rows.
|
|
*
|
|
* @return the count of active rows.
|
|
*/
|
|
public int getActiveRows() {
|
|
return mActiveRows;
|
|
}
|
|
|
|
/**
|
|
* Get the count of active transcript rows.
|
|
*
|
|
* @return the count of active transcript rows.
|
|
*/
|
|
public int getActiveTranscriptRows() {
|
|
return mActiveTranscriptRows;
|
|
}
|
|
|
|
public String getTranscriptText() {
|
|
return internalGetTranscriptText(true);
|
|
}
|
|
|
|
private String internalGetTranscriptText(boolean stripColors) {
|
|
StringBuilder builder = new StringBuilder();
|
|
char[] rowBuffer = mRowBuffer;
|
|
char[] data = mData;
|
|
int columns = mColumns;
|
|
for (int row = -mActiveTranscriptRows; row < mScreenRows; row++) {
|
|
int offset = getOffset(row);
|
|
int lastPrintingChar = -1;
|
|
for (int column = 0; column < columns; column++) {
|
|
char c = data[offset + column];
|
|
if (stripColors) {
|
|
c = (char) (c & 0xff);
|
|
}
|
|
if ((c & 0xff) != ' ') {
|
|
lastPrintingChar = column;
|
|
}
|
|
rowBuffer[column] = c;
|
|
}
|
|
if (mLineWrap[externalToInternalRow(row)]) {
|
|
builder.append(rowBuffer, 0, columns);
|
|
} else {
|
|
builder.append(rowBuffer, 0, lastPrintingChar + 1);
|
|
builder.append('\n');
|
|
}
|
|
}
|
|
return builder.toString();
|
|
}
|
|
|
|
public void resize(int columns, int rows, int foreColor, int backColor) {
|
|
init(columns, mTotalRows, rows, foreColor, backColor);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Renders text into a screen. Contains all the terminal-specific knowlege and
|
|
* state. Emulates a subset of the X Window System xterm terminal, which in turn
|
|
* is an emulator for a subset of the Digital Equipment Corporation vt100
|
|
* terminal. Missing functionality: text attributes (bold, underline, reverse
|
|
* video, color) alternate screen cursor key and keypad escape sequences.
|
|
*/
|
|
class TerminalEmulator {
|
|
|
|
/**
|
|
* The cursor row. Numbered 0..mRows-1.
|
|
*/
|
|
private int mCursorRow;
|
|
|
|
/**
|
|
* The cursor column. Numbered 0..mColumns-1.
|
|
*/
|
|
private int mCursorCol;
|
|
|
|
/**
|
|
* The number of character rows in the terminal screen.
|
|
*/
|
|
private int mRows;
|
|
|
|
/**
|
|
* The number of character columns in the terminal screen.
|
|
*/
|
|
private int mColumns;
|
|
|
|
/**
|
|
* Used to send data to the remote process. Needed to implement the various
|
|
* "report" escape sequences.
|
|
*/
|
|
private FileOutputStream mTermOut;
|
|
|
|
/**
|
|
* Stores the characters that appear on the screen of the emulated terminal.
|
|
*/
|
|
private Screen mScreen;
|
|
|
|
/**
|
|
* Keeps track of the current argument of the current escape sequence.
|
|
* Ranges from 0 to MAX_ESCAPE_PARAMETERS-1. (Typically just 0 or 1.)
|
|
*/
|
|
private int mArgIndex;
|
|
|
|
/**
|
|
* The number of parameter arguments. This name comes from the ANSI standard
|
|
* for terminal escape codes.
|
|
*/
|
|
private static final int MAX_ESCAPE_PARAMETERS = 16;
|
|
|
|
/**
|
|
* Holds the arguments of the current escape sequence.
|
|
*/
|
|
private int[] mArgs = new int[MAX_ESCAPE_PARAMETERS];
|
|
|
|
// Escape processing states:
|
|
|
|
/**
|
|
* Escape processing state: Not currently in an escape sequence.
|
|
*/
|
|
private static final int ESC_NONE = 0;
|
|
|
|
/**
|
|
* Escape processing state: Have seen an ESC character
|
|
*/
|
|
private static final int ESC = 1;
|
|
|
|
/**
|
|
* Escape processing state: Have seen ESC POUND
|
|
*/
|
|
private static final int ESC_POUND = 2;
|
|
|
|
/**
|
|
* Escape processing state: Have seen ESC and a character-set-select char
|
|
*/
|
|
private static final int ESC_SELECT_LEFT_PAREN = 3;
|
|
|
|
/**
|
|
* Escape processing state: Have seen ESC and a character-set-select char
|
|
*/
|
|
private static final int ESC_SELECT_RIGHT_PAREN = 4;
|
|
|
|
/**
|
|
* Escape processing state: ESC [
|
|
*/
|
|
private static final int ESC_LEFT_SQUARE_BRACKET = 5;
|
|
|
|
/**
|
|
* Escape processing state: ESC [ ?
|
|
*/
|
|
private static final int ESC_LEFT_SQUARE_BRACKET_QUESTION_MARK = 6;
|
|
|
|
/**
|
|
* True if the current escape sequence should continue, false if the current
|
|
* escape sequence should be terminated. Used when parsing a single
|
|
* character.
|
|
*/
|
|
private boolean mContinueSequence;
|
|
|
|
/**
|
|
* The current state of the escape sequence state machine.
|
|
*/
|
|
private int mEscapeState;
|
|
|
|
/**
|
|
* Saved state of the cursor row, Used to implement the save/restore cursor
|
|
* position escape sequences.
|
|
*/
|
|
private int mSavedCursorRow;
|
|
|
|
/**
|
|
* Saved state of the cursor column, Used to implement the save/restore
|
|
* cursor position escape sequences.
|
|
*/
|
|
private int mSavedCursorCol;
|
|
|
|
// DecSet booleans
|
|
|
|
/**
|
|
* This mask indicates 132-column mode is set. (As opposed to 80-column
|
|
* mode.)
|
|
*/
|
|
private static final int K_132_COLUMN_MODE_MASK = 1 << 3;
|
|
|
|
/**
|
|
* This mask indicates that origin mode is set. (Cursor addressing is
|
|
* relative to the absolute screen size, rather than the currently set top
|
|
* and bottom margins.)
|
|
*/
|
|
private static final int K_ORIGIN_MODE_MASK = 1 << 6;
|
|
|
|
/**
|
|
* This mask indicates that wraparound mode is set. (As opposed to
|
|
* stop-at-right-column mode.)
|
|
*/
|
|
private static final int K_WRAPAROUND_MODE_MASK = 1 << 7;
|
|
|
|
/**
|
|
* Holds multiple DECSET flags. The data is stored this way, rather than in
|
|
* separate booleans, to make it easier to implement the save-and-restore
|
|
* semantics. The various k*ModeMask masks can be used to extract and modify
|
|
* the individual flags current states.
|
|
*/
|
|
private int mDecFlags;
|
|
|
|
/**
|
|
* Saves away a snapshot of the DECSET flags. Used to implement save and
|
|
* restore escape sequences.
|
|
*/
|
|
private int mSavedDecFlags;
|
|
|
|
// Modes set with Set Mode / Reset Mode
|
|
|
|
/**
|
|
* True if insert mode (as opposed to replace mode) is active. In insert
|
|
* mode new characters are inserted, pushing existing text to the right.
|
|
*/
|
|
private boolean mInsertMode;
|
|
|
|
/**
|
|
* Automatic newline mode. Configures whether pressing return on the
|
|
* keyboard automatically generates a return as well. Not currently
|
|
* implemented.
|
|
*/
|
|
private boolean mAutomaticNewlineMode;
|
|
|
|
/**
|
|
* An array of tab stops. mTabStop[i] is true if there is a tab stop set for
|
|
* column i.
|
|
*/
|
|
private boolean[] mTabStop;
|
|
|
|
// The margins allow portions of the screen to be locked.
|
|
|
|
/**
|
|
* The top margin of the screen, for scrolling purposes. Ranges from 0 to
|
|
* mRows-2.
|
|
*/
|
|
private int mTopMargin;
|
|
|
|
/**
|
|
* The bottom margin of the screen, for scrolling purposes. Ranges from
|
|
* mTopMargin + 2 to mRows. (Defines the first row after the scrolling
|
|
* region.
|
|
*/
|
|
private int mBottomMargin;
|
|
|
|
/**
|
|
* True if the next character to be emitted will be automatically wrapped to
|
|
* the next line. Used to disambiguate the case where the cursor is
|
|
* positioned on column mColumns-1.
|
|
*/
|
|
private boolean mAboutToAutoWrap;
|
|
|
|
/**
|
|
* Used for debugging, counts how many chars have been processed.
|
|
*/
|
|
private int mProcessedCharCount;
|
|
|
|
/**
|
|
* Foreground color, 0..7, mask with 8 for bold
|
|
*/
|
|
private int mForeColor;
|
|
|
|
/**
|
|
* Background color, 0..7, mask with 8 for underline
|
|
*/
|
|
private int mBackColor;
|
|
|
|
private boolean mInverseColors;
|
|
|
|
private boolean mbKeypadApplicationMode;
|
|
|
|
private boolean mAlternateCharSet;
|
|
|
|
/**
|
|
* Construct a terminal emulator that uses the supplied screen
|
|
*
|
|
* @param screen the screen to render characters into.
|
|
* @param columns the number of columns to emulate
|
|
* @param rows the number of rows to emulate
|
|
* @param termOut the output file descriptor that talks to the pseudo-tty.
|
|
*/
|
|
public TerminalEmulator(Screen screen, int columns, int rows,
|
|
FileOutputStream termOut) {
|
|
mScreen = screen;
|
|
mRows = rows;
|
|
mColumns = columns;
|
|
mTabStop = new boolean[mColumns];
|
|
mTermOut = termOut;
|
|
reset();
|
|
}
|
|
|
|
public void updateSize(int columns, int rows) {
|
|
if (mRows == rows && mColumns == columns) {
|
|
return;
|
|
}
|
|
String transcriptText = mScreen.getTranscriptText();
|
|
|
|
mScreen.resize(columns, rows, mForeColor, mBackColor);
|
|
|
|
if (mRows != rows) {
|
|
mRows = rows;
|
|
mTopMargin = 0;
|
|
mBottomMargin = mRows;
|
|
}
|
|
if (mColumns != columns) {
|
|
int oldColumns = mColumns;
|
|
mColumns = columns;
|
|
boolean[] oldTabStop = mTabStop;
|
|
mTabStop = new boolean[mColumns];
|
|
int toTransfer = Math.min(oldColumns, columns);
|
|
System.arraycopy(oldTabStop, 0, mTabStop, 0, toTransfer);
|
|
while (mCursorCol >= columns) {
|
|
mCursorCol -= columns;
|
|
mCursorRow = Math.min(mBottomMargin-1, mCursorRow + 1);
|
|
}
|
|
}
|
|
mCursorRow = 0;
|
|
mCursorCol = 0;
|
|
mAboutToAutoWrap = false;
|
|
|
|
int end = transcriptText.length()-1;
|
|
while ((end >= 0) && transcriptText.charAt(end) == '\n') {
|
|
end--;
|
|
}
|
|
for(int i = 0; i <= end; i++) {
|
|
byte c = (byte) transcriptText.charAt(i);
|
|
if (c == '\n') {
|
|
setCursorCol(0);
|
|
doLinefeed();
|
|
} else {
|
|
emit(c);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the cursor's current row.
|
|
*
|
|
* @return the cursor's current row.
|
|
*/
|
|
public final int getCursorRow() {
|
|
return mCursorRow;
|
|
}
|
|
|
|
/**
|
|
* Get the cursor's current column.
|
|
*
|
|
* @return the cursor's current column.
|
|
*/
|
|
public final int getCursorCol() {
|
|
return mCursorCol;
|
|
}
|
|
|
|
public final boolean getKeypadApplicationMode() {
|
|
return mbKeypadApplicationMode;
|
|
}
|
|
|
|
private void setDefaultTabStops() {
|
|
for (int i = 0; i < mColumns; i++) {
|
|
mTabStop[i] = (i & 7) == 0 && i != 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Accept bytes (typically from the pseudo-teletype) and process them.
|
|
*
|
|
* @param buffer a byte array containing the bytes to be processed
|
|
* @param base the first index of the array to process
|
|
* @param length the number of bytes in the array to process
|
|
*/
|
|
public void append(byte[] buffer, int base, int length) {
|
|
for (int i = 0; i < length; i++) {
|
|
byte b = buffer[base + i];
|
|
try {
|
|
if (Term.LOG_CHARACTERS_FLAG) {
|
|
char printableB = (char) b;
|
|
if (b < 32 || b > 126) {
|
|
printableB = ' ';
|
|
}
|
|
Log.w(Term.LOG_TAG, "'" + Character.toString(printableB)
|
|
+ "' (" + Integer.toString(b) + ")");
|
|
}
|
|
process(b);
|
|
mProcessedCharCount++;
|
|
} catch (Exception e) {
|
|
Log.e(Term.LOG_TAG, "Exception while processing character "
|
|
+ Integer.toString(mProcessedCharCount) + " code "
|
|
+ Integer.toString(b), e);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void process(byte b) {
|
|
switch (b) {
|
|
case 0: // NUL
|
|
// Do nothing
|
|
break;
|
|
|
|
case 7: // BEL
|
|
// Do nothing
|
|
break;
|
|
|
|
case 8: // BS
|
|
setCursorCol(Math.max(0, mCursorCol - 1));
|
|
break;
|
|
|
|
case 9: // HT
|
|
// Move to next tab stop, but not past edge of screen
|
|
setCursorCol(nextTabStop(mCursorCol));
|
|
break;
|
|
|
|
case 13:
|
|
setCursorCol(0);
|
|
break;
|
|
|
|
case 10: // CR
|
|
case 11: // VT
|
|
case 12: // LF
|
|
doLinefeed();
|
|
break;
|
|
|
|
case 14: // SO:
|
|
setAltCharSet(true);
|
|
break;
|
|
|
|
case 15: // SI:
|
|
setAltCharSet(false);
|
|
break;
|
|
|
|
|
|
case 24: // CAN
|
|
case 26: // SUB
|
|
if (mEscapeState != ESC_NONE) {
|
|
mEscapeState = ESC_NONE;
|
|
emit((byte) 127);
|
|
}
|
|
break;
|
|
|
|
case 27: // ESC
|
|
// Always starts an escape sequence
|
|
startEscapeSequence(ESC);
|
|
break;
|
|
|
|
case (byte) 0x9b: // CSI
|
|
startEscapeSequence(ESC_LEFT_SQUARE_BRACKET);
|
|
break;
|
|
|
|
default:
|
|
mContinueSequence = false;
|
|
switch (mEscapeState) {
|
|
case ESC_NONE:
|
|
if (b >= 32) {
|
|
emit(b);
|
|
}
|
|
break;
|
|
|
|
case ESC:
|
|
doEsc(b);
|
|
break;
|
|
|
|
case ESC_POUND:
|
|
doEscPound(b);
|
|
break;
|
|
|
|
case ESC_SELECT_LEFT_PAREN:
|
|
doEscSelectLeftParen(b);
|
|
break;
|
|
|
|
case ESC_SELECT_RIGHT_PAREN:
|
|
doEscSelectRightParen(b);
|
|
break;
|
|
|
|
case ESC_LEFT_SQUARE_BRACKET:
|
|
doEscLeftSquareBracket(b);
|
|
break;
|
|
|
|
case ESC_LEFT_SQUARE_BRACKET_QUESTION_MARK:
|
|
doEscLSBQuest(b);
|
|
break;
|
|
|
|
default:
|
|
unknownSequence(b);
|
|
break;
|
|
}
|
|
if (!mContinueSequence) {
|
|
mEscapeState = ESC_NONE;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void setAltCharSet(boolean alternateCharSet) {
|
|
mAlternateCharSet = alternateCharSet;
|
|
}
|
|
|
|
private int nextTabStop(int cursorCol) {
|
|
for (int i = cursorCol; i < mColumns; i++) {
|
|
if (mTabStop[i]) {
|
|
return i;
|
|
}
|
|
}
|
|
return mColumns - 1;
|
|
}
|
|
|
|
private void doEscLSBQuest(byte b) {
|
|
int mask = getDecFlagsMask(getArg0(0));
|
|
switch (b) {
|
|
case 'h': // Esc [ ? Pn h - DECSET
|
|
mDecFlags |= mask;
|
|
break;
|
|
|
|
case 'l': // Esc [ ? Pn l - DECRST
|
|
mDecFlags &= ~mask;
|
|
break;
|
|
|
|
case 'r': // Esc [ ? Pn r - restore
|
|
mDecFlags = (mDecFlags & ~mask) | (mSavedDecFlags & mask);
|
|
break;
|
|
|
|
case 's': // Esc [ ? Pn s - save
|
|
mSavedDecFlags = (mSavedDecFlags & ~mask) | (mDecFlags & mask);
|
|
break;
|
|
|
|
default:
|
|
parseArg(b);
|
|
break;
|
|
}
|
|
|
|
// 132 column mode
|
|
if ((mask & K_132_COLUMN_MODE_MASK) != 0) {
|
|
// We don't actually set 132 cols, but we do want the
|
|
// side effect of clearing the screen and homing the cursor.
|
|
blockClear(0, 0, mColumns, mRows);
|
|
setCursorRowCol(0, 0);
|
|
}
|
|
|
|
// origin mode
|
|
if ((mask & K_ORIGIN_MODE_MASK) != 0) {
|
|
// Home the cursor.
|
|
setCursorPosition(0, 0);
|
|
}
|
|
}
|
|
|
|
private int getDecFlagsMask(int argument) {
|
|
if (argument >= 1 && argument <= 9) {
|
|
return (1 << argument);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
private void startEscapeSequence(int escapeState) {
|
|
mEscapeState = escapeState;
|
|
mArgIndex = 0;
|
|
for (int j = 0; j < MAX_ESCAPE_PARAMETERS; j++) {
|
|
mArgs[j] = -1;
|
|
}
|
|
}
|
|
|
|
private void doLinefeed() {
|
|
int newCursorRow = mCursorRow + 1;
|
|
if (newCursorRow >= mBottomMargin) {
|
|
scroll();
|
|
newCursorRow = mBottomMargin - 1;
|
|
}
|
|
setCursorRow(newCursorRow);
|
|
}
|
|
|
|
private void continueSequence() {
|
|
mContinueSequence = true;
|
|
}
|
|
|
|
private void continueSequence(int state) {
|
|
mEscapeState = state;
|
|
mContinueSequence = true;
|
|
}
|
|
|
|
private void doEscSelectLeftParen(byte b) {
|
|
doSelectCharSet(true, b);
|
|
}
|
|
|
|
private void doEscSelectRightParen(byte b) {
|
|
doSelectCharSet(false, b);
|
|
}
|
|
|
|
private void doSelectCharSet(boolean isG0CharSet, byte b) {
|
|
switch (b) {
|
|
case 'A': // United Kingdom character set
|
|
break;
|
|
case 'B': // ASCII set
|
|
break;
|
|
case '0': // Special Graphics
|
|
break;
|
|
case '1': // Alternate character set
|
|
break;
|
|
case '2':
|
|
break;
|
|
default:
|
|
unknownSequence(b);
|
|
}
|
|
}
|
|
|
|
private void doEscPound(byte b) {
|
|
switch (b) {
|
|
case '8': // Esc # 8 - DECALN alignment test
|
|
mScreen.blockSet(0, 0, mColumns, mRows, 'E',
|
|
getForeColor(), getBackColor());
|
|
break;
|
|
|
|
default:
|
|
unknownSequence(b);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void doEsc(byte b) {
|
|
switch (b) {
|
|
case '#':
|
|
continueSequence(ESC_POUND);
|
|
break;
|
|
|
|
case '(':
|
|
continueSequence(ESC_SELECT_LEFT_PAREN);
|
|
break;
|
|
|
|
case ')':
|
|
continueSequence(ESC_SELECT_RIGHT_PAREN);
|
|
break;
|
|
|
|
case '7': // DECSC save cursor
|
|
mSavedCursorRow = mCursorRow;
|
|
mSavedCursorCol = mCursorCol;
|
|
break;
|
|
|
|
case '8': // DECRC restore cursor
|
|
setCursorRowCol(mSavedCursorRow, mSavedCursorCol);
|
|
break;
|
|
|
|
case 'D': // INDEX
|
|
doLinefeed();
|
|
break;
|
|
|
|
case 'E': // NEL
|
|
setCursorCol(0);
|
|
doLinefeed();
|
|
break;
|
|
|
|
case 'F': // Cursor to lower-left corner of screen
|
|
setCursorRowCol(0, mBottomMargin - 1);
|
|
break;
|
|
|
|
case 'H': // Tab set
|
|
mTabStop[mCursorCol] = true;
|
|
break;
|
|
|
|
case 'M': // Reverse index
|
|
if (mCursorRow == 0) {
|
|
mScreen.blockCopy(0, mTopMargin + 1, mColumns, mBottomMargin
|
|
- (mTopMargin + 1), 0, mTopMargin);
|
|
blockClear(0, mBottomMargin - 1, mColumns);
|
|
} else {
|
|
mCursorRow--;
|
|
}
|
|
|
|
break;
|
|
|
|
case 'N': // SS2
|
|
unimplementedSequence(b);
|
|
break;
|
|
|
|
case '0': // SS3
|
|
unimplementedSequence(b);
|
|
break;
|
|
|
|
case 'P': // Device control string
|
|
unimplementedSequence(b);
|
|
break;
|
|
|
|
case 'Z': // return terminal ID
|
|
sendDeviceAttributes();
|
|
break;
|
|
|
|
case '[':
|
|
continueSequence(ESC_LEFT_SQUARE_BRACKET);
|
|
break;
|
|
|
|
case '=': // DECKPAM
|
|
mbKeypadApplicationMode = true;
|
|
break;
|
|
|
|
case '>' : // DECKPNM
|
|
mbKeypadApplicationMode = false;
|
|
break;
|
|
|
|
default:
|
|
unknownSequence(b);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void doEscLeftSquareBracket(byte b) {
|
|
switch (b) {
|
|
case '@': // ESC [ Pn @ - ICH Insert Characters
|
|
{
|
|
int charsAfterCursor = mColumns - mCursorCol;
|
|
int charsToInsert = Math.min(getArg0(1), charsAfterCursor);
|
|
int charsToMove = charsAfterCursor - charsToInsert;
|
|
mScreen.blockCopy(mCursorCol, mCursorRow, charsToMove, 1,
|
|
mCursorCol + charsToInsert, mCursorRow);
|
|
blockClear(mCursorCol, mCursorRow, charsToInsert);
|
|
}
|
|
break;
|
|
|
|
case 'A': // ESC [ Pn A - Cursor Up
|
|
setCursorRow(Math.max(mTopMargin, mCursorRow - getArg0(1)));
|
|
break;
|
|
|
|
case 'B': // ESC [ Pn B - Cursor Down
|
|
setCursorRow(Math.min(mBottomMargin - 1, mCursorRow + getArg0(1)));
|
|
break;
|
|
|
|
case 'C': // ESC [ Pn C - Cursor Right
|
|
setCursorCol(Math.min(mColumns - 1, mCursorCol + getArg0(1)));
|
|
break;
|
|
|
|
case 'D': // ESC [ Pn D - Cursor Left
|
|
setCursorCol(Math.max(0, mCursorCol - getArg0(1)));
|
|
break;
|
|
|
|
case 'G': // ESC [ Pn G - Cursor Horizontal Absolute
|
|
setCursorCol(Math.min(Math.max(1, getArg0(1)), mColumns) - 1);
|
|
break;
|
|
|
|
case 'H': // ESC [ Pn ; H - Cursor Position
|
|
setHorizontalVerticalPosition();
|
|
break;
|
|
|
|
case 'J': // ESC [ Pn J - Erase in Display
|
|
switch (getArg0(0)) {
|
|
case 0: // Clear below
|
|
blockClear(mCursorCol, mCursorRow, mColumns - mCursorCol);
|
|
blockClear(0, mCursorRow + 1, mColumns,
|
|
mBottomMargin - (mCursorRow + 1));
|
|
break;
|
|
|
|
case 1: // Erase from the start of the screen to the cursor.
|
|
blockClear(0, mTopMargin, mColumns, mCursorRow - mTopMargin);
|
|
blockClear(0, mCursorRow, mCursorCol + 1);
|
|
break;
|
|
|
|
case 2: // Clear all
|
|
blockClear(0, mTopMargin, mColumns, mBottomMargin - mTopMargin);
|
|
break;
|
|
|
|
default:
|
|
unknownSequence(b);
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case 'K': // ESC [ Pn K - Erase in Line
|
|
switch (getArg0(0)) {
|
|
case 0: // Clear to right
|
|
blockClear(mCursorCol, mCursorRow, mColumns - mCursorCol);
|
|
break;
|
|
|
|
case 1: // Erase start of line to cursor (including cursor)
|
|
blockClear(0, mCursorRow, mCursorCol + 1);
|
|
break;
|
|
|
|
case 2: // Clear whole line
|
|
blockClear(0, mCursorRow, mColumns);
|
|
break;
|
|
|
|
default:
|
|
unknownSequence(b);
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case 'L': // Insert Lines
|
|
{
|
|
int linesAfterCursor = mBottomMargin - mCursorRow;
|
|
int linesToInsert = Math.min(getArg0(1), linesAfterCursor);
|
|
int linesToMove = linesAfterCursor - linesToInsert;
|
|
mScreen.blockCopy(0, mCursorRow, mColumns, linesToMove, 0,
|
|
mCursorRow + linesToInsert);
|
|
blockClear(0, mCursorRow, mColumns, linesToInsert);
|
|
}
|
|
break;
|
|
|
|
case 'M': // Delete Lines
|
|
{
|
|
int linesAfterCursor = mBottomMargin - mCursorRow;
|
|
int linesToDelete = Math.min(getArg0(1), linesAfterCursor);
|
|
int linesToMove = linesAfterCursor - linesToDelete;
|
|
mScreen.blockCopy(0, mCursorRow + linesToDelete, mColumns,
|
|
linesToMove, 0, mCursorRow);
|
|
blockClear(0, mCursorRow + linesToMove, mColumns, linesToDelete);
|
|
}
|
|
break;
|
|
|
|
case 'P': // Delete Characters
|
|
{
|
|
int charsAfterCursor = mColumns - mCursorCol;
|
|
int charsToDelete = Math.min(getArg0(1), charsAfterCursor);
|
|
int charsToMove = charsAfterCursor - charsToDelete;
|
|
mScreen.blockCopy(mCursorCol + charsToDelete, mCursorRow,
|
|
charsToMove, 1, mCursorCol, mCursorRow);
|
|
blockClear(mCursorCol + charsToMove, mCursorRow, charsToDelete);
|
|
}
|
|
break;
|
|
|
|
case 'T': // Mouse tracking
|
|
unimplementedSequence(b);
|
|
break;
|
|
|
|
case '?': // Esc [ ? -- start of a private mode set
|
|
continueSequence(ESC_LEFT_SQUARE_BRACKET_QUESTION_MARK);
|
|
break;
|
|
|
|
case 'c': // Send device attributes
|
|
sendDeviceAttributes();
|
|
break;
|
|
|
|
case 'd': // ESC [ Pn d - Vert Position Absolute
|
|
setCursorRow(Math.min(Math.max(1, getArg0(1)), mRows) - 1);
|
|
break;
|
|
|
|
case 'f': // Horizontal and Vertical Position
|
|
setHorizontalVerticalPosition();
|
|
break;
|
|
|
|
case 'g': // Clear tab stop
|
|
switch (getArg0(0)) {
|
|
case 0:
|
|
mTabStop[mCursorCol] = false;
|
|
break;
|
|
|
|
case 3:
|
|
for (int i = 0; i < mColumns; i++) {
|
|
mTabStop[i] = false;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
// Specified to have no effect.
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case 'h': // Set Mode
|
|
doSetMode(true);
|
|
break;
|
|
|
|
case 'l': // Reset Mode
|
|
doSetMode(false);
|
|
break;
|
|
|
|
case 'm': // Esc [ Pn m - character attributes.
|
|
selectGraphicRendition();
|
|
break;
|
|
|
|
case 'r': // Esc [ Pn ; Pn r - set top and bottom margins
|
|
{
|
|
// The top margin defaults to 1, the bottom margin
|
|
// (unusually for arguments) defaults to mRows.
|
|
//
|
|
// The escape sequence numbers top 1..23, but we
|
|
// number top 0..22.
|
|
// The escape sequence numbers bottom 2..24, and
|
|
// so do we (because we use a zero based numbering
|
|
// scheme, but we store the first line below the
|
|
// bottom-most scrolling line.
|
|
// As a result, we adjust the top line by -1, but
|
|
// we leave the bottom line alone.
|
|
//
|
|
// Also require that top + 2 <= bottom
|
|
|
|
int top = Math.max(0, Math.min(getArg0(1) - 1, mRows - 2));
|
|
int bottom = Math.max(top + 2, Math.min(getArg1(mRows), mRows));
|
|
mTopMargin = top;
|
|
mBottomMargin = bottom;
|
|
|
|
// The cursor is placed in the home position
|
|
setCursorRowCol(mTopMargin, 0);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
parseArg(b);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void selectGraphicRendition() {
|
|
for (int i = 0; i <= mArgIndex; i++) {
|
|
int code = mArgs[i];
|
|
if ( code < 0) {
|
|
if (mArgIndex > 0) {
|
|
continue;
|
|
} else {
|
|
code = 0;
|
|
}
|
|
}
|
|
if (code == 0) { // reset
|
|
mInverseColors = false;
|
|
mForeColor = 7;
|
|
mBackColor = 0;
|
|
} else if (code == 1) { // bold
|
|
mForeColor |= 0x8;
|
|
} else if (code == 4) { // underscore
|
|
mBackColor |= 0x8;
|
|
} else if (code == 7) { // inverse
|
|
mInverseColors = true;
|
|
} else if (code >= 30 && code <= 37) { // foreground color
|
|
mForeColor = (mForeColor & 0x8) | (code - 30);
|
|
} else if (code >= 40 && code <= 47) { // background color
|
|
mBackColor = (mBackColor & 0x8) | (code - 40);
|
|
} else {
|
|
if (Term.LOG_UNKNOWN_ESCAPE_SEQUENCES) {
|
|
Log.w(Term.LOG_TAG, String.format("SGR unknown code %d", code));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void blockClear(int sx, int sy, int w) {
|
|
blockClear(sx, sy, w, 1);
|
|
}
|
|
|
|
private void blockClear(int sx, int sy, int w, int h) {
|
|
mScreen.blockSet(sx, sy, w, h, ' ', getForeColor(), getBackColor());
|
|
}
|
|
|
|
private int getForeColor() {
|
|
return mInverseColors ?
|
|
((mBackColor & 0x7) | (mForeColor & 0x8)) : mForeColor;
|
|
}
|
|
|
|
private int getBackColor() {
|
|
return mInverseColors ?
|
|
((mForeColor & 0x7) | (mBackColor & 0x8)) : mBackColor;
|
|
}
|
|
|
|
private void doSetMode(boolean newValue) {
|
|
int modeBit = getArg0(0);
|
|
switch (modeBit) {
|
|
case 4:
|
|
mInsertMode = newValue;
|
|
break;
|
|
|
|
case 20:
|
|
mAutomaticNewlineMode = newValue;
|
|
break;
|
|
|
|
default:
|
|
unknownParameter(modeBit);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void setHorizontalVerticalPosition() {
|
|
|
|
// Parameters are Row ; Column
|
|
|
|
setCursorPosition(getArg1(1) - 1, getArg0(1) - 1);
|
|
}
|
|
|
|
private void setCursorPosition(int x, int y) {
|
|
int effectiveTopMargin = 0;
|
|
int effectiveBottomMargin = mRows;
|
|
if ((mDecFlags & K_ORIGIN_MODE_MASK) != 0) {
|
|
effectiveTopMargin = mTopMargin;
|
|
effectiveBottomMargin = mBottomMargin;
|
|
}
|
|
int newRow =
|
|
Math.max(effectiveTopMargin, Math.min(effectiveTopMargin + y,
|
|
effectiveBottomMargin - 1));
|
|
int newCol = Math.max(0, Math.min(x, mColumns - 1));
|
|
setCursorRowCol(newRow, newCol);
|
|
}
|
|
|
|
private void sendDeviceAttributes() {
|
|
// This identifies us as a DEC vt100 with advanced
|
|
// video options. This is what the xterm terminal
|
|
// emulator sends.
|
|
byte[] attributes =
|
|
{
|
|
/* VT100 */
|
|
(byte) 27, (byte) '[', (byte) '?', (byte) '1',
|
|
(byte) ';', (byte) '2', (byte) 'c'
|
|
|
|
/* VT220
|
|
(byte) 27, (byte) '[', (byte) '?', (byte) '6',
|
|
(byte) '0', (byte) ';',
|
|
(byte) '1', (byte) ';',
|
|
(byte) '2', (byte) ';',
|
|
(byte) '6', (byte) ';',
|
|
(byte) '8', (byte) ';',
|
|
(byte) '9', (byte) ';',
|
|
(byte) '1', (byte) '5', (byte) ';',
|
|
(byte) 'c'
|
|
*/
|
|
};
|
|
|
|
write(attributes);
|
|
}
|
|
|
|
/**
|
|
* Send data to the shell process
|
|
* @param data
|
|
*/
|
|
private void write(byte[] data) {
|
|
try {
|
|
mTermOut.write(data);
|
|
mTermOut.flush();
|
|
} catch (IOException e) {
|
|
// Ignore exception
|
|
// We don't really care if the receiver isn't listening.
|
|
// We just make a best effort to answer the query.
|
|
}
|
|
}
|
|
|
|
private void scroll() {
|
|
mScreen.scroll(mTopMargin, mBottomMargin,
|
|
getForeColor(), getBackColor());
|
|
}
|
|
|
|
/**
|
|
* Process the next ASCII character of a parameter.
|
|
*
|
|
* @param b The next ASCII character of the paramater sequence.
|
|
*/
|
|
private void parseArg(byte b) {
|
|
if (b >= '0' && b <= '9') {
|
|
if (mArgIndex < mArgs.length) {
|
|
int oldValue = mArgs[mArgIndex];
|
|
int thisDigit = b - '0';
|
|
int value;
|
|
if (oldValue >= 0) {
|
|
value = oldValue * 10 + thisDigit;
|
|
} else {
|
|
value = thisDigit;
|
|
}
|
|
mArgs[mArgIndex] = value;
|
|
}
|
|
continueSequence();
|
|
} else if (b == ';') {
|
|
if (mArgIndex < mArgs.length) {
|
|
mArgIndex++;
|
|
}
|
|
continueSequence();
|
|
} else {
|
|
unknownSequence(b);
|
|
}
|
|
}
|
|
|
|
private int getArg0(int defaultValue) {
|
|
return getArg(0, defaultValue);
|
|
}
|
|
|
|
private int getArg1(int defaultValue) {
|
|
return getArg(1, defaultValue);
|
|
}
|
|
|
|
private int getArg(int index, int defaultValue) {
|
|
int result = mArgs[index];
|
|
if (result < 0) {
|
|
result = defaultValue;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private void unimplementedSequence(byte b) {
|
|
if (Term.LOG_UNKNOWN_ESCAPE_SEQUENCES) {
|
|
logError("unimplemented", b);
|
|
}
|
|
finishSequence();
|
|
}
|
|
|
|
private void unknownSequence(byte b) {
|
|
if (Term.LOG_UNKNOWN_ESCAPE_SEQUENCES) {
|
|
logError("unknown", b);
|
|
}
|
|
finishSequence();
|
|
}
|
|
|
|
private void unknownParameter(int parameter) {
|
|
if (Term.LOG_UNKNOWN_ESCAPE_SEQUENCES) {
|
|
StringBuilder buf = new StringBuilder();
|
|
buf.append("Unknown parameter");
|
|
buf.append(parameter);
|
|
logError(buf.toString());
|
|
}
|
|
}
|
|
|
|
private void logError(String errorType, byte b) {
|
|
if (Term.LOG_UNKNOWN_ESCAPE_SEQUENCES) {
|
|
StringBuilder buf = new StringBuilder();
|
|
buf.append(errorType);
|
|
buf.append(" sequence ");
|
|
buf.append(" EscapeState: ");
|
|
buf.append(mEscapeState);
|
|
buf.append(" char: '");
|
|
buf.append((char) b);
|
|
buf.append("' (");
|
|
buf.append(b);
|
|
buf.append(")");
|
|
boolean firstArg = true;
|
|
for (int i = 0; i <= mArgIndex; i++) {
|
|
int value = mArgs[i];
|
|
if (value >= 0) {
|
|
if (firstArg) {
|
|
firstArg = false;
|
|
buf.append("args = ");
|
|
}
|
|
buf.append(String.format("%d; ", value));
|
|
}
|
|
}
|
|
logError(buf.toString());
|
|
}
|
|
}
|
|
|
|
private void logError(String error) {
|
|
if (Term.LOG_UNKNOWN_ESCAPE_SEQUENCES) {
|
|
Log.e(Term.LOG_TAG, error);
|
|
}
|
|
finishSequence();
|
|
}
|
|
|
|
private void finishSequence() {
|
|
mEscapeState = ESC_NONE;
|
|
}
|
|
|
|
private boolean autoWrapEnabled() {
|
|
// Always enable auto wrap, because it's useful on a small screen
|
|
return true;
|
|
// return (mDecFlags & K_WRAPAROUND_MODE_MASK) != 0;
|
|
}
|
|
|
|
/**
|
|
* Send an ASCII character to the screen.
|
|
*
|
|
* @param b the ASCII character to display.
|
|
*/
|
|
private void emit(byte b) {
|
|
boolean autoWrap = autoWrapEnabled();
|
|
|
|
if (autoWrap) {
|
|
if (mCursorCol == mColumns - 1 && mAboutToAutoWrap) {
|
|
mScreen.setLineWrap(mCursorRow);
|
|
mCursorCol = 0;
|
|
if (mCursorRow + 1 < mBottomMargin) {
|
|
mCursorRow++;
|
|
} else {
|
|
scroll();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (mInsertMode) { // Move character to right one space
|
|
int destCol = mCursorCol + 1;
|
|
if (destCol < mColumns) {
|
|
mScreen.blockCopy(mCursorCol, mCursorRow, mColumns - destCol,
|
|
1, destCol, mCursorRow);
|
|
}
|
|
}
|
|
|
|
mScreen.set(mCursorCol, mCursorRow, b, getForeColor(), getBackColor());
|
|
|
|
if (autoWrap) {
|
|
mAboutToAutoWrap = (mCursorCol == mColumns - 1);
|
|
}
|
|
|
|
mCursorCol = Math.min(mCursorCol + 1, mColumns - 1);
|
|
}
|
|
|
|
private void setCursorRow(int row) {
|
|
mCursorRow = row;
|
|
mAboutToAutoWrap = false;
|
|
}
|
|
|
|
private void setCursorCol(int col) {
|
|
mCursorCol = col;
|
|
mAboutToAutoWrap = false;
|
|
}
|
|
|
|
private void setCursorRowCol(int row, int col) {
|
|
mCursorRow = Math.min(row, mRows-1);
|
|
mCursorCol = Math.min(col, mColumns-1);
|
|
mAboutToAutoWrap = false;
|
|
}
|
|
|
|
/**
|
|
* Reset the terminal emulator to its initial state.
|
|
*/
|
|
public void reset() {
|
|
mCursorRow = 0;
|
|
mCursorCol = 0;
|
|
mArgIndex = 0;
|
|
mContinueSequence = false;
|
|
mEscapeState = ESC_NONE;
|
|
mSavedCursorRow = 0;
|
|
mSavedCursorCol = 0;
|
|
mDecFlags = 0;
|
|
mSavedDecFlags = 0;
|
|
mInsertMode = false;
|
|
mAutomaticNewlineMode = false;
|
|
mTopMargin = 0;
|
|
mBottomMargin = mRows;
|
|
mAboutToAutoWrap = false;
|
|
mForeColor = 7;
|
|
mBackColor = 0;
|
|
mInverseColors = false;
|
|
mbKeypadApplicationMode = false;
|
|
mAlternateCharSet = false;
|
|
// mProcessedCharCount is preserved unchanged.
|
|
setDefaultTabStops();
|
|
blockClear(0, 0, mColumns, mRows);
|
|
}
|
|
|
|
public String getTranscriptText() {
|
|
return mScreen.getTranscriptText();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Text renderer interface
|
|
*/
|
|
|
|
interface TextRenderer {
|
|
int getCharacterWidth();
|
|
int getCharacterHeight();
|
|
void drawTextRun(Canvas canvas, float x, float y,
|
|
int lineOffset, char[] text,
|
|
int index, int count, boolean cursor, int foreColor, int backColor);
|
|
}
|
|
|
|
abstract class BaseTextRenderer implements TextRenderer {
|
|
protected int[] mForePaint = {
|
|
0xff000000, // Black
|
|
0xffff0000, // Red
|
|
0xff00ff00, // green
|
|
0xffffff00, // yellow
|
|
0xff0000ff, // blue
|
|
0xffff00ff, // magenta
|
|
0xff00ffff, // cyan
|
|
0xffffffff // white -- is overridden by constructor
|
|
};
|
|
protected int[] mBackPaint = {
|
|
0xff000000, // Black -- is overridden by constructor
|
|
0xffcc0000, // Red
|
|
0xff00cc00, // green
|
|
0xffcccc00, // yellow
|
|
0xff0000cc, // blue
|
|
0xffff00cc, // magenta
|
|
0xff00cccc, // cyan
|
|
0xffffffff // white
|
|
};
|
|
protected final static int mCursorPaint = 0xff808080;
|
|
|
|
public BaseTextRenderer(int forePaintColor, int backPaintColor) {
|
|
mForePaint[7] = forePaintColor;
|
|
mBackPaint[0] = backPaintColor;
|
|
|
|
}
|
|
}
|
|
|
|
class Bitmap4x8FontRenderer extends BaseTextRenderer {
|
|
private final static int kCharacterWidth = 4;
|
|
private final static int kCharacterHeight = 8;
|
|
private Bitmap mFont;
|
|
private int mCurrentForeColor;
|
|
private int mCurrentBackColor;
|
|
private float[] mColorMatrix;
|
|
private Paint mPaint;
|
|
private static final float BYTE_SCALE = 1.0f / 255.0f;
|
|
|
|
public Bitmap4x8FontRenderer(Resources resources,
|
|
int forePaintColor, int backPaintColor) {
|
|
super(forePaintColor, backPaintColor);
|
|
mFont = BitmapFactory.decodeResource(resources,
|
|
R.drawable.atari_small);
|
|
mPaint = new Paint();
|
|
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
|
|
}
|
|
|
|
public int getCharacterWidth() {
|
|
return kCharacterWidth;
|
|
}
|
|
|
|
public int getCharacterHeight() {
|
|
return kCharacterHeight;
|
|
}
|
|
|
|
public void drawTextRun(Canvas canvas, float x, float y,
|
|
int lineOffset, char[] text, int index, int count,
|
|
boolean cursor, int foreColor, int backColor) {
|
|
setColorMatrix(mForePaint[foreColor & 7],
|
|
cursor ? mCursorPaint : mBackPaint[backColor & 7]);
|
|
int destX = (int) x + kCharacterWidth * lineOffset;
|
|
int destY = (int) y;
|
|
Rect srcRect = new Rect();
|
|
Rect destRect = new Rect();
|
|
destRect.top = (destY - kCharacterHeight);
|
|
destRect.bottom = destY;
|
|
for(int i = 0; i < count; i++) {
|
|
char c = text[i + index];
|
|
if ((cursor || (c != 32)) && (c < 128)) {
|
|
int cellX = c & 31;
|
|
int cellY = (c >> 5) & 3;
|
|
int srcX = cellX * kCharacterWidth;
|
|
int srcY = cellY * kCharacterHeight;
|
|
srcRect.set(srcX, srcY,
|
|
srcX + kCharacterWidth, srcY + kCharacterHeight);
|
|
destRect.left = destX;
|
|
destRect.right = destX + kCharacterWidth;
|
|
canvas.drawBitmap(mFont, srcRect, destRect, mPaint);
|
|
}
|
|
destX += kCharacterWidth;
|
|
}
|
|
}
|
|
|
|
private void setColorMatrix(int foreColor, int backColor) {
|
|
if ((foreColor != mCurrentForeColor)
|
|
|| (backColor != mCurrentBackColor)
|
|
|| (mColorMatrix == null)) {
|
|
mCurrentForeColor = foreColor;
|
|
mCurrentBackColor = backColor;
|
|
if (mColorMatrix == null) {
|
|
mColorMatrix = new float[20];
|
|
mColorMatrix[18] = 1.0f; // Just copy Alpha
|
|
}
|
|
for (int component = 0; component < 3; component++) {
|
|
int rightShift = (2 - component) << 3;
|
|
int fore = 0xff & (foreColor >> rightShift);
|
|
int back = 0xff & (backColor >> rightShift);
|
|
int delta = back - fore;
|
|
mColorMatrix[component * 6] = delta * BYTE_SCALE;
|
|
mColorMatrix[component * 5 + 4] = fore;
|
|
}
|
|
mPaint.setColorFilter(new ColorMatrixColorFilter(mColorMatrix));
|
|
}
|
|
}
|
|
}
|
|
|
|
class PaintRenderer extends BaseTextRenderer {
|
|
public PaintRenderer(int fontSize, int forePaintColor, int backPaintColor) {
|
|
super(forePaintColor, backPaintColor);
|
|
mTextPaint = new Paint();
|
|
mTextPaint.setTypeface(Typeface.MONOSPACE);
|
|
mTextPaint.setAntiAlias(true);
|
|
mTextPaint.setTextSize(fontSize);
|
|
|
|
mCharHeight = (int) Math.ceil(mTextPaint.getFontSpacing());
|
|
mCharAscent = (int) Math.ceil(mTextPaint.ascent());
|
|
mCharDescent = mCharHeight + mCharAscent;
|
|
mCharWidth = (int) mTextPaint.measureText(EXAMPLE_CHAR, 0, 1);
|
|
}
|
|
|
|
public void drawTextRun(Canvas canvas, float x, float y, int lineOffset,
|
|
char[] text, int index, int count,
|
|
boolean cursor, int foreColor, int backColor) {
|
|
if (cursor) {
|
|
mTextPaint.setColor(mCursorPaint);
|
|
} else {
|
|
mTextPaint.setColor(mBackPaint[backColor & 0x7]);
|
|
}
|
|
float left = x + lineOffset * mCharWidth;
|
|
canvas.drawRect(left, y + mCharAscent,
|
|
left + count * mCharWidth, y + mCharDescent,
|
|
mTextPaint);
|
|
boolean bold = ( foreColor & 0x8 ) != 0;
|
|
boolean underline = (backColor & 0x8) != 0;
|
|
if (bold) {
|
|
mTextPaint.setFakeBoldText(true);
|
|
}
|
|
if (underline) {
|
|
mTextPaint.setUnderlineText(true);
|
|
}
|
|
mTextPaint.setColor(mForePaint[foreColor & 0x7]);
|
|
canvas.drawText(text, index, count, left, y, mTextPaint);
|
|
if (bold) {
|
|
mTextPaint.setFakeBoldText(false);
|
|
}
|
|
if (underline) {
|
|
mTextPaint.setUnderlineText(false);
|
|
}
|
|
}
|
|
|
|
public int getCharacterHeight() {
|
|
return mCharHeight;
|
|
}
|
|
|
|
public int getCharacterWidth() {
|
|
return mCharWidth;
|
|
}
|
|
|
|
|
|
private Paint mTextPaint;
|
|
private int mCharWidth;
|
|
private int mCharHeight;
|
|
private int mCharAscent;
|
|
private int mCharDescent;
|
|
private static final char[] EXAMPLE_CHAR = {'X'};
|
|
}
|
|
|
|
/**
|
|
* A multi-thread-safe produce-consumer byte array.
|
|
* Only allows one producer and one consumer.
|
|
*/
|
|
|
|
class ByteQueue {
|
|
public ByteQueue(int size) {
|
|
mBuffer = new byte[size];
|
|
}
|
|
|
|
public int getBytesAvailable() {
|
|
synchronized(this) {
|
|
return mStoredBytes;
|
|
}
|
|
}
|
|
|
|
public int read(byte[] buffer, int offset, int length)
|
|
throws InterruptedException {
|
|
if (length + offset > buffer.length) {
|
|
throw
|
|
new IllegalArgumentException("length + offset > buffer.length");
|
|
}
|
|
if (length < 0) {
|
|
throw
|
|
new IllegalArgumentException("length < 0");
|
|
|
|
}
|
|
if (length == 0) {
|
|
return 0;
|
|
}
|
|
synchronized(this) {
|
|
while (mStoredBytes == 0) {
|
|
wait();
|
|
}
|
|
int totalRead = 0;
|
|
int bufferLength = mBuffer.length;
|
|
boolean wasFull = bufferLength == mStoredBytes;
|
|
while (length > 0 && mStoredBytes > 0) {
|
|
int oneRun = Math.min(bufferLength - mHead, mStoredBytes);
|
|
int bytesToCopy = Math.min(length, oneRun);
|
|
System.arraycopy(mBuffer, mHead, buffer, offset, bytesToCopy);
|
|
mHead += bytesToCopy;
|
|
if (mHead >= bufferLength) {
|
|
mHead = 0;
|
|
}
|
|
mStoredBytes -= bytesToCopy;
|
|
length -= bytesToCopy;
|
|
offset += bytesToCopy;
|
|
totalRead += bytesToCopy;
|
|
}
|
|
if (wasFull) {
|
|
notify();
|
|
}
|
|
return totalRead;
|
|
}
|
|
}
|
|
|
|
public void write(byte[] buffer, int offset, int length)
|
|
throws InterruptedException {
|
|
if (length + offset > buffer.length) {
|
|
throw
|
|
new IllegalArgumentException("length + offset > buffer.length");
|
|
}
|
|
if (length < 0) {
|
|
throw
|
|
new IllegalArgumentException("length < 0");
|
|
|
|
}
|
|
if (length == 0) {
|
|
return;
|
|
}
|
|
synchronized(this) {
|
|
int bufferLength = mBuffer.length;
|
|
boolean wasEmpty = mStoredBytes == 0;
|
|
while (length > 0) {
|
|
while(bufferLength == mStoredBytes) {
|
|
wait();
|
|
}
|
|
int tail = mHead + mStoredBytes;
|
|
int oneRun;
|
|
if (tail >= bufferLength) {
|
|
tail = tail - bufferLength;
|
|
oneRun = mHead - tail;
|
|
} else {
|
|
oneRun = bufferLength - tail;
|
|
}
|
|
int bytesToCopy = Math.min(oneRun, length);
|
|
System.arraycopy(buffer, offset, mBuffer, tail, bytesToCopy);
|
|
offset += bytesToCopy;
|
|
mStoredBytes += bytesToCopy;
|
|
length -= bytesToCopy;
|
|
}
|
|
if (wasEmpty) {
|
|
notify();
|
|
}
|
|
}
|
|
}
|
|
|
|
private byte[] mBuffer;
|
|
private int mHead;
|
|
private int mStoredBytes;
|
|
}
|
|
/**
|
|
* A view on a transcript and a terminal emulator. Displays the text of the
|
|
* transcript and the current cursor position of the terminal emulator.
|
|
*/
|
|
class EmulatorView extends View implements GestureDetector.OnGestureListener {
|
|
|
|
/**
|
|
* We defer some initialization until we have been layed out in the view
|
|
* hierarchy. The boolean tracks when we know what our size is.
|
|
*/
|
|
private boolean mKnownSize;
|
|
|
|
/**
|
|
* Our transcript. Contains the screen and the transcript.
|
|
*/
|
|
private TranscriptScreen mTranscriptScreen;
|
|
|
|
/**
|
|
* Number of rows in the transcript.
|
|
*/
|
|
private static final int TRANSCRIPT_ROWS = 10000;
|
|
|
|
/**
|
|
* Total width of each character, in pixels
|
|
*/
|
|
private int mCharacterWidth;
|
|
|
|
/**
|
|
* Total height of each character, in pixels
|
|
*/
|
|
private int mCharacterHeight;
|
|
|
|
/**
|
|
* Used to render text
|
|
*/
|
|
private TextRenderer mTextRenderer;
|
|
|
|
/**
|
|
* Text size. Zero means 4 x 8 font.
|
|
*/
|
|
private int mTextSize;
|
|
|
|
/**
|
|
* Foreground color.
|
|
*/
|
|
private int mForeground;
|
|
|
|
/**
|
|
* Background color.
|
|
*/
|
|
private int mBackground;
|
|
|
|
/**
|
|
* Used to paint the cursor
|
|
*/
|
|
private Paint mCursorPaint;
|
|
|
|
private Paint mBackgroundPaint;
|
|
|
|
/**
|
|
* Our terminal emulator. We use this to get the current cursor position.
|
|
*/
|
|
private TerminalEmulator mEmulator;
|
|
|
|
/**
|
|
* The number of rows of text to display.
|
|
*/
|
|
private int mRows;
|
|
|
|
/**
|
|
* The number of columns of text to display.
|
|
*/
|
|
private int mColumns;
|
|
|
|
/**
|
|
* The number of columns that are visible on the display.
|
|
*/
|
|
|
|
private int mVisibleColumns;
|
|
|
|
/**
|
|
* The top row of text to display. Ranges from -activeTranscriptRows to 0
|
|
*/
|
|
private int mTopRow;
|
|
|
|
private int mLeftColumn;
|
|
|
|
private FileDescriptor mTermFd;
|
|
/**
|
|
* Used to receive data from the remote process.
|
|
*/
|
|
private FileInputStream mTermIn;
|
|
|
|
private FileOutputStream mTermOut;
|
|
|
|
private ByteQueue mByteQueue;
|
|
|
|
/**
|
|
* Used to temporarily hold data received from the remote process. Allocated
|
|
* once and used permanently to minimize heap thrashing.
|
|
*/
|
|
private byte[] mReceiveBuffer;
|
|
|
|
/**
|
|
* Our private message id, which we use to receive new input from the
|
|
* remote process.
|
|
*/
|
|
private static final int UPDATE = 1;
|
|
|
|
/**
|
|
* Thread that polls for input from the remote process
|
|
*/
|
|
|
|
private Thread mPollingThread;
|
|
|
|
private GestureDetector mGestureDetector;
|
|
private float mScrollRemainder;
|
|
private TermKeyListener mKeyListener;
|
|
|
|
/**
|
|
* Our message handler class. Implements a periodic callback.
|
|
*/
|
|
private final Handler mHandler = new Handler() {
|
|
/**
|
|
* Handle the callback message. Call our enclosing class's update
|
|
* method.
|
|
*
|
|
* @param msg The callback message.
|
|
*/
|
|
@Override
|
|
public void handleMessage(Message msg) {
|
|
if (msg.what == UPDATE) {
|
|
update();
|
|
}
|
|
}
|
|
};
|
|
|
|
public EmulatorView(Context context) {
|
|
super(context);
|
|
commonConstructor(context);
|
|
}
|
|
|
|
public void register(TermKeyListener listener) {
|
|
mKeyListener = listener;
|
|
}
|
|
|
|
public void setColors(int foreground, int background) {
|
|
mForeground = foreground;
|
|
mBackground = background;
|
|
updateText();
|
|
}
|
|
|
|
public String getTranscriptText() {
|
|
return mEmulator.getTranscriptText();
|
|
}
|
|
|
|
public void resetTerminal() {
|
|
mEmulator.reset();
|
|
invalidate();
|
|
}
|
|
|
|
@Override
|
|
public boolean onCheckIsTextEditor() {
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
|
|
return new BaseInputConnection(this, false) {
|
|
|
|
@Override
|
|
public boolean beginBatchEdit() {
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean clearMetaKeyStates(int states) {
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean commitCompletion(CompletionInfo text) {
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean commitText(CharSequence text, int newCursorPosition) {
|
|
sendText(text);
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean deleteSurroundingText(int leftLength, int rightLength) {
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean endBatchEdit() {
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean finishComposingText() {
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public int getCursorCapsMode(int reqModes) {
|
|
return 0;
|
|
}
|
|
|
|
@Override
|
|
public ExtractedText getExtractedText(ExtractedTextRequest request,
|
|
int flags) {
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
public CharSequence getTextAfterCursor(int n, int flags) {
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
public CharSequence getTextBeforeCursor(int n, int flags) {
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
public boolean performEditorAction(int actionCode) {
|
|
if(actionCode == EditorInfo.IME_ACTION_UNSPECIFIED) {
|
|
// The "return" key has been pressed on the IME.
|
|
sendText("\n");
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public boolean performContextMenuAction(int id) {
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean performPrivateCommand(String action, Bundle data) {
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean sendKeyEvent(KeyEvent event) {
|
|
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
|
switch(event.getKeyCode()) {
|
|
case KeyEvent.KEYCODE_DEL:
|
|
sendChar(127);
|
|
break;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean setComposingText(CharSequence text, int newCursorPosition) {
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean setSelection(int start, int end) {
|
|
return true;
|
|
}
|
|
|
|
private void sendChar(int c) {
|
|
try {
|
|
mapAndSend(c);
|
|
} catch (IOException ex) {
|
|
|
|
}
|
|
}
|
|
private void sendText(CharSequence text) {
|
|
int n = text.length();
|
|
try {
|
|
for(int i = 0; i < n; i++) {
|
|
char c = text.charAt(i);
|
|
mapAndSend(c);
|
|
}
|
|
} catch (IOException e) {
|
|
}
|
|
}
|
|
|
|
private void mapAndSend(int c) throws IOException {
|
|
mTermOut.write(
|
|
mKeyListener.mapControlChar(c));
|
|
}
|
|
};
|
|
}
|
|
|
|
public boolean getKeypadApplicationMode() {
|
|
return mEmulator.getKeypadApplicationMode();
|
|
}
|
|
|
|
public EmulatorView(Context context, AttributeSet attrs) {
|
|
this(context, attrs, 0);
|
|
}
|
|
|
|
public EmulatorView(Context context, AttributeSet attrs,
|
|
int defStyle) {
|
|
super(context, attrs, defStyle);
|
|
TypedArray a =
|
|
context.obtainStyledAttributes(android.R.styleable.View);
|
|
initializeScrollbars(a);
|
|
a.recycle();
|
|
commonConstructor(context);
|
|
}
|
|
|
|
private void commonConstructor(Context context) {
|
|
mTextRenderer = null;
|
|
mCursorPaint = new Paint();
|
|
mCursorPaint.setARGB(255,128,128,128);
|
|
mBackgroundPaint = new Paint();
|
|
mTopRow = 0;
|
|
mLeftColumn = 0;
|
|
mGestureDetector = new GestureDetector(context, this, null);
|
|
mGestureDetector.setIsLongpressEnabled(false);
|
|
setVerticalScrollBarEnabled(true);
|
|
}
|
|
|
|
@Override
|
|
protected int computeVerticalScrollRange() {
|
|
return mTranscriptScreen.getActiveRows();
|
|
}
|
|
|
|
@Override
|
|
protected int computeVerticalScrollExtent() {
|
|
return mRows;
|
|
}
|
|
|
|
@Override
|
|
protected int computeVerticalScrollOffset() {
|
|
return mTranscriptScreen.getActiveRows() + mTopRow - mRows;
|
|
}
|
|
|
|
/**
|
|
* Call this to initialize the view.
|
|
*
|
|
* @param termFd the file descriptor
|
|
* @param termOut the output stream for the pseudo-teletype
|
|
*/
|
|
public void initialize(FileDescriptor termFd, FileOutputStream termOut) {
|
|
mTermOut = termOut;
|
|
mTermFd = termFd;
|
|
mTextSize = 10;
|
|
mForeground = Term.WHITE;
|
|
mBackground = Term.BLACK;
|
|
updateText();
|
|
mTermIn = new FileInputStream(mTermFd);
|
|
mReceiveBuffer = new byte[4 * 1024];
|
|
mByteQueue = new ByteQueue(4 * 1024);
|
|
}
|
|
|
|
/**
|
|
* Accept a sequence of bytes (typically from the pseudo-tty) and process
|
|
* them.
|
|
*
|
|
* @param buffer a byte array containing bytes to be processed
|
|
* @param base the index of the first byte in the buffer to process
|
|
* @param length the number of bytes to process
|
|
*/
|
|
public void append(byte[] buffer, int base, int length) {
|
|
mEmulator.append(buffer, base, length);
|
|
ensureCursorVisible();
|
|
invalidate();
|
|
}
|
|
|
|
/**
|
|
* Page the terminal view (scroll it up or down by delta screenfulls.)
|
|
*
|
|
* @param delta the number of screens to scroll. Positive means scroll down,
|
|
* negative means scroll up.
|
|
*/
|
|
public void page(int delta) {
|
|
mTopRow =
|
|
Math.min(0, Math.max(-(mTranscriptScreen
|
|
.getActiveTranscriptRows()), mTopRow + mRows * delta));
|
|
invalidate();
|
|
}
|
|
|
|
/**
|
|
* Page the terminal view horizontally.
|
|
*
|
|
* @param deltaColumns the number of columns to scroll. Positive scrolls to
|
|
* the right.
|
|
*/
|
|
public void pageHorizontal(int deltaColumns) {
|
|
mLeftColumn =
|
|
Math.max(0, Math.min(mLeftColumn + deltaColumns, mColumns
|
|
- mVisibleColumns));
|
|
invalidate();
|
|
}
|
|
|
|
/**
|
|
* Sets the text size, which in turn sets the number of rows and columns
|
|
*
|
|
* @param fontSize the new font size, in pixels.
|
|
*/
|
|
public void setTextSize(int fontSize) {
|
|
mTextSize = fontSize;
|
|
updateText();
|
|
}
|
|
|
|
// Begin GestureDetector.OnGestureListener methods
|
|
|
|
public boolean onSingleTapUp(MotionEvent e) {
|
|
return true;
|
|
}
|
|
|
|
public void onLongPress(MotionEvent e) {
|
|
}
|
|
|
|
public boolean onScroll(MotionEvent e1, MotionEvent e2,
|
|
float distanceX, float distanceY) {
|
|
distanceY += mScrollRemainder;
|
|
int deltaRows = (int) (distanceY / mCharacterHeight);
|
|
mScrollRemainder = distanceY - deltaRows * mCharacterHeight;
|
|
mTopRow =
|
|
Math.min(0, Math.max(-(mTranscriptScreen
|
|
.getActiveTranscriptRows()), mTopRow + deltaRows));
|
|
invalidate();
|
|
|
|
return true;
|
|
}
|
|
|
|
public void onSingleTapConfirmed(MotionEvent e) {
|
|
}
|
|
|
|
public boolean onJumpTapDown(MotionEvent e1, MotionEvent e2) {
|
|
// Scroll to bottom
|
|
mTopRow = 0;
|
|
invalidate();
|
|
return true;
|
|
}
|
|
|
|
public boolean onJumpTapUp(MotionEvent e1, MotionEvent e2) {
|
|
// Scroll to top
|
|
mTopRow = -mTranscriptScreen.getActiveTranscriptRows();
|
|
invalidate();
|
|
return true;
|
|
}
|
|
|
|
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
|
|
float velocityY) {
|
|
// TODO: add animation man's (non animated) fling
|
|
mScrollRemainder = 0.0f;
|
|
onScroll(e1, e2, 2 * velocityX, -2 * velocityY);
|
|
return true;
|
|
}
|
|
|
|
public void onShowPress(MotionEvent e) {
|
|
}
|
|
|
|
public boolean onDown(MotionEvent e) {
|
|
mScrollRemainder = 0.0f;
|
|
return true;
|
|
}
|
|
|
|
// End GestureDetector.OnGestureListener methods
|
|
|
|
@Override public boolean onTouchEvent(MotionEvent ev) {
|
|
return mGestureDetector.onTouchEvent(ev);
|
|
}
|
|
|
|
private void updateText() {
|
|
if (mTextSize > 0) {
|
|
mTextRenderer = new PaintRenderer(mTextSize, mForeground,
|
|
mBackground);
|
|
}
|
|
else {
|
|
mTextRenderer = new Bitmap4x8FontRenderer(getResources(),
|
|
mForeground, mBackground);
|
|
}
|
|
mBackgroundPaint.setColor(mBackground);
|
|
mCharacterWidth = mTextRenderer.getCharacterWidth();
|
|
mCharacterHeight = mTextRenderer.getCharacterHeight();
|
|
|
|
if (mKnownSize) {
|
|
updateSize(getWidth(), getHeight());
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
|
updateSize(w, h);
|
|
if (!mKnownSize) {
|
|
mKnownSize = true;
|
|
|
|
// Set up a thread to read input from the
|
|
// pseudo-teletype:
|
|
|
|
mPollingThread = new Thread(new Runnable() {
|
|
|
|
public void run() {
|
|
try {
|
|
while(true) {
|
|
int read = mTermIn.read(mBuffer);
|
|
mByteQueue.write(mBuffer, 0, read);
|
|
mHandler.sendMessage(
|
|
mHandler.obtainMessage(UPDATE));
|
|
}
|
|
} catch (IOException e) {
|
|
} catch (InterruptedException e) {
|
|
}
|
|
}
|
|
private byte[] mBuffer = new byte[4096];
|
|
});
|
|
mPollingThread.setName("Input reader");
|
|
mPollingThread.start();
|
|
}
|
|
}
|
|
|
|
private void updateSize(int w, int h) {
|
|
mColumns = w / mCharacterWidth;
|
|
mRows = h / mCharacterHeight;
|
|
|
|
// Inform the attached pty of our new size:
|
|
Exec.setPtyWindowSize(mTermFd, mRows, mColumns, w, h);
|
|
|
|
|
|
if (mTranscriptScreen != null) {
|
|
mEmulator.updateSize(mColumns, mRows);
|
|
} else {
|
|
mTranscriptScreen =
|
|
new TranscriptScreen(mColumns, TRANSCRIPT_ROWS, mRows, 0, 7);
|
|
mEmulator =
|
|
new TerminalEmulator(mTranscriptScreen, mColumns, mRows,
|
|
mTermOut);
|
|
}
|
|
|
|
// Reset our paging:
|
|
mTopRow = 0;
|
|
mLeftColumn = 0;
|
|
|
|
invalidate();
|
|
}
|
|
|
|
void updateSize() {
|
|
if (mKnownSize) {
|
|
updateSize(getWidth(), getHeight());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Look for new input from the ptty, send it to the terminal emulator.
|
|
*/
|
|
private void update() {
|
|
int bytesAvailable = mByteQueue.getBytesAvailable();
|
|
int bytesToRead = Math.min(bytesAvailable, mReceiveBuffer.length);
|
|
try {
|
|
int bytesRead = mByteQueue.read(mReceiveBuffer, 0, bytesToRead);
|
|
append(mReceiveBuffer, 0, bytesRead);
|
|
} catch (InterruptedException e) {
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onDraw(Canvas canvas) {
|
|
int w = getWidth();
|
|
int h = getHeight();
|
|
canvas.drawRect(0, 0, w, h, mBackgroundPaint);
|
|
mVisibleColumns = w / mCharacterWidth;
|
|
float x = -mLeftColumn * mCharacterWidth;
|
|
float y = mCharacterHeight;
|
|
int endLine = mTopRow + mRows;
|
|
int cx = mEmulator.getCursorCol();
|
|
int cy = mEmulator.getCursorRow();
|
|
for (int i = mTopRow; i < endLine; i++) {
|
|
int cursorX = -1;
|
|
if (i == cy) {
|
|
cursorX = cx;
|
|
}
|
|
mTranscriptScreen.drawText(i, canvas, x, y, mTextRenderer, cursorX);
|
|
y += mCharacterHeight;
|
|
}
|
|
}
|
|
|
|
private void ensureCursorVisible() {
|
|
mTopRow = 0;
|
|
if (mVisibleColumns > 0) {
|
|
int cx = mEmulator.getCursorCol();
|
|
int visibleCursorX = mEmulator.getCursorCol() - mLeftColumn;
|
|
if (visibleCursorX < 0) {
|
|
mLeftColumn = cx;
|
|
} else if (visibleCursorX >= mVisibleColumns) {
|
|
mLeftColumn = (cx - mVisibleColumns) + 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* An ASCII key listener. Supports control characters and escape. Keeps track of
|
|
* the current state of the alt, shift, and control keys.
|
|
*/
|
|
class TermKeyListener {
|
|
/**
|
|
* The state engine for a modifier key. Can be pressed, released, locked,
|
|
* and so on.
|
|
*
|
|
*/
|
|
private class ModifierKey {
|
|
|
|
private int mState;
|
|
|
|
private static final int UNPRESSED = 0;
|
|
|
|
private static final int PRESSED = 1;
|
|
|
|
private static final int RELEASED = 2;
|
|
|
|
private static final int USED = 3;
|
|
|
|
private static final int LOCKED = 4;
|
|
|
|
/**
|
|
* Construct a modifier key. UNPRESSED by default.
|
|
*
|
|
*/
|
|
public ModifierKey() {
|
|
mState = UNPRESSED;
|
|
}
|
|
|
|
public void onPress() {
|
|
switch (mState) {
|
|
case PRESSED:
|
|
// This is a repeat before use
|
|
break;
|
|
case RELEASED:
|
|
mState = LOCKED;
|
|
break;
|
|
case USED:
|
|
// This is a repeat after use
|
|
break;
|
|
case LOCKED:
|
|
mState = UNPRESSED;
|
|
break;
|
|
default:
|
|
mState = PRESSED;
|
|
break;
|
|
}
|
|
}
|
|
|
|
public void onRelease() {
|
|
switch (mState) {
|
|
case USED:
|
|
mState = UNPRESSED;
|
|
break;
|
|
case PRESSED:
|
|
mState = RELEASED;
|
|
break;
|
|
default:
|
|
// Leave state alone
|
|
break;
|
|
}
|
|
}
|
|
|
|
public void adjustAfterKeypress() {
|
|
switch (mState) {
|
|
case PRESSED:
|
|
mState = USED;
|
|
break;
|
|
case RELEASED:
|
|
mState = UNPRESSED;
|
|
break;
|
|
default:
|
|
// Leave state alone
|
|
break;
|
|
}
|
|
}
|
|
|
|
public boolean isActive() {
|
|
return mState != UNPRESSED;
|
|
}
|
|
}
|
|
|
|
private ModifierKey mAltKey = new ModifierKey();
|
|
|
|
private ModifierKey mCapKey = new ModifierKey();
|
|
|
|
private ModifierKey mControlKey = new ModifierKey();
|
|
|
|
/**
|
|
* Construct a term key listener.
|
|
*
|
|
*/
|
|
public TermKeyListener() {
|
|
}
|
|
|
|
public void handleControlKey(boolean down) {
|
|
if (down) {
|
|
mControlKey.onPress();
|
|
} else {
|
|
mControlKey.onRelease();
|
|
}
|
|
}
|
|
|
|
public int mapControlChar(int ch) {
|
|
int result = ch;
|
|
if (mControlKey.isActive()) {
|
|
// Search is the control key.
|
|
if (result >= 'a' && result <= 'z') {
|
|
result = (char) (result - 'a' + '\001');
|
|
} else if (result == ' ') {
|
|
result = 0;
|
|
} else if ((result == '[') || (result == '1')) {
|
|
result = 27;
|
|
} else if ((result == '\\') || (result == '.')) {
|
|
result = 28;
|
|
} else if ((result == ']') || (result == '0')) {
|
|
result = 29;
|
|
} else if ((result == '^') || (result == '6')) {
|
|
result = 30; // control-^
|
|
} else if ((result == '_') || (result == '5')) {
|
|
result = 31;
|
|
}
|
|
}
|
|
|
|
if (result > -1) {
|
|
mAltKey.adjustAfterKeypress();
|
|
mCapKey.adjustAfterKeypress();
|
|
mControlKey.adjustAfterKeypress();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Handle a keyDown event.
|
|
*
|
|
* @param keyCode the keycode of the keyDown event
|
|
* @return the ASCII byte to transmit to the pseudo-teletype, or -1 if this
|
|
* event does not produce an ASCII byte.
|
|
*/
|
|
public int keyDown(int keyCode, KeyEvent event) {
|
|
int result = -1;
|
|
switch (keyCode) {
|
|
case KeyEvent.KEYCODE_ALT_RIGHT:
|
|
case KeyEvent.KEYCODE_ALT_LEFT:
|
|
mAltKey.onPress();
|
|
break;
|
|
|
|
case KeyEvent.KEYCODE_SHIFT_LEFT:
|
|
case KeyEvent.KEYCODE_SHIFT_RIGHT:
|
|
mCapKey.onPress();
|
|
break;
|
|
|
|
case KeyEvent.KEYCODE_ENTER:
|
|
// Convert newlines into returns. The vt100 sends a
|
|
// '\r' when the 'Return' key is pressed, but our
|
|
// KeyEvent translates this as a '\n'.
|
|
result = '\r';
|
|
break;
|
|
|
|
case KeyEvent.KEYCODE_DEL:
|
|
// Convert DEL into 127 (instead of 8)
|
|
result = 127;
|
|
break;
|
|
|
|
default: {
|
|
result = event.getUnicodeChar(
|
|
(mCapKey.isActive() ? KeyEvent.META_SHIFT_ON : 0) |
|
|
(mAltKey.isActive() ? KeyEvent.META_ALT_ON : 0));
|
|
break;
|
|
}
|
|
}
|
|
|
|
result = mapControlChar(result);
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Handle a keyUp event.
|
|
*
|
|
* @param keyCode the keyCode of the keyUp event
|
|
*/
|
|
public void keyUp(int keyCode) {
|
|
switch (keyCode) {
|
|
case KeyEvent.KEYCODE_ALT_LEFT:
|
|
case KeyEvent.KEYCODE_ALT_RIGHT:
|
|
mAltKey.onRelease();
|
|
break;
|
|
case KeyEvent.KEYCODE_SHIFT_LEFT:
|
|
case KeyEvent.KEYCODE_SHIFT_RIGHT:
|
|
mCapKey.onRelease();
|
|
break;
|
|
default:
|
|
// Ignore other keyUps
|
|
break;
|
|
}
|
|
}
|
|
}
|