2 * Copyright (C) 2007 The Android Open Source Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package jackpal.androidterm;
19 import java.io.FileDescriptor;
20 import java.io.FileInputStream;
21 import java.io.FileOutputStream;
22 import java.io.IOException;
23 import java.io.UnsupportedEncodingException;
24 import java.util.ArrayList;
26 import android.app.Activity;
27 import android.app.AlertDialog;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.content.SharedPreferences;
31 import android.content.res.Configuration;
32 import android.content.res.Resources;
33 import android.graphics.Bitmap;
34 import android.graphics.BitmapFactory;
35 import android.graphics.Canvas;
36 import android.graphics.ColorMatrixColorFilter;
37 import android.graphics.Paint;
38 import android.graphics.PorterDuff;
39 import android.graphics.PorterDuffXfermode;
40 import android.graphics.Rect;
41 import android.graphics.Typeface;
42 import android.net.Uri;
43 import android.os.Bundle;
44 import android.os.Handler;
45 import android.os.Message;
46 import android.preference.PreferenceManager;
47 import android.text.ClipboardManager;
48 import android.util.AttributeSet;
49 import android.util.DisplayMetrics;
50 import android.util.Log;
51 import android.view.ContextMenu;
52 import android.view.ContextMenu.ContextMenuInfo;
53 import android.view.GestureDetector;
54 import android.view.KeyEvent;
55 import android.view.Menu;
56 import android.view.MenuItem;
57 import android.view.MotionEvent;
58 import android.view.View;
59 import android.view.Window;
60 import android.view.WindowManager;
61 import android.view.inputmethod.BaseInputConnection;
62 import android.view.inputmethod.EditorInfo;
63 import android.view.inputmethod.InputConnection;
64 import android.view.inputmethod.InputMethodManager;
67 * A terminal emulator activity.
70 public class Term extends Activity {
72 * Set to true to add debugging code and logging.
74 public static final boolean DEBUG = false;
77 * Set to true to log each character received from the remote process to the
78 * android log, which makes it easier to debug some kinds of problems with
79 * emulating escape sequences and control codes.
81 public static final boolean LOG_CHARACTERS_FLAG = DEBUG && false;
84 * Set to true to log unknown escape sequences.
86 public static final boolean LOG_UNKNOWN_ESCAPE_SEQUENCES = DEBUG && false;
89 * The tag we use when logging, so that our messages can be distinguished
90 * from other messages in the log. Public because it's used by several
93 public static final String LOG_TAG = "Term";
96 * Our main view. Displays the emulated terminal screen.
98 private EmulatorView mEmulatorView;
101 * The pseudo-teletype (pty) file descriptor that we use to communicate with
102 * another process, typically a shell.
104 private FileDescriptor mTermFd;
107 * Used to send data to the remote process.
109 private FileOutputStream mTermOut;
112 * A key listener that tracks the modifier keys and allows the full ASCII
113 * character set to be entered.
115 private TermKeyListener mKeyListener;
118 * The name of our emulator view in the view resource.
120 private static final int EMULATOR_VIEW = R.id.emulatorView;
122 private int mStatusBar = 0;
123 private int mCursorStyle = 0;
124 private int mCursorBlink = 0;
125 private int mFontSize = 9;
126 private int mColorId = 2;
127 private int mControlKeyId = 0;
128 private int mUseCookedIME = 0;
130 private static final String STATUSBAR_KEY = "statusbar";
131 private static final String CURSORSTYLE_KEY = "cursorstyle";
132 private static final String CURSORBLINK_KEY = "cursorblink";
133 private static final String FONTSIZE_KEY = "fontsize";
134 private static final String COLOR_KEY = "color";
135 private static final String CONTROLKEY_KEY = "controlkey";
136 private static final String IME_KEY = "ime";
137 private static final String SHELL_KEY = "shell";
138 private static final String INITIALCOMMAND_KEY = "initialcommand";
140 public static final int WHITE = 0xffffffff;
141 public static final int BLACK = 0xff000000;
142 public static final int BLUE = 0xff344ebd;
143 public static final int GREEN = 0xff00ff00;
144 public static final int AMBER = 0xffffb651;
145 public static final int RED = 0xffff0113;
147 private static final int[][] COLOR_SCHEMES = {
148 {BLACK, WHITE}, {WHITE, BLACK}, {WHITE, BLUE}, {GREEN, BLACK}, {AMBER, BLACK}, {RED, BLACK}};
150 private static final int[] CONTROL_KEY_SCHEMES = {
151 KeyEvent.KEYCODE_DPAD_CENTER,
153 KeyEvent.KEYCODE_ALT_LEFT,
154 KeyEvent.KEYCODE_ALT_RIGHT,
155 KeyEvent.KEYCODE_VOLUME_UP,
156 KeyEvent.KEYCODE_VOLUME_DOWN
158 private static final String[] CONTROL_KEY_NAME = {
159 "Ball", "@", "Left-Alt", "Right-Alt", "Vol-Up", "Vol-Dn"
162 private int mControlKeyCode;
164 private final static String DEFAULT_SHELL = "/system/bin/sh -";
165 private String mShell;
167 private final static String DEFAULT_INITIAL_COMMAND =
168 "export PATH=/data/local/bin:$PATH";
169 private String mInitialCommand;
171 private SharedPreferences mPrefs;
173 private final static int COPY_ALL_ID = 0;
174 private final static int PASTE_ID = 1;
176 private boolean mAlreadyStarted = false;
179 public void onCreate(Bundle icicle) {
180 super.onCreate(icicle);
181 Log.e(Term.LOG_TAG, "onCreate");
182 mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
185 setContentView(R.layout.term_activity);
187 mEmulatorView = (EmulatorView) findViewById(EMULATOR_VIEW);
191 mKeyListener = new TermKeyListener();
193 mEmulatorView.setFocusable(true);
194 mEmulatorView.setFocusableInTouchMode(true);
195 mEmulatorView.requestFocus();
196 mEmulatorView.register(this, mKeyListener);
198 registerForContextMenu(mEmulatorView);
201 mAlreadyStarted = true;
205 public void onDestroy() {
207 if (mTermFd != null) {
213 private void startListening() {
214 int[] processId = new int[1];
216 createSubprocess(processId);
217 final int procId = processId[0];
219 final Handler handler = new Handler() {
221 public void handleMessage(Message msg) {
225 Runnable watchForDeath = new Runnable() {
228 Log.i(Term.LOG_TAG, "waiting for: " + procId);
229 int result = Exec.waitFor(procId);
230 Log.i(Term.LOG_TAG, "Subprocess exited: " + result);
231 handler.sendEmptyMessage(result);
235 Thread watcher = new Thread(watchForDeath);
238 mTermOut = new FileOutputStream(mTermFd);
240 mEmulatorView.initialize(mTermFd, mTermOut);
242 sendInitialCommand();
245 private void sendInitialCommand() {
246 String initialCommand = mInitialCommand;
247 if (initialCommand == null || initialCommand.equals("")) {
248 initialCommand = DEFAULT_INITIAL_COMMAND;
250 if (initialCommand.length() > 0) {
251 write(initialCommand + '\r');
255 private void restart() {
256 startActivity(getIntent());
260 private void write(String data) {
262 mTermOut.write(data.getBytes());
264 } catch (IOException e) {
266 // We don't really care if the receiver isn't listening.
267 // We just make a best effort to answer the query.
271 private void createSubprocess(int[] processId) {
272 String shell = mShell;
273 if (shell == null || shell.equals("")) {
274 shell = DEFAULT_SHELL;
276 ArrayList<String> args = parse(shell);
277 String arg0 = args.get(0);
280 if (args.size() >= 2) {
283 if (args.size() >= 3) {
286 mTermFd = Exec.createSubprocess(arg0, arg1, arg2, processId);
289 private ArrayList<String> parse(String cmd) {
291 final int WHITESPACE = 1;
292 final int INQUOTE = 2;
293 int state = WHITESPACE;
294 ArrayList<String> result = new ArrayList<String>();
295 int cmdLen = cmd.length();
296 StringBuilder builder = new StringBuilder();
297 for (int i = 0; i < cmdLen; i++) {
298 char c = cmd.charAt(i);
299 if (state == PLAIN) {
300 if (Character.isWhitespace(c)) {
301 result.add(builder.toString());
302 builder.delete(0,builder.length());
304 } else if (c == '"') {
309 } else if (state == WHITESPACE) {
310 if (Character.isWhitespace(c)) {
312 } else if (c == '"') {
318 } else if (state == INQUOTE) {
320 if (i + 1 < cmdLen) {
322 builder.append(cmd.charAt(i));
324 } else if (c == '"') {
331 if (builder.length() > 0) {
332 result.add(builder.toString());
337 private void readPrefs() {
338 mStatusBar = readIntPref(STATUSBAR_KEY, mStatusBar, 1);
339 // mCursorStyle = readIntPref(CURSORSTYLE_KEY, mCursorStyle, 2);
340 // mCursorBlink = readIntPref(CURSORBLINK_KEY, mCursorBlink, 1);
341 mFontSize = readIntPref(FONTSIZE_KEY, mFontSize, 20);
342 mColorId = readIntPref(COLOR_KEY, mColorId, COLOR_SCHEMES.length - 1);
343 mControlKeyId = readIntPref(CONTROLKEY_KEY, mControlKeyId,
344 CONTROL_KEY_SCHEMES.length - 1);
345 mUseCookedIME = readIntPref(IME_KEY, mUseCookedIME, 1);
347 String newShell = readStringPref(SHELL_KEY, mShell);
348 if ((newShell == null) || ! newShell.equals(mShell)) {
349 if (mShell != null) {
350 Log.i(Term.LOG_TAG, "New shell set. Restarting.");
357 String newInitialCommand = readStringPref(INITIALCOMMAND_KEY,
359 if ((newInitialCommand == null)
360 || ! newInitialCommand.equals(mInitialCommand)) {
361 if (mInitialCommand != null) {
362 Log.i(Term.LOG_TAG, "New initial command set. Restarting.");
365 mInitialCommand = newInitialCommand;
370 private void updatePrefs() {
371 DisplayMetrics metrics = new DisplayMetrics();
372 getWindowManager().getDefaultDisplay().getMetrics(metrics);
373 mEmulatorView.setTextSize((int) (mFontSize * metrics.density));
374 mEmulatorView.setCursorStyle(mCursorStyle, mCursorBlink);
375 mEmulatorView.setUseCookedIME(mUseCookedIME != 0);
377 mControlKeyCode = CONTROL_KEY_SCHEMES[mControlKeyId];
379 Window win = getWindow();
380 WindowManager.LayoutParams params = win.getAttributes();
381 final int FULLSCREEN = WindowManager.LayoutParams.FLAG_FULLSCREEN;
382 int desiredFlag = mStatusBar != 0 ? 0 : FULLSCREEN;
383 if (desiredFlag != (params.flags & FULLSCREEN)) {
384 if (mAlreadyStarted) {
385 // Can't switch to/from fullscreen after
386 // starting the activity.
389 win.setFlags(desiredFlag, FULLSCREEN);
395 private int readIntPref(String key, int defaultValue, int maxValue) {
398 val = Integer.parseInt(
399 mPrefs.getString(key, Integer.toString(defaultValue)));
400 } catch (NumberFormatException e) {
403 val = Math.max(0, Math.min(val, maxValue));
407 private String readStringPref(String key, String defaultValue) {
408 return mPrefs.getString(key, defaultValue);
411 public int getControlKeyCode() {
412 return mControlKeyCode;
416 public void onResume() {
420 mEmulatorView.onResume();
424 public void onPause() {
426 mEmulatorView.onPause();
430 public void onConfigurationChanged(Configuration newConfig) {
431 super.onConfigurationChanged(newConfig);
433 mEmulatorView.updateSize(true);
437 public boolean onCreateOptionsMenu(Menu menu) {
438 getMenuInflater().inflate(R.menu.main, menu);
443 public boolean onOptionsItemSelected(MenuItem item) {
444 int id = item.getItemId();
445 if (id == R.id.menu_preferences) {
447 } else if (id == R.id.menu_reset) {
449 } else if (id == R.id.menu_send_email) {
451 } else if (id == R.id.menu_special_keys) {
453 } else if (id == R.id.menu_toggle_soft_keyboard) {
454 doToggleSoftKeyboard();
456 return super.onOptionsItemSelected(item);
460 public void onCreateContextMenu(ContextMenu menu, View v,
461 ContextMenuInfo menuInfo) {
462 super.onCreateContextMenu(menu, v, menuInfo);
463 menu.setHeaderTitle(R.string.edit_text);
464 menu.add(0, COPY_ALL_ID, 0, R.string.copy_all);
465 menu.add(0, PASTE_ID, 0, R.string.paste);
467 menu.getItem(PASTE_ID).setEnabled(false);
472 public boolean onContextItemSelected(MenuItem item) {
473 switch (item.getItemId()) {
481 return super.onContextItemSelected(item);
485 private boolean canPaste() {
486 ClipboardManager clip = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
487 if (clip.hasText()) {
493 private void doPreferences() {
494 startActivity(new Intent(this, TermPreferences.class));
497 private void setColors() {
498 int[] scheme = COLOR_SCHEMES[mColorId];
499 mEmulatorView.setColors(scheme[0], scheme[1]);
502 private void doResetTerminal() {
506 private void doEmailTranscript() {
507 // Don't really want to supply an address, but
508 // currently it's required, otherwise we get an
510 String addr = "user@example.com";
512 new Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:"
515 intent.putExtra("body", mEmulatorView.getTranscriptText().trim());
516 startActivity(intent);
519 private void doCopyAll() {
520 ClipboardManager clip = (ClipboardManager)
521 getSystemService(Context.CLIPBOARD_SERVICE);
522 clip.setText(mEmulatorView.getTranscriptText().trim());
525 private void doPaste() {
526 ClipboardManager clip = (ClipboardManager)
527 getSystemService(Context.CLIPBOARD_SERVICE);
528 CharSequence paste = clip.getText();
531 utf8 = paste.toString().getBytes("UTF-8");
532 } catch (UnsupportedEncodingException e) {
533 Log.e(Term.LOG_TAG, "UTF-8 encoding not found.");
537 mTermOut.write(utf8);
538 } catch (IOException e) {
539 Log.e(Term.LOG_TAG, "could not write paste text to terminal.");
543 private void doDocumentKeys() {
544 String controlKey = CONTROL_KEY_NAME[mControlKeyId];
545 new AlertDialog.Builder(this).
546 setTitle("Press " + controlKey + " and Key").
547 setMessage(controlKey + " Space ==> Control-@ (NUL)\n"
548 + controlKey + " A..Z ==> Control-A..Z\n"
549 + controlKey + " 1 ==> Control-[ (ESC)\n"
550 + controlKey + " 5 ==> Control-_\n"
551 + controlKey + " . ==> Control-\\\n"
552 + controlKey + " 0 ==> Control-]\n"
553 + controlKey + " 6 ==> Control-^").
557 private void doToggleSoftKeyboard() {
558 InputMethodManager imm = (InputMethodManager)
559 getSystemService(Context.INPUT_METHOD_SERVICE);
560 imm.toggleSoftInput(InputMethodManager.SHOW_FORCED,0);
567 * An abstract screen interface. A terminal screen stores lines of text. (The
568 * reason to abstract it is to allow different implementations, and to hide
569 * implementation details from clients.)
574 * Set line wrap flag for a given row. Affects how lines are logically
575 * wrapped when changing screen size or converting to a transcript.
577 void setLineWrap(int row);
580 * Store byte b into the screen at location (x, y)
582 * @param x X coordinate (also known as column)
583 * @param y Y coordinate (also known as row)
584 * @param b ASCII character to store
585 * @param foreColor the foreground color
586 * @param backColor the background color
588 void set(int x, int y, byte b, int foreColor, int backColor);
591 * Scroll the screen down one line. To scroll the whole screen of a 24 line
592 * screen, the arguments would be (0, 24).
594 * @param topMargin First line that is scrolled.
595 * @param bottomMargin One line after the last line that is scrolled.
597 void scroll(int topMargin, int bottomMargin, int foreColor, int backColor);
600 * Block copy characters from one position in the screen to another. The two
601 * positions can overlap. All characters of the source and destination must
602 * be within the bounds of the screen, or else an InvalidParemeterException
605 * @param sx source X coordinate
606 * @param sy source Y coordinate
609 * @param dx destination X coordinate
610 * @param dy destination Y coordinate
612 void blockCopy(int sx, int sy, int w, int h, int dx, int dy);
615 * Block set characters. All characters must be within the bounds of the
616 * screen, or else and InvalidParemeterException will be thrown. Typically
617 * this is called with a "val" argument of 32 to clear a block of
624 * @param val value to set.
625 * @param foreColor the foreground color
626 * @param backColor the background color
628 void blockSet(int sx, int sy, int w, int h, int val, int foreColor, int
632 * Get the contents of the transcript buffer as a text string.
634 * @return the contents of the transcript buffer.
636 String getTranscriptText();
643 void resize(int columns, int rows, int foreColor, int backColor);
648 * A TranscriptScreen is a screen that remembers data that's been scrolled. The
649 * old data is stored in a ring buffer to minimize the amount of copying that
650 * needs to be done. The transcript does its own drawing, to avoid having to
651 * expose its internal data structures.
653 class TranscriptScreen implements Screen {
654 private static final String TAG = "TranscriptScreen";
657 * The width of the transcript, in characters. Fixed at initialization.
659 private int mColumns;
662 * The total number of rows in the transcript and the screen. Fixed at
665 private int mTotalRows;
668 * The number of rows in the active portion of the transcript. Doesn't
669 * include the screen.
671 private int mActiveTranscriptRows;
674 * Which row is currently the topmost line of the transcript. Used to
675 * implement a circular buffer.
680 * The number of active rows, includes both the transcript and the screen.
682 private int mActiveRows;
685 * The number of rows in the screen.
687 private int mScreenRows;
690 * The data for both the screen and the transcript. The first mScreenRows *
691 * mLineWidth characters are the screen, the rest are the transcript.
692 * The low byte encodes the ASCII character, the high byte encodes the
693 * foreground and background colors, plus underline and bold.
695 private char[] mData;
698 * The data's stored as color-encoded chars, but the drawing routines require chars, so we
699 * need a temporary buffer to hold a row's worth of characters.
701 private char[] mRowBuffer;
704 * Flags that keep track of whether the current line logically wraps to the
705 * next line. This is used when resizing the screen and when copying to the
706 * clipboard or an email attachment
709 private boolean[] mLineWrap;
712 * Create a transcript screen.
714 * @param columns the width of the screen in characters.
715 * @param totalRows the height of the entire text area, in rows of text.
716 * @param screenRows the height of just the screen, not including the
717 * transcript that holds lines that have scrolled off the top of the
720 public TranscriptScreen(int columns, int totalRows, int screenRows,
721 int foreColor, int backColor) {
722 init(columns, totalRows, screenRows, foreColor, backColor);
725 private void init(int columns, int totalRows, int screenRows, int foreColor, int backColor) {
727 mTotalRows = totalRows;
728 mActiveTranscriptRows = 0;
730 mActiveRows = screenRows;
731 mScreenRows = screenRows;
732 int totalSize = columns * totalRows;
733 mData = new char[totalSize];
734 blockSet(0, 0, mColumns, mScreenRows, ' ', foreColor, backColor);
735 mRowBuffer = new char[columns];
736 mLineWrap = new boolean[totalRows];
741 * Convert a row value from the public external coordinate system to our
742 * internal private coordinate system. External coordinate system:
743 * -mActiveTranscriptRows to mScreenRows-1, with the screen being
744 * 0..mScreenRows-1 Internal coordinate system: 0..mScreenRows-1 rows of
745 * mData are the visible rows. mScreenRows..mActiveRows - 1 are the
746 * transcript, stored as a circular buffer.
748 * @param row a row in the external coordinate system.
749 * @return The row corresponding to the input argument in the private
752 private int externalToInternalRow(int row) {
753 if (row < -mActiveTranscriptRows || row >= mScreenRows) {
754 String errorMessage = "externalToInternalRow "+ row +
755 " " + mActiveTranscriptRows + " " + mScreenRows;
756 Log.e(TAG, errorMessage);
757 throw new IllegalArgumentException(errorMessage);
760 return row; // This is a visible row.
763 + ((mHead + mActiveTranscriptRows + row) % mActiveTranscriptRows);
766 private int getOffset(int externalLine) {
767 return externalToInternalRow(externalLine) * mColumns;
770 private int getOffset(int x, int y) {
771 return getOffset(y) + x;
774 public void setLineWrap(int row) {
775 mLineWrap[externalToInternalRow(row)] = true;
779 * Store byte b into the screen at location (x, y)
781 * @param x X coordinate (also known as column)
782 * @param y Y coordinate (also known as row)
783 * @param b ASCII character to store
784 * @param foreColor the foreground color
785 * @param backColor the background color
787 public void set(int x, int y, byte b, int foreColor, int backColor) {
788 mData[getOffset(x, y)] = encode(b, foreColor, backColor);
791 private char encode(int b, int foreColor, int backColor) {
792 return (char) ((foreColor << 12) | (backColor << 8) | b);
796 * Scroll the screen down one line. To scroll the whole screen of a 24 line
797 * screen, the arguments would be (0, 24).
799 * @param topMargin First line that is scrolled.
800 * @param bottomMargin One line after the last line that is scrolled.
802 public void scroll(int topMargin, int bottomMargin, int foreColor,
804 // Separate out reasons so that stack crawls help us
805 // figure out which condition was violated.
806 if (topMargin > bottomMargin - 1) {
807 throw new IllegalArgumentException();
810 if (topMargin > mScreenRows - 1) {
811 throw new IllegalArgumentException();
814 if (bottomMargin > mScreenRows) {
815 throw new IllegalArgumentException();
818 // Adjust the transcript so that the last line of the transcript
819 // is ready to receive the newly scrolled data
821 int expansionRows = Math.min(1, mTotalRows - mActiveRows);
822 int rollRows = 1 - expansionRows;
823 mActiveRows += expansionRows;
824 mActiveTranscriptRows += expansionRows;
825 if (mActiveTranscriptRows > 0) {
826 mHead = (mHead + rollRows) % mActiveTranscriptRows;
830 // Block move the scroll line to the transcript
831 int topOffset = getOffset(topMargin);
832 int destOffset = getOffset(-1);
833 System.arraycopy(mData, topOffset, mData, destOffset, mColumns);
835 int topLine = externalToInternalRow(topMargin);
836 int destLine = externalToInternalRow(-1);
837 System.arraycopy(mLineWrap, topLine, mLineWrap, destLine, 1);
839 // Block move the scrolled data up
840 int numScrollChars = (bottomMargin - topMargin - 1) * mColumns;
841 System.arraycopy(mData, topOffset + mColumns, mData, topOffset,
843 int numScrollLines = (bottomMargin - topMargin - 1);
844 System.arraycopy(mLineWrap, topLine + 1, mLineWrap, topLine,
847 // Erase the bottom line of the scroll region
848 blockSet(0, bottomMargin - 1, mColumns, 1, ' ', foreColor, backColor);
849 mLineWrap[externalToInternalRow(bottomMargin-1)] = false;
852 private void consistencyCheck() {
853 checkPositive(mColumns);
854 checkPositive(mTotalRows);
855 checkRange(0, mActiveTranscriptRows, mTotalRows);
856 if (mActiveTranscriptRows == 0) {
857 checkEqual(mHead, 0);
859 checkRange(0, mHead, mActiveTranscriptRows-1);
861 checkEqual(mScreenRows + mActiveTranscriptRows, mActiveRows);
862 checkRange(0, mScreenRows, mTotalRows);
864 checkEqual(mTotalRows, mLineWrap.length);
865 checkEqual(mTotalRows*mColumns, mData.length);
866 checkEqual(mColumns, mRowBuffer.length);
869 private void checkPositive(int n) {
871 throw new IllegalArgumentException("checkPositive " + n);
875 private void checkRange(int a, int b, int c) {
876 if (a > b || b > c) {
877 throw new IllegalArgumentException("checkRange " + a + " <= " + b + " <= " + c);
881 private void checkEqual(int a, int b) {
883 throw new IllegalArgumentException("checkEqual " + a + " == " + b);
888 * Block copy characters from one position in the screen to another. The two
889 * positions can overlap. All characters of the source and destination must
890 * be within the bounds of the screen, or else an InvalidParemeterException
893 * @param sx source X coordinate
894 * @param sy source Y coordinate
897 * @param dx destination X coordinate
898 * @param dy destination Y coordinate
900 public void blockCopy(int sx, int sy, int w, int h, int dx, int dy) {
901 if (sx < 0 || sx + w > mColumns || sy < 0 || sy + h > mScreenRows
902 || dx < 0 || dx + w > mColumns || dy < 0
903 || dy + h > mScreenRows) {
904 throw new IllegalArgumentException();
907 // Move in increasing order
908 for (int y = 0; y < h; y++) {
909 int srcOffset = getOffset(sx, sy + y);
910 int dstOffset = getOffset(dx, dy + y);
911 System.arraycopy(mData, srcOffset, mData, dstOffset, w);
914 // Move in decreasing order
915 for (int y = 0; y < h; y++) {
916 int y2 = h - (y + 1);
917 int srcOffset = getOffset(sx, sy + y2);
918 int dstOffset = getOffset(dx, dy + y2);
919 System.arraycopy(mData, srcOffset, mData, dstOffset, w);
925 * Block set characters. All characters must be within the bounds of the
926 * screen, or else and InvalidParemeterException will be thrown. Typically
927 * this is called with a "val" argument of 32 to clear a block of
934 * @param val value to set.
936 public void blockSet(int sx, int sy, int w, int h, int val,
937 int foreColor, int backColor) {
938 if (sx < 0 || sx + w > mColumns || sy < 0 || sy + h > mScreenRows) {
939 throw new IllegalArgumentException();
942 char encodedVal = encode(val, foreColor, backColor);
943 for (int y = 0; y < h; y++) {
944 int offset = getOffset(sx, sy + y);
945 for (int x = 0; x < w; x++) {
946 data[offset + x] = encodedVal;
952 * Draw a row of text. Out-of-bounds rows are blank, not errors.
954 * @param row The row of text to draw.
955 * @param canvas The canvas to draw to.
956 * @param x The x coordinate origin of the drawing
957 * @param y The y coordinate origin of the drawing
958 * @param renderer The renderer to use to draw the text
959 * @param cx the cursor X coordinate, -1 means don't draw it
961 public final void drawText(int row, Canvas canvas, float x, float y,
962 TextRenderer renderer, int cx) {
964 // Out-of-bounds rows are blank.
965 if (row < -mActiveTranscriptRows || row >= mScreenRows) {
969 // Copy the data from the byte array to a char array so they can
972 int offset = getOffset(row);
973 char[] rowBuffer = mRowBuffer;
975 int columns = mColumns;
977 int lastRunStart = -1;
978 final int CURSOR_MASK = 0x10000;
979 for (int i = 0; i < columns; i++) {
980 char c = data[offset + i];
981 int colors = (char) (c & 0xff00);
983 // Set cursor background color:
984 colors |= CURSOR_MASK;
986 rowBuffer[i] = (char) (c & 0x00ff);
987 if (colors != lastColors) {
988 if (lastRunStart >= 0) {
989 renderer.drawTextRun(canvas, x, y, lastRunStart, rowBuffer,
990 lastRunStart, i - lastRunStart,
991 (lastColors & CURSOR_MASK) != 0,
992 0xf & (lastColors >> 12), 0xf & (lastColors >> 8));
998 if (lastRunStart >= 0) {
999 renderer.drawTextRun(canvas, x, y, lastRunStart, rowBuffer,
1000 lastRunStart, columns - lastRunStart,
1001 (lastColors & CURSOR_MASK) != 0,
1002 0xf & (lastColors >> 12), 0xf & (lastColors >> 8));
1007 * Get the count of active rows.
1009 * @return the count of active rows.
1011 public int getActiveRows() {
1016 * Get the count of active transcript rows.
1018 * @return the count of active transcript rows.
1020 public int getActiveTranscriptRows() {
1021 return mActiveTranscriptRows;
1024 public String getTranscriptText() {
1025 return internalGetTranscriptText(true);
1028 private String internalGetTranscriptText(boolean stripColors) {
1029 StringBuilder builder = new StringBuilder();
1030 char[] rowBuffer = mRowBuffer;
1031 char[] data = mData;
1032 int columns = mColumns;
1033 for (int row = -mActiveTranscriptRows; row < mScreenRows; row++) {
1034 int offset = getOffset(row);
1035 int lastPrintingChar = -1;
1036 for (int column = 0; column < columns; column++) {
1037 char c = data[offset + column];
1039 c = (char) (c & 0xff);
1041 if ((c & 0xff) != ' ') {
1042 lastPrintingChar = column;
1044 rowBuffer[column] = c;
1046 if (mLineWrap[externalToInternalRow(row)]) {
1047 builder.append(rowBuffer, 0, columns);
1049 builder.append(rowBuffer, 0, lastPrintingChar + 1);
1050 builder.append('\n');
1053 return builder.toString();
1056 public void resize(int columns, int rows, int foreColor, int backColor) {
1057 init(columns, mTotalRows, rows, foreColor, backColor);
1062 * Renders text into a screen. Contains all the terminal-specific knowlege and
1063 * state. Emulates a subset of the X Window System xterm terminal, which in turn
1064 * is an emulator for a subset of the Digital Equipment Corporation vt100
1065 * terminal. Missing functionality: text attributes (bold, underline, reverse
1066 * video, color) alternate screen cursor key and keypad escape sequences.
1068 class TerminalEmulator {
1071 * The cursor row. Numbered 0..mRows-1.
1073 private int mCursorRow;
1076 * The cursor column. Numbered 0..mColumns-1.
1078 private int mCursorCol;
1081 * The number of character rows in the terminal screen.
1086 * The number of character columns in the terminal screen.
1088 private int mColumns;
1091 * Used to send data to the remote process. Needed to implement the various
1092 * "report" escape sequences.
1094 private FileOutputStream mTermOut;
1097 * Stores the characters that appear on the screen of the emulated terminal.
1099 private Screen mScreen;
1102 * Keeps track of the current argument of the current escape sequence.
1103 * Ranges from 0 to MAX_ESCAPE_PARAMETERS-1. (Typically just 0 or 1.)
1105 private int mArgIndex;
1108 * The number of parameter arguments. This name comes from the ANSI standard
1109 * for terminal escape codes.
1111 private static final int MAX_ESCAPE_PARAMETERS = 16;
1114 * Holds the arguments of the current escape sequence.
1116 private int[] mArgs = new int[MAX_ESCAPE_PARAMETERS];
1118 // Escape processing states:
1121 * Escape processing state: Not currently in an escape sequence.
1123 private static final int ESC_NONE = 0;
1126 * Escape processing state: Have seen an ESC character
1128 private static final int ESC = 1;
1131 * Escape processing state: Have seen ESC POUND
1133 private static final int ESC_POUND = 2;
1136 * Escape processing state: Have seen ESC and a character-set-select char
1138 private static final int ESC_SELECT_LEFT_PAREN = 3;
1141 * Escape processing state: Have seen ESC and a character-set-select char
1143 private static final int ESC_SELECT_RIGHT_PAREN = 4;
1146 * Escape processing state: ESC [
1148 private static final int ESC_LEFT_SQUARE_BRACKET = 5;
1151 * Escape processing state: ESC [ ?
1153 private static final int ESC_LEFT_SQUARE_BRACKET_QUESTION_MARK = 6;
1156 * True if the current escape sequence should continue, false if the current
1157 * escape sequence should be terminated. Used when parsing a single
1160 private boolean mContinueSequence;
1163 * The current state of the escape sequence state machine.
1165 private int mEscapeState;
1168 * Saved state of the cursor row, Used to implement the save/restore cursor
1169 * position escape sequences.
1171 private int mSavedCursorRow;
1174 * Saved state of the cursor column, Used to implement the save/restore
1175 * cursor position escape sequences.
1177 private int mSavedCursorCol;
1182 * This mask indicates 132-column mode is set. (As opposed to 80-column
1185 private static final int K_132_COLUMN_MODE_MASK = 1 << 3;
1188 * This mask indicates that origin mode is set. (Cursor addressing is
1189 * relative to the absolute screen size, rather than the currently set top
1190 * and bottom margins.)
1192 private static final int K_ORIGIN_MODE_MASK = 1 << 6;
1195 * This mask indicates that wraparound mode is set. (As opposed to
1196 * stop-at-right-column mode.)
1198 private static final int K_WRAPAROUND_MODE_MASK = 1 << 7;
1201 * Holds multiple DECSET flags. The data is stored this way, rather than in
1202 * separate booleans, to make it easier to implement the save-and-restore
1203 * semantics. The various k*ModeMask masks can be used to extract and modify
1204 * the individual flags current states.
1206 private int mDecFlags;
1209 * Saves away a snapshot of the DECSET flags. Used to implement save and
1210 * restore escape sequences.
1212 private int mSavedDecFlags;
1214 // Modes set with Set Mode / Reset Mode
1217 * True if insert mode (as opposed to replace mode) is active. In insert
1218 * mode new characters are inserted, pushing existing text to the right.
1220 private boolean mInsertMode;
1223 * Automatic newline mode. Configures whether pressing return on the
1224 * keyboard automatically generates a return as well. Not currently
1227 private boolean mAutomaticNewlineMode;
1230 * An array of tab stops. mTabStop[i] is true if there is a tab stop set for
1233 private boolean[] mTabStop;
1235 // The margins allow portions of the screen to be locked.
1238 * The top margin of the screen, for scrolling purposes. Ranges from 0 to
1241 private int mTopMargin;
1244 * The bottom margin of the screen, for scrolling purposes. Ranges from
1245 * mTopMargin + 2 to mRows. (Defines the first row after the scrolling
1248 private int mBottomMargin;
1251 * True if the next character to be emitted will be automatically wrapped to
1252 * the next line. Used to disambiguate the case where the cursor is
1253 * positioned on column mColumns-1.
1255 private boolean mAboutToAutoWrap;
1258 * Used for debugging, counts how many chars have been processed.
1260 private int mProcessedCharCount;
1263 * Foreground color, 0..7, mask with 8 for bold
1265 private int mForeColor;
1268 * Background color, 0..7, mask with 8 for underline
1270 private int mBackColor;
1272 private boolean mInverseColors;
1274 private boolean mbKeypadApplicationMode;
1276 private boolean mAlternateCharSet;
1279 * Construct a terminal emulator that uses the supplied screen
1281 * @param screen the screen to render characters into.
1282 * @param columns the number of columns to emulate
1283 * @param rows the number of rows to emulate
1284 * @param termOut the output file descriptor that talks to the pseudo-tty.
1286 public TerminalEmulator(Screen screen, int columns, int rows,
1287 FileOutputStream termOut) {
1291 mTabStop = new boolean[mColumns];
1296 public void updateSize(int columns, int rows) {
1297 if (mRows == rows && mColumns == columns) {
1301 throw new IllegalArgumentException("rows:" + columns);
1305 throw new IllegalArgumentException("rows:" + rows);
1308 String transcriptText = mScreen.getTranscriptText();
1310 mScreen.resize(columns, rows, mForeColor, mBackColor);
1312 if (mRows != rows) {
1315 mBottomMargin = mRows;
1317 if (mColumns != columns) {
1318 int oldColumns = mColumns;
1320 boolean[] oldTabStop = mTabStop;
1321 mTabStop = new boolean[mColumns];
1322 int toTransfer = Math.min(oldColumns, columns);
1323 System.arraycopy(oldTabStop, 0, mTabStop, 0, toTransfer);
1324 while (mCursorCol >= columns) {
1325 mCursorCol -= columns;
1326 mCursorRow = Math.min(mBottomMargin-1, mCursorRow + 1);
1331 mAboutToAutoWrap = false;
1333 int end = transcriptText.length()-1;
1334 while ((end >= 0) && transcriptText.charAt(end) == '\n') {
1337 for(int i = 0; i <= end; i++) {
1338 byte c = (byte) transcriptText.charAt(i);
1349 * Get the cursor's current row.
1351 * @return the cursor's current row.
1353 public final int getCursorRow() {
1358 * Get the cursor's current column.
1360 * @return the cursor's current column.
1362 public final int getCursorCol() {
1366 public final boolean getKeypadApplicationMode() {
1367 return mbKeypadApplicationMode;
1370 private void setDefaultTabStops() {
1371 for (int i = 0; i < mColumns; i++) {
1372 mTabStop[i] = (i & 7) == 0 && i != 0;
1377 * Accept bytes (typically from the pseudo-teletype) and process them.
1379 * @param buffer a byte array containing the bytes to be processed
1380 * @param base the first index of the array to process
1381 * @param length the number of bytes in the array to process
1383 public void append(byte[] buffer, int base, int length) {
1384 for (int i = 0; i < length; i++) {
1385 byte b = buffer[base + i];
1387 if (Term.LOG_CHARACTERS_FLAG) {
1388 char printableB = (char) b;
1389 if (b < 32 || b > 126) {
1392 Log.w(Term.LOG_TAG, "'" + Character.toString(printableB)
1393 + "' (" + Integer.toString(b) + ")");
1396 mProcessedCharCount++;
1397 } catch (Exception e) {
1398 Log.e(Term.LOG_TAG, "Exception while processing character "
1399 + Integer.toString(mProcessedCharCount) + " code "
1400 + Integer.toString(b), e);
1405 private void process(byte b) {
1416 setCursorCol(Math.max(0, mCursorCol - 1));
1420 // Move to next tab stop, but not past edge of screen
1421 setCursorCol(nextTabStop(mCursorCol));
1435 setAltCharSet(true);
1439 setAltCharSet(false);
1445 if (mEscapeState != ESC_NONE) {
1446 mEscapeState = ESC_NONE;
1452 // Always starts an escape sequence
1453 startEscapeSequence(ESC);
1456 case (byte) 0x9b: // CSI
1457 startEscapeSequence(ESC_LEFT_SQUARE_BRACKET);
1461 mContinueSequence = false;
1462 switch (mEscapeState) {
1477 case ESC_SELECT_LEFT_PAREN:
1478 doEscSelectLeftParen(b);
1481 case ESC_SELECT_RIGHT_PAREN:
1482 doEscSelectRightParen(b);
1485 case ESC_LEFT_SQUARE_BRACKET:
1486 doEscLeftSquareBracket(b);
1489 case ESC_LEFT_SQUARE_BRACKET_QUESTION_MARK:
1497 if (!mContinueSequence) {
1498 mEscapeState = ESC_NONE;
1504 private void setAltCharSet(boolean alternateCharSet) {
1505 mAlternateCharSet = alternateCharSet;
1508 private int nextTabStop(int cursorCol) {
1509 for (int i = cursorCol; i < mColumns; i++) {
1514 return mColumns - 1;
1517 private void doEscLSBQuest(byte b) {
1518 int mask = getDecFlagsMask(getArg0(0));
1520 case 'h': // Esc [ ? Pn h - DECSET
1524 case 'l': // Esc [ ? Pn l - DECRST
1528 case 'r': // Esc [ ? Pn r - restore
1529 mDecFlags = (mDecFlags & ~mask) | (mSavedDecFlags & mask);
1532 case 's': // Esc [ ? Pn s - save
1533 mSavedDecFlags = (mSavedDecFlags & ~mask) | (mDecFlags & mask);
1542 if ((mask & K_132_COLUMN_MODE_MASK) != 0) {
1543 // We don't actually set 132 cols, but we do want the
1544 // side effect of clearing the screen and homing the cursor.
1545 blockClear(0, 0, mColumns, mRows);
1546 setCursorRowCol(0, 0);
1550 if ((mask & K_ORIGIN_MODE_MASK) != 0) {
1552 setCursorPosition(0, 0);
1556 private int getDecFlagsMask(int argument) {
1557 if (argument >= 1 && argument <= 9) {
1558 return (1 << argument);
1564 private void startEscapeSequence(int escapeState) {
1565 mEscapeState = escapeState;
1567 for (int j = 0; j < MAX_ESCAPE_PARAMETERS; j++) {
1572 private void doLinefeed() {
1573 int newCursorRow = mCursorRow + 1;
1574 if (newCursorRow >= mBottomMargin) {
1576 newCursorRow = mBottomMargin - 1;
1578 setCursorRow(newCursorRow);
1581 private void continueSequence() {
1582 mContinueSequence = true;
1585 private void continueSequence(int state) {
1586 mEscapeState = state;
1587 mContinueSequence = true;
1590 private void doEscSelectLeftParen(byte b) {
1591 doSelectCharSet(true, b);
1594 private void doEscSelectRightParen(byte b) {
1595 doSelectCharSet(false, b);
1598 private void doSelectCharSet(boolean isG0CharSet, byte b) {
1600 case 'A': // United Kingdom character set
1602 case 'B': // ASCII set
1604 case '0': // Special Graphics
1606 case '1': // Alternate character set
1615 private void doEscPound(byte b) {
1617 case '8': // Esc # 8 - DECALN alignment test
1618 mScreen.blockSet(0, 0, mColumns, mRows, 'E',
1619 getForeColor(), getBackColor());
1628 private void doEsc(byte b) {
1631 continueSequence(ESC_POUND);
1635 continueSequence(ESC_SELECT_LEFT_PAREN);
1639 continueSequence(ESC_SELECT_RIGHT_PAREN);
1642 case '7': // DECSC save cursor
1643 mSavedCursorRow = mCursorRow;
1644 mSavedCursorCol = mCursorCol;
1647 case '8': // DECRC restore cursor
1648 setCursorRowCol(mSavedCursorRow, mSavedCursorCol);
1660 case 'F': // Cursor to lower-left corner of screen
1661 setCursorRowCol(0, mBottomMargin - 1);
1664 case 'H': // Tab set
1665 mTabStop[mCursorCol] = true;
1668 case 'M': // Reverse index
1669 if (mCursorRow == 0) {
1670 mScreen.blockCopy(0, mTopMargin + 1, mColumns, mBottomMargin
1671 - (mTopMargin + 1), 0, mTopMargin);
1672 blockClear(0, mBottomMargin - 1, mColumns);
1680 unimplementedSequence(b);
1684 unimplementedSequence(b);
1687 case 'P': // Device control string
1688 unimplementedSequence(b);
1691 case 'Z': // return terminal ID
1692 sendDeviceAttributes();
1696 continueSequence(ESC_LEFT_SQUARE_BRACKET);
1699 case '=': // DECKPAM
1700 mbKeypadApplicationMode = true;
1703 case '>' : // DECKPNM
1704 mbKeypadApplicationMode = false;
1713 private void doEscLeftSquareBracket(byte b) {
1715 case '@': // ESC [ Pn @ - ICH Insert Characters
1717 int charsAfterCursor = mColumns - mCursorCol;
1718 int charsToInsert = Math.min(getArg0(1), charsAfterCursor);
1719 int charsToMove = charsAfterCursor - charsToInsert;
1720 mScreen.blockCopy(mCursorCol, mCursorRow, charsToMove, 1,
1721 mCursorCol + charsToInsert, mCursorRow);
1722 blockClear(mCursorCol, mCursorRow, charsToInsert);
1726 case 'A': // ESC [ Pn A - Cursor Up
1727 setCursorRow(Math.max(mTopMargin, mCursorRow - getArg0(1)));
1730 case 'B': // ESC [ Pn B - Cursor Down
1731 setCursorRow(Math.min(mBottomMargin - 1, mCursorRow + getArg0(1)));
1734 case 'C': // ESC [ Pn C - Cursor Right
1735 setCursorCol(Math.min(mColumns - 1, mCursorCol + getArg0(1)));
1738 case 'D': // ESC [ Pn D - Cursor Left
1739 setCursorCol(Math.max(0, mCursorCol - getArg0(1)));
1742 case 'G': // ESC [ Pn G - Cursor Horizontal Absolute
1743 setCursorCol(Math.min(Math.max(1, getArg0(1)), mColumns) - 1);
1746 case 'H': // ESC [ Pn ; H - Cursor Position
1747 setHorizontalVerticalPosition();
1750 case 'J': // ESC [ Pn J - Erase in Display
1751 switch (getArg0(0)) {
1752 case 0: // Clear below
1753 blockClear(mCursorCol, mCursorRow, mColumns - mCursorCol);
1754 blockClear(0, mCursorRow + 1, mColumns,
1755 mBottomMargin - (mCursorRow + 1));
1758 case 1: // Erase from the start of the screen to the cursor.
1759 blockClear(0, mTopMargin, mColumns, mCursorRow - mTopMargin);
1760 blockClear(0, mCursorRow, mCursorCol + 1);
1763 case 2: // Clear all
1764 blockClear(0, mTopMargin, mColumns, mBottomMargin - mTopMargin);
1773 case 'K': // ESC [ Pn K - Erase in Line
1774 switch (getArg0(0)) {
1775 case 0: // Clear to right
1776 blockClear(mCursorCol, mCursorRow, mColumns - mCursorCol);
1779 case 1: // Erase start of line to cursor (including cursor)
1780 blockClear(0, mCursorRow, mCursorCol + 1);
1783 case 2: // Clear whole line
1784 blockClear(0, mCursorRow, mColumns);
1793 case 'L': // Insert Lines
1795 int linesAfterCursor = mBottomMargin - mCursorRow;
1796 int linesToInsert = Math.min(getArg0(1), linesAfterCursor);
1797 int linesToMove = linesAfterCursor - linesToInsert;
1798 mScreen.blockCopy(0, mCursorRow, mColumns, linesToMove, 0,
1799 mCursorRow + linesToInsert);
1800 blockClear(0, mCursorRow, mColumns, linesToInsert);
1804 case 'M': // Delete Lines
1806 int linesAfterCursor = mBottomMargin - mCursorRow;
1807 int linesToDelete = Math.min(getArg0(1), linesAfterCursor);
1808 int linesToMove = linesAfterCursor - linesToDelete;
1809 mScreen.blockCopy(0, mCursorRow + linesToDelete, mColumns,
1810 linesToMove, 0, mCursorRow);
1811 blockClear(0, mCursorRow + linesToMove, mColumns, linesToDelete);
1815 case 'P': // Delete Characters
1817 int charsAfterCursor = mColumns - mCursorCol;
1818 int charsToDelete = Math.min(getArg0(1), charsAfterCursor);
1819 int charsToMove = charsAfterCursor - charsToDelete;
1820 mScreen.blockCopy(mCursorCol + charsToDelete, mCursorRow,
1821 charsToMove, 1, mCursorCol, mCursorRow);
1822 blockClear(mCursorCol + charsToMove, mCursorRow, charsToDelete);
1826 case 'T': // Mouse tracking
1827 unimplementedSequence(b);
1830 case '?': // Esc [ ? -- start of a private mode set
1831 continueSequence(ESC_LEFT_SQUARE_BRACKET_QUESTION_MARK);
1834 case 'c': // Send device attributes
1835 sendDeviceAttributes();
1838 case 'd': // ESC [ Pn d - Vert Position Absolute
1839 setCursorRow(Math.min(Math.max(1, getArg0(1)), mRows) - 1);
1842 case 'f': // Horizontal and Vertical Position
1843 setHorizontalVerticalPosition();
1846 case 'g': // Clear tab stop
1847 switch (getArg0(0)) {
1849 mTabStop[mCursorCol] = false;
1853 for (int i = 0; i < mColumns; i++) {
1854 mTabStop[i] = false;
1859 // Specified to have no effect.
1864 case 'h': // Set Mode
1868 case 'l': // Reset Mode
1872 case 'm': // Esc [ Pn m - character attributes.
1873 selectGraphicRendition();
1876 case 'r': // Esc [ Pn ; Pn r - set top and bottom margins
1878 // The top margin defaults to 1, the bottom margin
1879 // (unusually for arguments) defaults to mRows.
1881 // The escape sequence numbers top 1..23, but we
1882 // number top 0..22.
1883 // The escape sequence numbers bottom 2..24, and
1884 // so do we (because we use a zero based numbering
1885 // scheme, but we store the first line below the
1886 // bottom-most scrolling line.
1887 // As a result, we adjust the top line by -1, but
1888 // we leave the bottom line alone.
1890 // Also require that top + 2 <= bottom
1892 int top = Math.max(0, Math.min(getArg0(1) - 1, mRows - 2));
1893 int bottom = Math.max(top + 2, Math.min(getArg1(mRows), mRows));
1895 mBottomMargin = bottom;
1897 // The cursor is placed in the home position
1898 setCursorRowCol(mTopMargin, 0);
1908 private void selectGraphicRendition() {
1909 for (int i = 0; i <= mArgIndex; i++) {
1910 int code = mArgs[i];
1912 if (mArgIndex > 0) {
1918 if (code == 0) { // reset
1919 mInverseColors = false;
1922 } else if (code == 1) { // bold
1924 } else if (code == 4) { // underscore
1926 } else if (code == 7) { // inverse
1927 mInverseColors = true;
1928 } else if (code >= 30 && code <= 37) { // foreground color
1929 mForeColor = (mForeColor & 0x8) | (code - 30);
1930 } else if (code >= 40 && code <= 47) { // background color
1931 mBackColor = (mBackColor & 0x8) | (code - 40);
1933 if (Term.LOG_UNKNOWN_ESCAPE_SEQUENCES) {
1934 Log.w(Term.LOG_TAG, String.format("SGR unknown code %d", code));
1940 private void blockClear(int sx, int sy, int w) {
1941 blockClear(sx, sy, w, 1);
1944 private void blockClear(int sx, int sy, int w, int h) {
1945 mScreen.blockSet(sx, sy, w, h, ' ', getForeColor(), getBackColor());
1948 private int getForeColor() {
1949 return mInverseColors ?
1950 ((mBackColor & 0x7) | (mForeColor & 0x8)) : mForeColor;
1953 private int getBackColor() {
1954 return mInverseColors ?
1955 ((mForeColor & 0x7) | (mBackColor & 0x8)) : mBackColor;
1958 private void doSetMode(boolean newValue) {
1959 int modeBit = getArg0(0);
1962 mInsertMode = newValue;
1966 mAutomaticNewlineMode = newValue;
1970 unknownParameter(modeBit);
1975 private void setHorizontalVerticalPosition() {
1977 // Parameters are Row ; Column
1979 setCursorPosition(getArg1(1) - 1, getArg0(1) - 1);
1982 private void setCursorPosition(int x, int y) {
1983 int effectiveTopMargin = 0;
1984 int effectiveBottomMargin = mRows;
1985 if ((mDecFlags & K_ORIGIN_MODE_MASK) != 0) {
1986 effectiveTopMargin = mTopMargin;
1987 effectiveBottomMargin = mBottomMargin;
1990 Math.max(effectiveTopMargin, Math.min(effectiveTopMargin + y,
1991 effectiveBottomMargin - 1));
1992 int newCol = Math.max(0, Math.min(x, mColumns - 1));
1993 setCursorRowCol(newRow, newCol);
1996 private void sendDeviceAttributes() {
1997 // This identifies us as a DEC vt100 with advanced
1998 // video options. This is what the xterm terminal
2003 (byte) 27, (byte) '[', (byte) '?', (byte) '1',
2004 (byte) ';', (byte) '2', (byte) 'c'
2007 (byte) 27, (byte) '[', (byte) '?', (byte) '6',
2008 (byte) '0', (byte) ';',
2009 (byte) '1', (byte) ';',
2010 (byte) '2', (byte) ';',
2011 (byte) '6', (byte) ';',
2012 (byte) '8', (byte) ';',
2013 (byte) '9', (byte) ';',
2014 (byte) '1', (byte) '5', (byte) ';',
2023 * Send data to the shell process
2026 private void write(byte[] data) {
2028 mTermOut.write(data);
2030 } catch (IOException e) {
2032 // We don't really care if the receiver isn't listening.
2033 // We just make a best effort to answer the query.
2037 private void scroll() {
2038 mScreen.scroll(mTopMargin, mBottomMargin,
2039 getForeColor(), getBackColor());
2043 * Process the next ASCII character of a parameter.
2045 * @param b The next ASCII character of the paramater sequence.
2047 private void parseArg(byte b) {
2048 if (b >= '0' && b <= '9') {
2049 if (mArgIndex < mArgs.length) {
2050 int oldValue = mArgs[mArgIndex];
2051 int thisDigit = b - '0';
2053 if (oldValue >= 0) {
2054 value = oldValue * 10 + thisDigit;
2058 mArgs[mArgIndex] = value;
2061 } else if (b == ';') {
2062 if (mArgIndex < mArgs.length) {
2071 private int getArg0(int defaultValue) {
2072 return getArg(0, defaultValue);
2075 private int getArg1(int defaultValue) {
2076 return getArg(1, defaultValue);
2079 private int getArg(int index, int defaultValue) {
2080 int result = mArgs[index];
2082 result = defaultValue;
2087 private void unimplementedSequence(byte b) {
2088 if (Term.LOG_UNKNOWN_ESCAPE_SEQUENCES) {
2089 logError("unimplemented", b);
2094 private void unknownSequence(byte b) {
2095 if (Term.LOG_UNKNOWN_ESCAPE_SEQUENCES) {
2096 logError("unknown", b);
2101 private void unknownParameter(int parameter) {
2102 if (Term.LOG_UNKNOWN_ESCAPE_SEQUENCES) {
2103 StringBuilder buf = new StringBuilder();
2104 buf.append("Unknown parameter");
2105 buf.append(parameter);
2106 logError(buf.toString());
2110 private void logError(String errorType, byte b) {
2111 if (Term.LOG_UNKNOWN_ESCAPE_SEQUENCES) {
2112 StringBuilder buf = new StringBuilder();
2113 buf.append(errorType);
2114 buf.append(" sequence ");
2115 buf.append(" EscapeState: ");
2116 buf.append(mEscapeState);
2117 buf.append(" char: '");
2118 buf.append((char) b);
2122 boolean firstArg = true;
2123 for (int i = 0; i <= mArgIndex; i++) {
2124 int value = mArgs[i];
2128 buf.append("args = ");
2130 buf.append(String.format("%d; ", value));
2133 logError(buf.toString());
2137 private void logError(String error) {
2138 if (Term.LOG_UNKNOWN_ESCAPE_SEQUENCES) {
2139 Log.e(Term.LOG_TAG, error);
2144 private void finishSequence() {
2145 mEscapeState = ESC_NONE;
2148 private boolean autoWrapEnabled() {
2149 // Always enable auto wrap, because it's useful on a small screen
2151 // return (mDecFlags & K_WRAPAROUND_MODE_MASK) != 0;
2155 * Send an ASCII character to the screen.
2157 * @param b the ASCII character to display.
2159 private void emit(byte b) {
2160 boolean autoWrap = autoWrapEnabled();
2163 if (mCursorCol == mColumns - 1 && mAboutToAutoWrap) {
2164 mScreen.setLineWrap(mCursorRow);
2166 if (mCursorRow + 1 < mBottomMargin) {
2174 if (mInsertMode) { // Move character to right one space
2175 int destCol = mCursorCol + 1;
2176 if (destCol < mColumns) {
2177 mScreen.blockCopy(mCursorCol, mCursorRow, mColumns - destCol,
2178 1, destCol, mCursorRow);
2182 mScreen.set(mCursorCol, mCursorRow, b, getForeColor(), getBackColor());
2185 mAboutToAutoWrap = (mCursorCol == mColumns - 1);
2188 mCursorCol = Math.min(mCursorCol + 1, mColumns - 1);
2191 private void setCursorRow(int row) {
2193 mAboutToAutoWrap = false;
2196 private void setCursorCol(int col) {
2198 mAboutToAutoWrap = false;
2201 private void setCursorRowCol(int row, int col) {
2202 mCursorRow = Math.min(row, mRows-1);
2203 mCursorCol = Math.min(col, mColumns-1);
2204 mAboutToAutoWrap = false;
2208 * Reset the terminal emulator to its initial state.
2210 public void reset() {
2214 mContinueSequence = false;
2215 mEscapeState = ESC_NONE;
2216 mSavedCursorRow = 0;
2217 mSavedCursorCol = 0;
2220 mInsertMode = false;
2221 mAutomaticNewlineMode = false;
2223 mBottomMargin = mRows;
2224 mAboutToAutoWrap = false;
2227 mInverseColors = false;
2228 mbKeypadApplicationMode = false;
2229 mAlternateCharSet = false;
2230 // mProcessedCharCount is preserved unchanged.
2231 setDefaultTabStops();
2232 blockClear(0, 0, mColumns, mRows);
2235 public String getTranscriptText() {
2236 return mScreen.getTranscriptText();
2241 * Text renderer interface
2244 interface TextRenderer {
2245 int getCharacterWidth();
2246 int getCharacterHeight();
2247 void drawTextRun(Canvas canvas, float x, float y,
2248 int lineOffset, char[] text,
2249 int index, int count, boolean cursor, int foreColor, int backColor);
2252 abstract class BaseTextRenderer implements TextRenderer {
2253 protected int[] mForePaint = {
2254 0xff000000, // Black
2256 0xff00ff00, // green
2257 0xffffff00, // yellow
2259 0xffff00ff, // magenta
2261 0xffffffff // white -- is overridden by constructor
2263 protected int[] mBackPaint = {
2264 0xff000000, // Black -- is overridden by constructor
2266 0xff00cc00, // green
2267 0xffcccc00, // yellow
2269 0xffff00cc, // magenta
2273 protected final static int mCursorPaint = 0xff808080;
2275 public BaseTextRenderer(int forePaintColor, int backPaintColor) {
2276 mForePaint[7] = forePaintColor;
2277 mBackPaint[0] = backPaintColor;
2282 class Bitmap4x8FontRenderer extends BaseTextRenderer {
2283 private final static int kCharacterWidth = 4;
2284 private final static int kCharacterHeight = 8;
2285 private Bitmap mFont;
2286 private int mCurrentForeColor;
2287 private int mCurrentBackColor;
2288 private float[] mColorMatrix;
2289 private Paint mPaint;
2290 private static final float BYTE_SCALE = 1.0f / 255.0f;
2292 public Bitmap4x8FontRenderer(Resources resources,
2293 int forePaintColor, int backPaintColor) {
2294 super(forePaintColor, backPaintColor);
2295 mFont = BitmapFactory.decodeResource(resources,
2296 R.drawable.atari_small);
2297 mPaint = new Paint();
2298 mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
2301 public int getCharacterWidth() {
2302 return kCharacterWidth;
2305 public int getCharacterHeight() {
2306 return kCharacterHeight;
2309 public void drawTextRun(Canvas canvas, float x, float y,
2310 int lineOffset, char[] text, int index, int count,
2311 boolean cursor, int foreColor, int backColor) {
2312 setColorMatrix(mForePaint[foreColor & 7],
2313 cursor ? mCursorPaint : mBackPaint[backColor & 7]);
2314 int destX = (int) x + kCharacterWidth * lineOffset;
2315 int destY = (int) y;
2316 Rect srcRect = new Rect();
2317 Rect destRect = new Rect();
2318 destRect.top = (destY - kCharacterHeight);
2319 destRect.bottom = destY;
2320 for(int i = 0; i < count; i++) {
2321 char c = text[i + index];
2322 if ((cursor || (c != 32)) && (c < 128)) {
2324 int cellY = (c >> 5) & 3;
2325 int srcX = cellX * kCharacterWidth;
2326 int srcY = cellY * kCharacterHeight;
2327 srcRect.set(srcX, srcY,
2328 srcX + kCharacterWidth, srcY + kCharacterHeight);
2329 destRect.left = destX;
2330 destRect.right = destX + kCharacterWidth;
2331 canvas.drawBitmap(mFont, srcRect, destRect, mPaint);
2333 destX += kCharacterWidth;
2337 private void setColorMatrix(int foreColor, int backColor) {
2338 if ((foreColor != mCurrentForeColor)
2339 || (backColor != mCurrentBackColor)
2340 || (mColorMatrix == null)) {
2341 mCurrentForeColor = foreColor;
2342 mCurrentBackColor = backColor;
2343 if (mColorMatrix == null) {
2344 mColorMatrix = new float[20];
2345 mColorMatrix[18] = 1.0f; // Just copy Alpha
2347 for (int component = 0; component < 3; component++) {
2348 int rightShift = (2 - component) << 3;
2349 int fore = 0xff & (foreColor >> rightShift);
2350 int back = 0xff & (backColor >> rightShift);
2351 int delta = back - fore;
2352 mColorMatrix[component * 6] = delta * BYTE_SCALE;
2353 mColorMatrix[component * 5 + 4] = fore;
2355 mPaint.setColorFilter(new ColorMatrixColorFilter(mColorMatrix));
2360 class PaintRenderer extends BaseTextRenderer {
2361 public PaintRenderer(int fontSize, int forePaintColor, int backPaintColor) {
2362 super(forePaintColor, backPaintColor);
2363 mTextPaint = new Paint();
2364 mTextPaint.setTypeface(Typeface.MONOSPACE);
2365 mTextPaint.setAntiAlias(true);
2366 mTextPaint.setTextSize(fontSize);
2368 mCharHeight = (int) Math.ceil(mTextPaint.getFontSpacing());
2369 mCharAscent = (int) Math.ceil(mTextPaint.ascent());
2370 mCharDescent = mCharHeight + mCharAscent;
2371 mCharWidth = (int) mTextPaint.measureText(EXAMPLE_CHAR, 0, 1);
2374 public void drawTextRun(Canvas canvas, float x, float y, int lineOffset,
2375 char[] text, int index, int count,
2376 boolean cursor, int foreColor, int backColor) {
2378 mTextPaint.setColor(mCursorPaint);
2380 mTextPaint.setColor(mBackPaint[backColor & 0x7]);
2382 float left = x + lineOffset * mCharWidth;
2383 canvas.drawRect(left, y + mCharAscent,
2384 left + count * mCharWidth, y + mCharDescent,
2386 boolean bold = ( foreColor & 0x8 ) != 0;
2387 boolean underline = (backColor & 0x8) != 0;
2389 mTextPaint.setFakeBoldText(true);
2392 mTextPaint.setUnderlineText(true);
2394 mTextPaint.setColor(mForePaint[foreColor & 0x7]);
2395 canvas.drawText(text, index, count, left, y, mTextPaint);
2397 mTextPaint.setFakeBoldText(false);
2400 mTextPaint.setUnderlineText(false);
2404 public int getCharacterHeight() {
2408 public int getCharacterWidth() {
2413 private Paint mTextPaint;
2414 private int mCharWidth;
2415 private int mCharHeight;
2416 private int mCharAscent;
2417 private int mCharDescent;
2418 private static final char[] EXAMPLE_CHAR = {'X'};
2422 * A multi-thread-safe produce-consumer byte array.
2423 * Only allows one producer and one consumer.
2427 public ByteQueue(int size) {
2428 mBuffer = new byte[size];
2431 public int getBytesAvailable() {
2432 synchronized(this) {
2433 return mStoredBytes;
2437 public int read(byte[] buffer, int offset, int length)
2438 throws InterruptedException {
2439 if (length + offset > buffer.length) {
2441 new IllegalArgumentException("length + offset > buffer.length");
2445 new IllegalArgumentException("length < 0");
2451 synchronized(this) {
2452 while (mStoredBytes == 0) {
2456 int bufferLength = mBuffer.length;
2457 boolean wasFull = bufferLength == mStoredBytes;
2458 while (length > 0 && mStoredBytes > 0) {
2459 int oneRun = Math.min(bufferLength - mHead, mStoredBytes);
2460 int bytesToCopy = Math.min(length, oneRun);
2461 System.arraycopy(mBuffer, mHead, buffer, offset, bytesToCopy);
2462 mHead += bytesToCopy;
2463 if (mHead >= bufferLength) {
2466 mStoredBytes -= bytesToCopy;
2467 length -= bytesToCopy;
2468 offset += bytesToCopy;
2469 totalRead += bytesToCopy;
2478 public void write(byte[] buffer, int offset, int length)
2479 throws InterruptedException {
2480 if (length + offset > buffer.length) {
2482 new IllegalArgumentException("length + offset > buffer.length");
2486 new IllegalArgumentException("length < 0");
2492 synchronized(this) {
2493 int bufferLength = mBuffer.length;
2494 boolean wasEmpty = mStoredBytes == 0;
2495 while (length > 0) {
2496 while(bufferLength == mStoredBytes) {
2499 int tail = mHead + mStoredBytes;
2501 if (tail >= bufferLength) {
2502 tail = tail - bufferLength;
2503 oneRun = mHead - tail;
2505 oneRun = bufferLength - tail;
2507 int bytesToCopy = Math.min(oneRun, length);
2508 System.arraycopy(buffer, offset, mBuffer, tail, bytesToCopy);
2509 offset += bytesToCopy;
2510 mStoredBytes += bytesToCopy;
2511 length -= bytesToCopy;
2519 private byte[] mBuffer;
2521 private int mStoredBytes;
2524 * A view on a transcript and a terminal emulator. Displays the text of the
2525 * transcript and the current cursor position of the terminal emulator.
2527 class EmulatorView extends View implements GestureDetector.OnGestureListener {
2529 private final String TAG = "EmulatorView";
2530 private final boolean LOG_KEY_EVENTS = Term.DEBUG && false;
2535 * We defer some initialization until we have been layed out in the view
2536 * hierarchy. The boolean tracks when we know what our size is.
2538 private boolean mKnownSize;
2540 private int mVisibleWidth;
2541 private int mVisibleHeight;
2542 private Rect mVisibleRect = new Rect();
2545 * Our transcript. Contains the screen and the transcript.
2547 private TranscriptScreen mTranscriptScreen;
2550 * Number of rows in the transcript.
2552 private static final int TRANSCRIPT_ROWS = 10000;
2555 * Total width of each character, in pixels
2557 private int mCharacterWidth;
2560 * Total height of each character, in pixels
2562 private int mCharacterHeight;
2565 * Used to render text
2567 private TextRenderer mTextRenderer;
2570 * Text size. Zero means 4 x 8 font.
2572 private int mTextSize;
2574 private int mCursorStyle;
2575 private int mCursorBlink;
2580 private int mForeground;
2585 private int mBackground;
2588 * Used to paint the cursor
2590 private Paint mCursorPaint;
2592 private Paint mBackgroundPaint;
2594 private boolean mUseCookedIme;
2597 * Our terminal emulator. We use this to get the current cursor position.
2599 private TerminalEmulator mEmulator;
2602 * The number of rows of text to display.
2607 * The number of columns of text to display.
2609 private int mColumns;
2612 * The number of columns that are visible on the display.
2615 private int mVisibleColumns;
2618 * The top row of text to display. Ranges from -activeTranscriptRows to 0
2620 private int mTopRow;
2622 private int mLeftColumn;
2624 private FileDescriptor mTermFd;
2626 * Used to receive data from the remote process.
2628 private FileInputStream mTermIn;
2630 private FileOutputStream mTermOut;
2632 private ByteQueue mByteQueue;
2635 * Used to temporarily hold data received from the remote process. Allocated
2636 * once and used permanently to minimize heap thrashing.
2638 private byte[] mReceiveBuffer;
2641 * Our private message id, which we use to receive new input from the
2644 private static final int UPDATE = 1;
2646 private static final int SCREEN_CHECK_PERIOD = 1000;
2647 private static final int CURSOR_BLINK_PERIOD = 1000;
2649 private boolean mCursorVisible = true;
2652 * Used to poll if the view has changed size. Wish there was a better way to do this.
2654 private Runnable mCheckSize = new Runnable() {
2658 mHandler.postDelayed(this, SCREEN_CHECK_PERIOD);
2662 private Runnable mBlinkCursor = new Runnable() {
2664 if (mCursorBlink != 0) {
2665 mCursorVisible = ! mCursorVisible;
2666 mHandler.postDelayed(this, CURSOR_BLINK_PERIOD);
2668 mCursorVisible = true;
2670 // Perhaps just invalidate the character with the cursor.
2676 * Thread that polls for input from the remote process
2679 private Thread mPollingThread;
2681 private GestureDetector mGestureDetector;
2682 private float mScrollRemainder;
2683 private TermKeyListener mKeyListener;
2686 * Our message handler class. Implements a periodic callback.
2688 private final Handler mHandler = new Handler() {
2690 * Handle the callback message. Call our enclosing class's update
2693 * @param msg The callback message.
2696 public void handleMessage(Message msg) {
2697 if (msg.what == UPDATE) {
2703 public EmulatorView(Context context) {
2705 commonConstructor();
2708 public void onResume() {
2710 mHandler.postDelayed(mCheckSize, SCREEN_CHECK_PERIOD);
2711 if (mCursorBlink != 0) {
2712 mHandler.postDelayed(mBlinkCursor, CURSOR_BLINK_PERIOD);
2716 public void onPause() {
2717 mHandler.removeCallbacks(mCheckSize);
2718 if (mCursorBlink != 0) {
2719 mHandler.removeCallbacks(mBlinkCursor);
2723 public void register(Term term, TermKeyListener listener) {
2725 mKeyListener = listener;
2728 public void setColors(int foreground, int background) {
2729 mForeground = foreground;
2730 mBackground = background;
2734 public String getTranscriptText() {
2735 return mEmulator.getTranscriptText();
2738 public void resetTerminal() {
2744 public boolean onCheckIsTextEditor() {
2749 public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
2750 outAttrs.inputType = mUseCookedIme ?
2751 EditorInfo.TYPE_CLASS_TEXT :
2752 EditorInfo.TYPE_NULL;
2753 return new BaseInputConnection(this, false) {
2756 public boolean commitText(CharSequence text, int newCursorPosition) {
2762 public boolean performEditorAction(int actionCode) {
2763 if(actionCode == EditorInfo.IME_ACTION_UNSPECIFIED) {
2764 // The "return" key has been pressed on the IME.
2772 public boolean sendKeyEvent(KeyEvent event) {
2773 if (event.getAction() == KeyEvent.ACTION_DOWN) {
2774 // Some keys are sent here rather than to commitText.
2775 // In particular, del and the digit keys are sent here.
2776 // (And I have reports that the HTC Magic also sends Return here.)
2777 // As a bit of defensive programming, handle every
2778 // key with an ASCII meaning.
2779 int keyCode = event.getKeyCode();
2780 if (keyCode >= 0 && keyCode < KEYCODE_CHARS.length()) {
2781 char c = KEYCODE_CHARS.charAt(keyCode);
2790 private final String KEYCODE_CHARS =
2791 "\000\000\000\000\000\000\000" + "0123456789*#"
2792 + "\000\000\000\000\000\000\000\000\000\000"
2793 + "abcdefghijklmnopqrstuvwxyz,."
2794 + "\000\000\000\000"
2795 + "\011 " // tab, space
2796 + "\000\000\000" // sym .. envelope
2797 + "\015\177" // enter, del
2803 public boolean setComposingText(CharSequence text, int newCursorPosition) {
2808 public boolean setSelection(int start, int end) {
2813 public boolean deleteSurroundingText(int leftLength, int rightLength) {
2814 for (int i = 0; i < leftLength; i++) {
2816 new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));
2818 // TODO: handle forward deletes.
2822 private void sendChar(int c) {
2825 } catch (IOException ex) {
2829 private void sendText(CharSequence text) {
2830 int n = text.length();
2832 for(int i = 0; i < n; i++) {
2833 char c = text.charAt(i);
2836 } catch (IOException e) {
2840 private void mapAndSend(int c) throws IOException {
2842 mKeyListener.mapControlChar(c));
2847 public boolean getKeypadApplicationMode() {
2848 return mEmulator.getKeypadApplicationMode();
2851 public EmulatorView(Context context, AttributeSet attrs) {
2852 this(context, attrs, 0);
2855 public EmulatorView(Context context, AttributeSet attrs,
2857 super(context, attrs, defStyle);
2859 // context.obtainStyledAttributes(android.R.styleable.View);
2860 // initializeScrollbars(a);
2862 commonConstructor();
2865 private void commonConstructor() {
2866 mTextRenderer = null;
2867 mCursorPaint = new Paint();
2868 mCursorPaint.setARGB(255,128,128,128);
2869 mBackgroundPaint = new Paint();
2872 mGestureDetector = new GestureDetector(this);
2873 // mGestureDetector.setIsLongpressEnabled(false);
2874 setVerticalScrollBarEnabled(true);
2878 protected int computeVerticalScrollRange() {
2879 return mTranscriptScreen.getActiveRows();
2883 protected int computeVerticalScrollExtent() {
2888 protected int computeVerticalScrollOffset() {
2889 return mTranscriptScreen.getActiveRows() + mTopRow - mRows;
2893 * Call this to initialize the view.
2895 * @param termFd the file descriptor
2896 * @param termOut the output stream for the pseudo-teletype
2898 public void initialize(FileDescriptor termFd, FileOutputStream termOut) {
2902 mForeground = Term.WHITE;
2903 mBackground = Term.BLACK;
2905 mTermIn = new FileInputStream(mTermFd);
2906 mReceiveBuffer = new byte[4 * 1024];
2907 mByteQueue = new ByteQueue(4 * 1024);
2911 * Accept a sequence of bytes (typically from the pseudo-tty) and process
2914 * @param buffer a byte array containing bytes to be processed
2915 * @param base the index of the first byte in the buffer to process
2916 * @param length the number of bytes to process
2918 public void append(byte[] buffer, int base, int length) {
2919 mEmulator.append(buffer, base, length);
2920 ensureCursorVisible();
2925 * Page the terminal view (scroll it up or down by delta screenfulls.)
2927 * @param delta the number of screens to scroll. Positive means scroll down,
2928 * negative means scroll up.
2930 public void page(int delta) {
2932 Math.min(0, Math.max(-(mTranscriptScreen
2933 .getActiveTranscriptRows()), mTopRow + mRows * delta));
2938 * Page the terminal view horizontally.
2940 * @param deltaColumns the number of columns to scroll. Positive scrolls to
2943 public void pageHorizontal(int deltaColumns) {
2945 Math.max(0, Math.min(mLeftColumn + deltaColumns, mColumns
2946 - mVisibleColumns));
2951 * Sets the text size, which in turn sets the number of rows and columns
2953 * @param fontSize the new font size, in pixels.
2955 public void setTextSize(int fontSize) {
2956 mTextSize = fontSize;
2960 public void setCursorStyle(int style, int blink) {
2961 mCursorStyle = style;
2962 if (blink != 0 && mCursorBlink == 0) {
2963 mHandler.postDelayed(mBlinkCursor, CURSOR_BLINK_PERIOD);
2964 } else if (blink == 0 && mCursorBlink != 0) {
2965 mHandler.removeCallbacks(mBlinkCursor);
2967 mCursorBlink = blink;
2970 public void setUseCookedIME(boolean useRawIME) {
2971 mUseCookedIme = useRawIME;
2974 // Begin GestureDetector.OnGestureListener methods
2976 public boolean onSingleTapUp(MotionEvent e) {
2980 public void onLongPress(MotionEvent e) {
2984 public boolean onScroll(MotionEvent e1, MotionEvent e2,
2985 float distanceX, float distanceY) {
2986 distanceY += mScrollRemainder;
2987 int deltaRows = (int) (distanceY / mCharacterHeight);
2988 mScrollRemainder = distanceY - deltaRows * mCharacterHeight;
2990 Math.min(0, Math.max(-(mTranscriptScreen
2991 .getActiveTranscriptRows()), mTopRow + deltaRows));
2997 public void onSingleTapConfirmed(MotionEvent e) {
3000 public boolean onJumpTapDown(MotionEvent e1, MotionEvent e2) {
3007 public boolean onJumpTapUp(MotionEvent e1, MotionEvent e2) {
3009 mTopRow = -mTranscriptScreen.getActiveTranscriptRows();
3014 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
3016 // TODO: add animation man's (non animated) fling
3017 mScrollRemainder = 0.0f;
3018 onScroll(e1, e2, 2 * velocityX, -2 * velocityY);
3022 public void onShowPress(MotionEvent e) {
3025 public boolean onDown(MotionEvent e) {
3026 mScrollRemainder = 0.0f;
3030 // End GestureDetector.OnGestureListener methods
3032 @Override public boolean onTouchEvent(MotionEvent ev) {
3033 return mGestureDetector.onTouchEvent(ev);
3037 public boolean onKeyDown(int keyCode, KeyEvent event) {
3038 if (LOG_KEY_EVENTS) {
3039 Log.w(TAG, "onKeyDown " + keyCode);
3041 if (handleControlKey(keyCode, true)) {
3043 } else if (isSystemKey(keyCode, event)) {
3044 // Don't intercept the system keys
3045 return super.onKeyDown(keyCode, event);
3046 } else if (handleDPad(keyCode, true)) {
3050 // Translate the keyCode into an ASCII character.
3051 int letter = mKeyListener.keyDown(keyCode, event);
3055 mTermOut.write(letter);
3056 } catch (IOException e) {
3057 // Ignore I/O exceptions
3064 public boolean onKeyUp(int keyCode, KeyEvent event) {
3065 if (LOG_KEY_EVENTS) {
3066 Log.w(TAG, "onKeyUp " + keyCode);
3068 if (handleControlKey(keyCode, false)) {
3070 } else if (isSystemKey(keyCode, event)) {
3071 // Don't intercept the system keys
3072 return super.onKeyUp(keyCode, event);
3073 } else if (handleDPad(keyCode, false)) {
3077 mKeyListener.keyUp(keyCode);
3082 private boolean handleControlKey(int keyCode, boolean down) {
3083 if (keyCode == mTerm.getControlKeyCode()) {
3084 if (LOG_KEY_EVENTS) {
3085 Log.w(TAG, "handleControlKey " + keyCode);
3087 mKeyListener.handleControlKey(down);
3094 * Handle dpad left-right-up-down events. Don't handle
3095 * dpad-center, that's our control key.
3099 private boolean handleDPad(int keyCode, boolean down) {
3100 if (keyCode < KeyEvent.KEYCODE_DPAD_UP ||
3101 keyCode > KeyEvent.KEYCODE_DPAD_CENTER) {
3107 if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
3108 mTermOut.write('\r');
3112 case KeyEvent.KEYCODE_DPAD_UP:
3115 case KeyEvent.KEYCODE_DPAD_DOWN:
3118 case KeyEvent.KEYCODE_DPAD_LEFT:
3122 case KeyEvent.KEYCODE_DPAD_RIGHT:
3126 mTermOut.write(27); // ESC
3127 if (getKeypadApplicationMode()) {
3128 mTermOut.write('O');
3130 mTermOut.write('[');
3132 mTermOut.write(code);
3134 } catch (IOException e) {
3141 private boolean isSystemKey(int keyCode, KeyEvent event) {
3142 return event.isSystem();
3145 private void updateText() {
3146 if (mTextSize > 0) {
3147 mTextRenderer = new PaintRenderer(mTextSize, mForeground,
3151 mTextRenderer = new Bitmap4x8FontRenderer(getResources(),
3152 mForeground, mBackground);
3154 mBackgroundPaint.setColor(mBackground);
3155 mCharacterWidth = mTextRenderer.getCharacterWidth();
3156 mCharacterHeight = mTextRenderer.getCharacterHeight();
3162 protected void onSizeChanged(int w, int h, int oldw, int oldh) {
3163 boolean oldKnownSize = mKnownSize;
3168 if (!oldKnownSize) {
3169 // Set up a thread to read input from the
3172 mPollingThread = new Thread(new Runnable() {
3177 int read = mTermIn.read(mBuffer);
3178 mByteQueue.write(mBuffer, 0, read);
3179 mHandler.sendMessage(
3180 mHandler.obtainMessage(UPDATE));
3182 } catch (IOException e) {
3183 } catch (InterruptedException e) {
3186 private byte[] mBuffer = new byte[4096];
3188 mPollingThread.setName("Input reader");
3189 mPollingThread.start();
3193 private void updateSize(int w, int h) {
3194 mColumns = Math.max(1, w / mCharacterWidth);
3195 mRows = Math.max(1, h / mCharacterHeight);
3196 mVisibleColumns = mVisibleWidth / mCharacterWidth;
3198 // Inform the attached pty of our new size:
3199 Exec.setPtyWindowSize(mTermFd, mRows, mColumns, w, h);
3202 if (mTranscriptScreen != null) {
3203 mEmulator.updateSize(mColumns, mRows);
3206 new TranscriptScreen(mColumns, TRANSCRIPT_ROWS, mRows, 0, 7);
3208 new TerminalEmulator(mTranscriptScreen, mColumns, mRows,
3212 // Reset our paging:
3219 void updateSize(boolean force) {
3221 getWindowVisibleDisplayFrame(mVisibleRect);
3222 int w = mVisibleRect.width();
3223 int h = mVisibleRect.height();
3224 // Log.w("Term", "(" + w + ", " + h + ")");
3225 if (force || w != mVisibleWidth || h != mVisibleHeight) {
3228 updateSize(mVisibleWidth, mVisibleHeight);
3234 * Look for new input from the ptty, send it to the terminal emulator.
3236 private void update() {
3237 int bytesAvailable = mByteQueue.getBytesAvailable();
3238 int bytesToRead = Math.min(bytesAvailable, mReceiveBuffer.length);
3240 int bytesRead = mByteQueue.read(mReceiveBuffer, 0, bytesToRead);
3241 append(mReceiveBuffer, 0, bytesRead);
3242 } catch (InterruptedException e) {
3247 protected void onDraw(Canvas canvas) {
3250 int h = getHeight();
3251 canvas.drawRect(0, 0, w, h, mBackgroundPaint);
3252 float x = -mLeftColumn * mCharacterWidth;
3253 float y = mCharacterHeight;
3254 int endLine = mTopRow + mRows;
3255 int cx = mEmulator.getCursorCol();
3256 int cy = mEmulator.getCursorRow();
3257 for (int i = mTopRow; i < endLine; i++) {
3259 if (i == cy && mCursorVisible) {
3262 mTranscriptScreen.drawText(i, canvas, x, y, mTextRenderer, cursorX);
3263 y += mCharacterHeight;
3267 private void ensureCursorVisible() {
3269 if (mVisibleColumns > 0) {
3270 int cx = mEmulator.getCursorCol();
3271 int visibleCursorX = mEmulator.getCursorCol() - mLeftColumn;
3272 if (visibleCursorX < 0) {
3274 } else if (visibleCursorX >= mVisibleColumns) {
3275 mLeftColumn = (cx - mVisibleColumns) + 1;
3283 * An ASCII key listener. Supports control characters and escape. Keeps track of
3284 * the current state of the alt, shift, and control keys.
3286 class TermKeyListener {
3288 * The state engine for a modifier key. Can be pressed, released, locked,
3292 private class ModifierKey {
3296 private static final int UNPRESSED = 0;
3298 private static final int PRESSED = 1;
3300 private static final int RELEASED = 2;
3302 private static final int USED = 3;
3304 private static final int LOCKED = 4;
3307 * Construct a modifier key. UNPRESSED by default.
3310 public ModifierKey() {
3314 public void onPress() {
3317 // This is a repeat before use
3323 // This is a repeat after use
3334 public void onRelease() {
3343 // Leave state alone
3348 public void adjustAfterKeypress() {
3357 // Leave state alone
3362 public boolean isActive() {
3363 return mState != UNPRESSED;
3367 private ModifierKey mAltKey = new ModifierKey();
3369 private ModifierKey mCapKey = new ModifierKey();
3371 private ModifierKey mControlKey = new ModifierKey();
3374 * Construct a term key listener.
3377 public TermKeyListener() {
3380 public void handleControlKey(boolean down) {
3382 mControlKey.onPress();
3384 mControlKey.onRelease();
3388 public int mapControlChar(int ch) {
3390 if (mControlKey.isActive()) {
3391 // Search is the control key.
3392 if (result >= 'a' && result <= 'z') {
3393 result = (char) (result - 'a' + '\001');
3394 } else if (result == ' ') {
3396 } else if ((result == '[') || (result == '1')) {
3398 } else if ((result == '\\') || (result == '.')) {
3400 } else if ((result == ']') || (result == '0')) {
3402 } else if ((result == '^') || (result == '6')) {
3403 result = 30; // control-^
3404 } else if ((result == '_') || (result == '5')) {
3410 mAltKey.adjustAfterKeypress();
3411 mCapKey.adjustAfterKeypress();
3412 mControlKey.adjustAfterKeypress();
3418 * Handle a keyDown event.
3420 * @param keyCode the keycode of the keyDown event
3421 * @return the ASCII byte to transmit to the pseudo-teletype, or -1 if this
3422 * event does not produce an ASCII byte.
3424 public int keyDown(int keyCode, KeyEvent event) {
3427 case KeyEvent.KEYCODE_ALT_RIGHT:
3428 case KeyEvent.KEYCODE_ALT_LEFT:
3432 case KeyEvent.KEYCODE_SHIFT_LEFT:
3433 case KeyEvent.KEYCODE_SHIFT_RIGHT:
3437 case KeyEvent.KEYCODE_ENTER:
3438 // Convert newlines into returns. The vt100 sends a
3439 // '\r' when the 'Return' key is pressed, but our
3440 // KeyEvent translates this as a '\n'.
3444 case KeyEvent.KEYCODE_DEL:
3445 // Convert DEL into 127 (instead of 8)
3450 result = event.getUnicodeChar(
3451 (mCapKey.isActive() ? KeyEvent.META_SHIFT_ON : 0) |
3452 (mAltKey.isActive() ? KeyEvent.META_ALT_ON : 0));
3457 result = mapControlChar(result);
3463 * Handle a keyUp event.
3465 * @param keyCode the keyCode of the keyUp event
3467 public void keyUp(int keyCode) {
3469 case KeyEvent.KEYCODE_ALT_LEFT:
3470 case KeyEvent.KEYCODE_ALT_RIGHT:
3471 mAltKey.onRelease();
3473 case KeyEvent.KEYCODE_SHIFT_LEFT:
3474 case KeyEvent.KEYCODE_SHIFT_RIGHT:
3475 mCapKey.onRelease();
3478 // Ignore other keyUps