mirror of
https://github.com/fankes/termux-app.git
synced 2025-09-05 02:05:25 +08:00
Restructure library modules
terminal/ -> terminal-emulator/ view/ -> terminal-view/
This commit is contained in:
44
terminal-emulator/build.gradle
Normal file
44
terminal-emulator/build.gradle
Normal file
@@ -0,0 +1,44 @@
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
compileSdkVersion 25
|
||||
buildToolsVersion "25.0.2"
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 25
|
||||
|
||||
externalNativeBuild {
|
||||
ndkBuild {
|
||||
cFlags "-std=c11", "-Wall", "-Wextra", "-Werror", "-Os", "-fno-stack-protector", "-Wl,--gc-sections"
|
||||
}
|
||||
}
|
||||
|
||||
ndk {
|
||||
abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
ndkBuild {
|
||||
path "src/main/jni/Android.mk"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(Test) {
|
||||
testLogging {
|
||||
events "started", "passed", "skipped", "failed"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testCompile 'junit:junit:4.12'
|
||||
}
|
25
terminal-emulator/proguard-rules.pro
vendored
Normal file
25
terminal-emulator/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in /Users/fornwall/lib/android-sdk/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
3
terminal-emulator/src/main/AndroidManifest.xml
Normal file
3
terminal-emulator/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.termux.terminal">
|
||||
</manifest>
|
@@ -0,0 +1,108 @@
|
||||
package com.termux.terminal;
|
||||
|
||||
/** A circular byte buffer allowing one producer and one consumer thread. */
|
||||
final class ByteQueue {
|
||||
|
||||
private final byte[] mBuffer;
|
||||
private int mHead;
|
||||
private int mStoredBytes;
|
||||
private boolean mOpen = true;
|
||||
|
||||
public ByteQueue(int size) {
|
||||
mBuffer = new byte[size];
|
||||
}
|
||||
|
||||
public synchronized void close() {
|
||||
mOpen = false;
|
||||
notify();
|
||||
}
|
||||
|
||||
public synchronized int read(byte[] buffer, boolean block) {
|
||||
while (mStoredBytes == 0 && mOpen) {
|
||||
if (block) {
|
||||
try {
|
||||
wait();
|
||||
} catch (InterruptedException e) {
|
||||
// Ignore.
|
||||
}
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
if (!mOpen) return -1;
|
||||
|
||||
int totalRead = 0;
|
||||
int bufferLength = mBuffer.length;
|
||||
boolean wasFull = bufferLength == mStoredBytes;
|
||||
int length = buffer.length;
|
||||
int offset = 0;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to write the specified portion of the provided buffer to the queue.
|
||||
* <p/>
|
||||
* Returns whether the output was totally written, false if it was closed before.
|
||||
*/
|
||||
public boolean write(byte[] buffer, int offset, int lengthToWrite) {
|
||||
if (lengthToWrite + offset > buffer.length) {
|
||||
throw new IllegalArgumentException("length + offset > buffer.length");
|
||||
} else if (lengthToWrite <= 0) {
|
||||
throw new IllegalArgumentException("length <= 0");
|
||||
}
|
||||
|
||||
final int bufferLength = mBuffer.length;
|
||||
|
||||
synchronized (this) {
|
||||
while (lengthToWrite > 0) {
|
||||
while (bufferLength == mStoredBytes && mOpen) {
|
||||
try {
|
||||
wait();
|
||||
} catch (InterruptedException e) {
|
||||
// Ignore.
|
||||
}
|
||||
}
|
||||
if (!mOpen) return false;
|
||||
final boolean wasEmpty = mStoredBytes == 0;
|
||||
int bytesToWriteBeforeWaiting = Math.min(lengthToWrite, bufferLength - mStoredBytes);
|
||||
lengthToWrite -= bytesToWriteBeforeWaiting;
|
||||
|
||||
while (bytesToWriteBeforeWaiting > 0) {
|
||||
int tail = mHead + mStoredBytes;
|
||||
int oneRun;
|
||||
if (tail >= bufferLength) {
|
||||
// Buffer: [.............]
|
||||
// ________________H_______T
|
||||
// =>
|
||||
// Buffer: [.............]
|
||||
// ___________T____H
|
||||
// onRun= _____----_
|
||||
tail = tail - bufferLength;
|
||||
oneRun = mHead - tail;
|
||||
} else {
|
||||
oneRun = bufferLength - tail;
|
||||
}
|
||||
int bytesToCopy = Math.min(oneRun, bytesToWriteBeforeWaiting);
|
||||
System.arraycopy(buffer, offset, mBuffer, tail, bytesToCopy);
|
||||
offset += bytesToCopy;
|
||||
bytesToWriteBeforeWaiting -= bytesToCopy;
|
||||
mStoredBytes += bytesToCopy;
|
||||
}
|
||||
if (wasEmpty) notify();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
package com.termux.terminal;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
public final class EmulatorDebug {
|
||||
|
||||
/** The tag to use with {@link Log}. */
|
||||
public static final String LOG_TAG = "termux";
|
||||
|
||||
}
|
41
terminal-emulator/src/main/java/com/termux/terminal/JNI.java
Normal file
41
terminal-emulator/src/main/java/com/termux/terminal/JNI.java
Normal file
@@ -0,0 +1,41 @@
|
||||
package com.termux.terminal;
|
||||
|
||||
/**
|
||||
* Native methods for creating and managing pseudoterminal subprocesses. C code is in jni/termux.c.
|
||||
*/
|
||||
final class JNI {
|
||||
|
||||
static {
|
||||
System.loadLibrary("termux");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a subprocess. Differs from {@link ProcessBuilder} in that a pseudoterminal is used to communicate with the
|
||||
* subprocess.
|
||||
* <p/>
|
||||
* Callers are responsible for calling {@link #close(int)} on the returned file descriptor.
|
||||
*
|
||||
* @param cmd The command to execute
|
||||
* @param cwd The current working directory for the executed command
|
||||
* @param args An array of arguments to the command
|
||||
* @param envVars An array of strings of the form "VAR=value" to be added to the environment of the process
|
||||
* @param processId A one-element array to which the process ID of the started process will be written.
|
||||
* @return the file descriptor resulting from opening /dev/ptmx master device. The sub process will have opened the
|
||||
* slave device counterpart (/dev/pts/$N) and have it as stdint, stdout and stderr.
|
||||
*/
|
||||
public static native int createSubprocess(String cmd, String cwd, String[] args, String[] envVars, int[] processId, int rows, int columns);
|
||||
|
||||
/** Set the window size for a given pty, which allows connected programs to learn how large their screen is. */
|
||||
public static native void setPtyWindowSize(int fd, int rows, int cols);
|
||||
|
||||
/**
|
||||
* Causes the calling thread to wait for the process associated with the receiver to finish executing.
|
||||
*
|
||||
* @return if >= 0, the exit status of the process. If < 0, the signal causing the process to stop negated.
|
||||
*/
|
||||
public static native int waitFor(int processId);
|
||||
|
||||
/** Close a file descriptor through the close(2) system call. */
|
||||
public static native void close(int fileDescriptor);
|
||||
|
||||
}
|
@@ -0,0 +1,313 @@
|
||||
package com.termux.terminal;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static android.view.KeyEvent.KEYCODE_BACK;
|
||||
import static android.view.KeyEvent.KEYCODE_BREAK;
|
||||
import static android.view.KeyEvent.KEYCODE_DEL;
|
||||
import static android.view.KeyEvent.KEYCODE_DPAD_CENTER;
|
||||
import static android.view.KeyEvent.KEYCODE_DPAD_DOWN;
|
||||
import static android.view.KeyEvent.KEYCODE_DPAD_LEFT;
|
||||
import static android.view.KeyEvent.KEYCODE_DPAD_RIGHT;
|
||||
import static android.view.KeyEvent.KEYCODE_DPAD_UP;
|
||||
import static android.view.KeyEvent.KEYCODE_ENTER;
|
||||
import static android.view.KeyEvent.KEYCODE_ESCAPE;
|
||||
import static android.view.KeyEvent.KEYCODE_F1;
|
||||
import static android.view.KeyEvent.KEYCODE_F10;
|
||||
import static android.view.KeyEvent.KEYCODE_F11;
|
||||
import static android.view.KeyEvent.KEYCODE_F12;
|
||||
import static android.view.KeyEvent.KEYCODE_F2;
|
||||
import static android.view.KeyEvent.KEYCODE_F3;
|
||||
import static android.view.KeyEvent.KEYCODE_F4;
|
||||
import static android.view.KeyEvent.KEYCODE_F5;
|
||||
import static android.view.KeyEvent.KEYCODE_F6;
|
||||
import static android.view.KeyEvent.KEYCODE_F7;
|
||||
import static android.view.KeyEvent.KEYCODE_F8;
|
||||
import static android.view.KeyEvent.KEYCODE_F9;
|
||||
import static android.view.KeyEvent.KEYCODE_FORWARD_DEL;
|
||||
import static android.view.KeyEvent.KEYCODE_INSERT;
|
||||
import static android.view.KeyEvent.KEYCODE_MOVE_END;
|
||||
import static android.view.KeyEvent.KEYCODE_MOVE_HOME;
|
||||
import static android.view.KeyEvent.KEYCODE_NUMPAD_0;
|
||||
import static android.view.KeyEvent.KEYCODE_NUMPAD_1;
|
||||
import static android.view.KeyEvent.KEYCODE_NUMPAD_2;
|
||||
import static android.view.KeyEvent.KEYCODE_NUMPAD_3;
|
||||
import static android.view.KeyEvent.KEYCODE_NUMPAD_4;
|
||||
import static android.view.KeyEvent.KEYCODE_NUMPAD_5;
|
||||
import static android.view.KeyEvent.KEYCODE_NUMPAD_6;
|
||||
import static android.view.KeyEvent.KEYCODE_NUMPAD_7;
|
||||
import static android.view.KeyEvent.KEYCODE_NUMPAD_8;
|
||||
import static android.view.KeyEvent.KEYCODE_NUMPAD_9;
|
||||
import static android.view.KeyEvent.KEYCODE_NUMPAD_ADD;
|
||||
import static android.view.KeyEvent.KEYCODE_NUMPAD_COMMA;
|
||||
import static android.view.KeyEvent.KEYCODE_NUMPAD_DIVIDE;
|
||||
import static android.view.KeyEvent.KEYCODE_NUMPAD_DOT;
|
||||
import static android.view.KeyEvent.KEYCODE_NUMPAD_ENTER;
|
||||
import static android.view.KeyEvent.KEYCODE_NUMPAD_EQUALS;
|
||||
import static android.view.KeyEvent.KEYCODE_NUMPAD_MULTIPLY;
|
||||
import static android.view.KeyEvent.KEYCODE_NUMPAD_SUBTRACT;
|
||||
import static android.view.KeyEvent.KEYCODE_NUM_LOCK;
|
||||
import static android.view.KeyEvent.KEYCODE_PAGE_DOWN;
|
||||
import static android.view.KeyEvent.KEYCODE_PAGE_UP;
|
||||
import static android.view.KeyEvent.KEYCODE_SPACE;
|
||||
import static android.view.KeyEvent.KEYCODE_SYSRQ;
|
||||
import static android.view.KeyEvent.KEYCODE_TAB;
|
||||
|
||||
public final class KeyHandler {
|
||||
|
||||
public static final int KEYMOD_ALT = 0x80000000;
|
||||
public static final int KEYMOD_CTRL = 0x40000000;
|
||||
public static final int KEYMOD_SHIFT = 0x20000000;
|
||||
|
||||
private static final Map<String, Integer> TERMCAP_TO_KEYCODE = new HashMap<>();
|
||||
|
||||
static {
|
||||
// terminfo: http://pubs.opengroup.org/onlinepubs/7990989799/xcurses/terminfo.html
|
||||
// termcap: http://man7.org/linux/man-pages/man5/termcap.5.html
|
||||
TERMCAP_TO_KEYCODE.put("%i", KEYMOD_SHIFT | KEYCODE_DPAD_RIGHT);
|
||||
TERMCAP_TO_KEYCODE.put("#2", KEYMOD_SHIFT | KEYCODE_MOVE_HOME); // Shifted home
|
||||
TERMCAP_TO_KEYCODE.put("#4", KEYMOD_SHIFT | KEYCODE_DPAD_LEFT);
|
||||
TERMCAP_TO_KEYCODE.put("*7", KEYMOD_SHIFT | KEYCODE_MOVE_END); // Shifted end key
|
||||
|
||||
TERMCAP_TO_KEYCODE.put("k1", KEYCODE_F1);
|
||||
TERMCAP_TO_KEYCODE.put("k2", KEYCODE_F2);
|
||||
TERMCAP_TO_KEYCODE.put("k3", KEYCODE_F3);
|
||||
TERMCAP_TO_KEYCODE.put("k4", KEYCODE_F4);
|
||||
TERMCAP_TO_KEYCODE.put("k5", KEYCODE_F5);
|
||||
TERMCAP_TO_KEYCODE.put("k6", KEYCODE_F6);
|
||||
TERMCAP_TO_KEYCODE.put("k7", KEYCODE_F7);
|
||||
TERMCAP_TO_KEYCODE.put("k8", KEYCODE_F8);
|
||||
TERMCAP_TO_KEYCODE.put("k9", KEYCODE_F9);
|
||||
TERMCAP_TO_KEYCODE.put("k;", KEYCODE_F10);
|
||||
TERMCAP_TO_KEYCODE.put("F1", KEYCODE_F11);
|
||||
TERMCAP_TO_KEYCODE.put("F2", KEYCODE_F12);
|
||||
TERMCAP_TO_KEYCODE.put("F3", KEYMOD_SHIFT | KEYCODE_F1);
|
||||
TERMCAP_TO_KEYCODE.put("F4", KEYMOD_SHIFT | KEYCODE_F2);
|
||||
TERMCAP_TO_KEYCODE.put("F5", KEYMOD_SHIFT | KEYCODE_F3);
|
||||
TERMCAP_TO_KEYCODE.put("F6", KEYMOD_SHIFT | KEYCODE_F4);
|
||||
TERMCAP_TO_KEYCODE.put("F7", KEYMOD_SHIFT | KEYCODE_F5);
|
||||
TERMCAP_TO_KEYCODE.put("F8", KEYMOD_SHIFT | KEYCODE_F6);
|
||||
TERMCAP_TO_KEYCODE.put("F9", KEYMOD_SHIFT | KEYCODE_F7);
|
||||
TERMCAP_TO_KEYCODE.put("FA", KEYMOD_SHIFT | KEYCODE_F8);
|
||||
TERMCAP_TO_KEYCODE.put("FB", KEYMOD_SHIFT | KEYCODE_F9);
|
||||
TERMCAP_TO_KEYCODE.put("FC", KEYMOD_SHIFT | KEYCODE_F10);
|
||||
TERMCAP_TO_KEYCODE.put("FD", KEYMOD_SHIFT | KEYCODE_F11);
|
||||
TERMCAP_TO_KEYCODE.put("FE", KEYMOD_SHIFT | KEYCODE_F12);
|
||||
|
||||
TERMCAP_TO_KEYCODE.put("kb", KEYCODE_DEL); // backspace key
|
||||
|
||||
TERMCAP_TO_KEYCODE.put("kd", KEYCODE_DPAD_DOWN); // terminfo=kcud1, down-arrow key
|
||||
TERMCAP_TO_KEYCODE.put("kh", KEYCODE_MOVE_HOME);
|
||||
TERMCAP_TO_KEYCODE.put("kl", KEYCODE_DPAD_LEFT);
|
||||
TERMCAP_TO_KEYCODE.put("kr", KEYCODE_DPAD_RIGHT);
|
||||
|
||||
// K1=Upper left of keypad:
|
||||
// t_K1 <kHome> keypad home key
|
||||
// t_K3 <kPageUp> keypad page-up key
|
||||
// t_K4 <kEnd> keypad end key
|
||||
// t_K5 <kPageDown> keypad page-down key
|
||||
TERMCAP_TO_KEYCODE.put("K1", KEYCODE_MOVE_HOME);
|
||||
TERMCAP_TO_KEYCODE.put("K3", KEYCODE_PAGE_UP);
|
||||
TERMCAP_TO_KEYCODE.put("K4", KEYCODE_MOVE_END);
|
||||
TERMCAP_TO_KEYCODE.put("K5", KEYCODE_PAGE_DOWN);
|
||||
|
||||
TERMCAP_TO_KEYCODE.put("ku", KEYCODE_DPAD_UP);
|
||||
|
||||
TERMCAP_TO_KEYCODE.put("kB", KEYMOD_SHIFT | KEYCODE_TAB); // termcap=kB, terminfo=kcbt: Back-tab
|
||||
TERMCAP_TO_KEYCODE.put("kD", KEYCODE_FORWARD_DEL); // terminfo=kdch1, delete-character key
|
||||
TERMCAP_TO_KEYCODE.put("kDN", KEYMOD_SHIFT | KEYCODE_DPAD_DOWN); // non-standard shifted arrow down
|
||||
TERMCAP_TO_KEYCODE.put("kF", KEYMOD_SHIFT | KEYCODE_DPAD_DOWN); // terminfo=kind, scroll-forward key
|
||||
TERMCAP_TO_KEYCODE.put("kI", KEYCODE_INSERT);
|
||||
TERMCAP_TO_KEYCODE.put("kN", KEYCODE_PAGE_UP);
|
||||
TERMCAP_TO_KEYCODE.put("kP", KEYCODE_PAGE_DOWN);
|
||||
TERMCAP_TO_KEYCODE.put("kR", KEYMOD_SHIFT | KEYCODE_DPAD_UP); // terminfo=kri, scroll-backward key
|
||||
TERMCAP_TO_KEYCODE.put("kUP", KEYMOD_SHIFT | KEYCODE_DPAD_UP); // non-standard shifted up
|
||||
|
||||
TERMCAP_TO_KEYCODE.put("@7", KEYCODE_MOVE_END);
|
||||
TERMCAP_TO_KEYCODE.put("@8", KEYCODE_NUMPAD_ENTER);
|
||||
}
|
||||
|
||||
static String getCodeFromTermcap(String termcap, boolean cursorKeysApplication, boolean keypadApplication) {
|
||||
Integer keyCodeAndMod = TERMCAP_TO_KEYCODE.get(termcap);
|
||||
if (keyCodeAndMod == null) return null;
|
||||
int keyCode = keyCodeAndMod;
|
||||
int keyMod = 0;
|
||||
if ((keyCode & KEYMOD_SHIFT) != 0) {
|
||||
keyMod |= KEYMOD_SHIFT;
|
||||
keyCode &= ~KEYMOD_SHIFT;
|
||||
}
|
||||
if ((keyCode & KEYMOD_CTRL) != 0) {
|
||||
keyMod |= KEYMOD_CTRL;
|
||||
keyCode &= ~KEYMOD_CTRL;
|
||||
}
|
||||
if ((keyCode & KEYMOD_ALT) != 0) {
|
||||
keyMod |= KEYMOD_ALT;
|
||||
keyCode &= ~KEYMOD_ALT;
|
||||
}
|
||||
return getCode(keyCode, keyMod, cursorKeysApplication, keypadApplication);
|
||||
}
|
||||
|
||||
public static String getCode(int keyCode, int keyMode, boolean cursorApp, boolean keypadApplication) {
|
||||
switch (keyCode) {
|
||||
case KEYCODE_DPAD_CENTER:
|
||||
return "\015";
|
||||
|
||||
case KEYCODE_DPAD_UP:
|
||||
return (keyMode == 0) ? (cursorApp ? "\033OA" : "\033[A") : transformForModifiers("\033[1", keyMode, 'A');
|
||||
case KEYCODE_DPAD_DOWN:
|
||||
return (keyMode == 0) ? (cursorApp ? "\033OB" : "\033[B") : transformForModifiers("\033[1", keyMode, 'B');
|
||||
case KEYCODE_DPAD_RIGHT:
|
||||
return (keyMode == 0) ? (cursorApp ? "\033OC" : "\033[C") : transformForModifiers("\033[1", keyMode, 'C');
|
||||
case KEYCODE_DPAD_LEFT:
|
||||
return (keyMode == 0) ? (cursorApp ? "\033OD" : "\033[D") : transformForModifiers("\033[1", keyMode, 'D');
|
||||
|
||||
case KEYCODE_MOVE_HOME:
|
||||
// Note that KEYCODE_HOME is handled by the system and never delivered to applications.
|
||||
// On a Logitech k810 keyboard KEYCODE_MOVE_HOME is sent by FN+LeftArrow.
|
||||
return (keyMode == 0) ? (cursorApp ? "\033OH" : "\033[H") : transformForModifiers("\033[1", keyMode, 'H');
|
||||
case KEYCODE_MOVE_END:
|
||||
return (keyMode == 0) ? (cursorApp ? "\033OF" : "\033[F") : transformForModifiers("\033[1", keyMode, 'F');
|
||||
|
||||
// An xterm can send function keys F1 to F4 in two modes: vt100 compatible or
|
||||
// not. Because Vim may not know what the xterm is sending, both types of keys
|
||||
// are recognized. The same happens for the <Home> and <End> keys.
|
||||
// normal vt100 ~
|
||||
// <F1> t_k1 <Esc>[11~ <xF1> <Esc>OP *<xF1>-xterm*
|
||||
// <F2> t_k2 <Esc>[12~ <xF2> <Esc>OQ *<xF2>-xterm*
|
||||
// <F3> t_k3 <Esc>[13~ <xF3> <Esc>OR *<xF3>-xterm*
|
||||
// <F4> t_k4 <Esc>[14~ <xF4> <Esc>OS *<xF4>-xterm*
|
||||
// <Home> t_kh <Esc>[7~ <xHome> <Esc>OH *<xHome>-xterm*
|
||||
// <End> t_@7 <Esc>[4~ <xEnd> <Esc>OF *<xEnd>-xterm*
|
||||
case KEYCODE_F1:
|
||||
return (keyMode == 0) ? "\033OP" : transformForModifiers("\033[1", keyMode, 'P');
|
||||
case KEYCODE_F2:
|
||||
return (keyMode == 0) ? "\033OQ" : transformForModifiers("\033[1", keyMode, 'Q');
|
||||
case KEYCODE_F3:
|
||||
return (keyMode == 0) ? "\033OR" : transformForModifiers("\033[1", keyMode, 'R');
|
||||
case KEYCODE_F4:
|
||||
return (keyMode == 0) ? "\033OS" : transformForModifiers("\033[1", keyMode, 'S');
|
||||
case KEYCODE_F5:
|
||||
return transformForModifiers("\033[15", keyMode, '~');
|
||||
case KEYCODE_F6:
|
||||
return transformForModifiers("\033[17", keyMode, '~');
|
||||
case KEYCODE_F7:
|
||||
return transformForModifiers("\033[18", keyMode, '~');
|
||||
case KEYCODE_F8:
|
||||
return transformForModifiers("\033[19", keyMode, '~');
|
||||
case KEYCODE_F9:
|
||||
return transformForModifiers("\033[20", keyMode, '~');
|
||||
case KEYCODE_F10:
|
||||
return transformForModifiers("\033[21", keyMode, '~');
|
||||
case KEYCODE_F11:
|
||||
return transformForModifiers("\033[23", keyMode, '~');
|
||||
case KEYCODE_F12:
|
||||
return transformForModifiers("\033[24", keyMode, '~');
|
||||
|
||||
case KEYCODE_SYSRQ:
|
||||
return "\033[32~"; // Sys Request / Print
|
||||
// Is this Scroll lock? case Cancel: return "\033[33~";
|
||||
case KEYCODE_BREAK:
|
||||
return "\033[34~"; // Pause/Break
|
||||
|
||||
case KEYCODE_ESCAPE:
|
||||
case KEYCODE_BACK:
|
||||
return "\033";
|
||||
|
||||
case KEYCODE_INSERT:
|
||||
return transformForModifiers("\033[2", keyMode, '~');
|
||||
case KEYCODE_FORWARD_DEL:
|
||||
return transformForModifiers("\033[3", keyMode, '~');
|
||||
|
||||
case KEYCODE_PAGE_UP:
|
||||
return "\033[5~";
|
||||
case KEYCODE_PAGE_DOWN:
|
||||
return "\033[6~";
|
||||
case KEYCODE_DEL:
|
||||
String prefix = ((keyMode & KEYMOD_ALT) == 0) ? "" : "\033";
|
||||
// Just do what xterm and gnome-terminal does:
|
||||
return prefix + (((keyMode & KEYMOD_CTRL) == 0) ? "\u007F" : "\u0008");
|
||||
case KEYCODE_NUM_LOCK:
|
||||
return "\033OP";
|
||||
|
||||
case KEYCODE_SPACE:
|
||||
// If ctrl is not down, return null so that it goes through normal input processing (which may e.g. cause a
|
||||
// combining accent to be written):
|
||||
return ((keyMode & KEYMOD_CTRL) == 0) ? null : "\0";
|
||||
case KEYCODE_TAB:
|
||||
// This is back-tab when shifted:
|
||||
return (keyMode & KEYMOD_SHIFT) == 0 ? "\011" : "\033[Z";
|
||||
case KEYCODE_ENTER:
|
||||
return ((keyMode & KEYMOD_ALT) == 0) ? "\r" : "\033\r";
|
||||
|
||||
case KEYCODE_NUMPAD_ENTER:
|
||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 'M') : "\n";
|
||||
case KEYCODE_NUMPAD_MULTIPLY:
|
||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 'j') : "*";
|
||||
case KEYCODE_NUMPAD_ADD:
|
||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 'k') : "+";
|
||||
case KEYCODE_NUMPAD_COMMA:
|
||||
return ",";
|
||||
case KEYCODE_NUMPAD_DOT:
|
||||
return keypadApplication ? "\033On" : ".";
|
||||
case KEYCODE_NUMPAD_SUBTRACT:
|
||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 'm') : "-";
|
||||
case KEYCODE_NUMPAD_DIVIDE:
|
||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 'o') : "/";
|
||||
case KEYCODE_NUMPAD_0:
|
||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 'p') : "0";
|
||||
case KEYCODE_NUMPAD_1:
|
||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 'q') : "1";
|
||||
case KEYCODE_NUMPAD_2:
|
||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 'r') : "2";
|
||||
case KEYCODE_NUMPAD_3:
|
||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 's') : "3";
|
||||
case KEYCODE_NUMPAD_4:
|
||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 't') : "4";
|
||||
case KEYCODE_NUMPAD_5:
|
||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 'u') : "5";
|
||||
case KEYCODE_NUMPAD_6:
|
||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 'v') : "6";
|
||||
case KEYCODE_NUMPAD_7:
|
||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 'w') : "7";
|
||||
case KEYCODE_NUMPAD_8:
|
||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 'x') : "8";
|
||||
case KEYCODE_NUMPAD_9:
|
||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 'y') : "9";
|
||||
case KEYCODE_NUMPAD_EQUALS:
|
||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 'X') : "=";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String transformForModifiers(String start, int keymod, char lastChar) {
|
||||
int modifier;
|
||||
switch (keymod) {
|
||||
case KEYMOD_SHIFT:
|
||||
modifier = 2;
|
||||
break;
|
||||
case KEYMOD_ALT:
|
||||
modifier = 3;
|
||||
break;
|
||||
case (KEYMOD_SHIFT | KEYMOD_ALT):
|
||||
modifier = 4;
|
||||
break;
|
||||
case KEYMOD_CTRL:
|
||||
modifier = 5;
|
||||
break;
|
||||
case KEYMOD_SHIFT | KEYMOD_CTRL:
|
||||
modifier = 6;
|
||||
break;
|
||||
case KEYMOD_ALT | KEYMOD_CTRL:
|
||||
modifier = 7;
|
||||
break;
|
||||
case KEYMOD_SHIFT | KEYMOD_ALT | KEYMOD_CTRL:
|
||||
modifier = 8;
|
||||
break;
|
||||
default:
|
||||
return start + lastChar;
|
||||
}
|
||||
return start + (";" + modifier) + lastChar;
|
||||
}
|
||||
}
|
@@ -0,0 +1,427 @@
|
||||
package com.termux.terminal;
|
||||
|
||||
/**
|
||||
* A circular buffer of {@link TerminalRow}:s which keeps notes about what is visible on a logical screen and the scroll
|
||||
* history.
|
||||
* <p/>
|
||||
* See {@link #externalToInternalRow(int)} for how to map from logical screen rows to array indices.
|
||||
*/
|
||||
public final class TerminalBuffer {
|
||||
|
||||
TerminalRow[] mLines;
|
||||
/** The length of {@link #mLines}. */
|
||||
int mTotalRows;
|
||||
/** The number of rows and columns visible on the screen. */
|
||||
int mScreenRows, mColumns;
|
||||
/** The number of rows kept in history. */
|
||||
private int mActiveTranscriptRows = 0;
|
||||
/** The index in the circular buffer where the visible screen starts. */
|
||||
private int mScreenFirstRow = 0;
|
||||
|
||||
/**
|
||||
* 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 TerminalBuffer(int columns, int totalRows, int screenRows) {
|
||||
mColumns = columns;
|
||||
mTotalRows = totalRows;
|
||||
mScreenRows = screenRows;
|
||||
mLines = new TerminalRow[totalRows];
|
||||
|
||||
blockSet(0, 0, columns, screenRows, ' ', TextStyle.NORMAL);
|
||||
}
|
||||
|
||||
public String getTranscriptText() {
|
||||
return getSelectedText(0, -getActiveTranscriptRows(), mColumns, mScreenRows).trim();
|
||||
}
|
||||
|
||||
public String getSelectedText(int selX1, int selY1, int selX2, int selY2) {
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
final int columns = mColumns;
|
||||
|
||||
if (selY1 < -getActiveTranscriptRows()) selY1 = -getActiveTranscriptRows();
|
||||
if (selY2 >= mScreenRows) selY2 = mScreenRows - 1;
|
||||
|
||||
for (int row = selY1; row <= selY2; row++) {
|
||||
int x1 = (row == selY1) ? selX1 : 0;
|
||||
int x2;
|
||||
if (row == selY2) {
|
||||
x2 = selX2 + 1;
|
||||
if (x2 > columns) x2 = columns;
|
||||
} else {
|
||||
x2 = columns;
|
||||
}
|
||||
TerminalRow lineObject = mLines[externalToInternalRow(row)];
|
||||
int x1Index = lineObject.findStartOfColumn(x1);
|
||||
int x2Index = (x2 < mColumns) ? lineObject.findStartOfColumn(x2) : lineObject.getSpaceUsed();
|
||||
if (x2Index == x1Index) {
|
||||
// Selected the start of a wide character.
|
||||
x2Index = lineObject.findStartOfColumn(x2 + 1);
|
||||
}
|
||||
char[] line = lineObject.mText;
|
||||
int lastPrintingCharIndex = -1;
|
||||
int i;
|
||||
boolean rowLineWrap = getLineWrap(row);
|
||||
if (rowLineWrap && x2 == columns) {
|
||||
// If the line was wrapped, we shouldn't lose trailing space:
|
||||
lastPrintingCharIndex = x2Index - 1;
|
||||
} else {
|
||||
for (i = x1Index; i < x2Index; ++i) {
|
||||
char c = line[i];
|
||||
if (c != ' ') lastPrintingCharIndex = i;
|
||||
}
|
||||
}
|
||||
if (lastPrintingCharIndex != -1)
|
||||
builder.append(line, x1Index, lastPrintingCharIndex - x1Index + 1);
|
||||
if (!rowLineWrap && row < selY2 && row < mScreenRows - 1) builder.append('\n');
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
public int getActiveTranscriptRows() {
|
||||
return mActiveTranscriptRows;
|
||||
}
|
||||
|
||||
public int getActiveRows() {
|
||||
return mActiveTranscriptRows + mScreenRows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a row value from the public external coordinate system to our internal private coordinate system.
|
||||
* <p/>
|
||||
* <ul>
|
||||
* <li>External coordinate system: -mActiveTranscriptRows to mScreenRows-1, with the screen being 0..mScreenRows-1.
|
||||
* <li>Internal coordinate system: the mScreenRows lines starting at mScreenFirstRow comprise the screen, while the
|
||||
* mActiveTranscriptRows lines ending at mScreenFirstRow-1 form the transcript (as a circular buffer).
|
||||
* </ul>
|
||||
* <p/>
|
||||
* External <---> Internal:
|
||||
* <p/>
|
||||
* <pre>
|
||||
* [ ... ] [ ... ]
|
||||
* [ -mActiveTranscriptRows ] [ mScreenFirstRow - mActiveTranscriptRows ]
|
||||
* [ ... ] [ ... ]
|
||||
* [ 0 (visible screen starts here) ] <-----> [ mScreenFirstRow ]
|
||||
* [ ... ] [ ... ]
|
||||
* [ mScreenRows-1 ] [ mScreenFirstRow + mScreenRows-1 ]
|
||||
* </pre>
|
||||
*
|
||||
* @param externalRow a row in the external coordinate system.
|
||||
* @return The row corresponding to the input argument in the private coordinate system.
|
||||
*/
|
||||
public int externalToInternalRow(int externalRow) {
|
||||
if (externalRow < -mActiveTranscriptRows || externalRow > mScreenRows)
|
||||
throw new IllegalArgumentException("extRow=" + externalRow + ", mScreenRows=" + mScreenRows + ", mActiveTranscriptRows=" + mActiveTranscriptRows);
|
||||
final int internalRow = mScreenFirstRow + externalRow;
|
||||
return (internalRow < 0) ? (mTotalRows + internalRow) : (internalRow % mTotalRows);
|
||||
}
|
||||
|
||||
public void setLineWrap(int row) {
|
||||
mLines[externalToInternalRow(row)].mLineWrap = true;
|
||||
}
|
||||
|
||||
public boolean getLineWrap(int row) {
|
||||
return mLines[externalToInternalRow(row)].mLineWrap;
|
||||
}
|
||||
|
||||
public void clearLineWrap(int row) {
|
||||
mLines[externalToInternalRow(row)].mLineWrap = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize the screen which this transcript backs. Currently, this only works if the number of columns does not
|
||||
* change or the rows expand (that is, it only works when shrinking the number of rows).
|
||||
*
|
||||
* @param newColumns The number of columns the screen should have.
|
||||
* @param newRows The number of rows the screen should have.
|
||||
* @param cursor An int[2] containing the (column, row) cursor location.
|
||||
*/
|
||||
public void resize(int newColumns, int newRows, int newTotalRows, int[] cursor, long currentStyle, boolean altScreen) {
|
||||
// newRows > mTotalRows should not normally happen since mTotalRows is TRANSCRIPT_ROWS (10000):
|
||||
if (newColumns == mColumns && newRows <= mTotalRows) {
|
||||
// Fast resize where just the rows changed.
|
||||
int shiftDownOfTopRow = mScreenRows - newRows;
|
||||
if (shiftDownOfTopRow > 0 && shiftDownOfTopRow < mScreenRows) {
|
||||
// Shrinking. Check if we can skip blank rows at bottom below cursor.
|
||||
for (int i = mScreenRows - 1; i > 0; i--) {
|
||||
if (cursor[1] >= i) break;
|
||||
int r = externalToInternalRow(i);
|
||||
if (mLines[r] == null || mLines[r].isBlank()) {
|
||||
if (--shiftDownOfTopRow == 0) break;
|
||||
}
|
||||
}
|
||||
} else if (shiftDownOfTopRow < 0) {
|
||||
// Negative shift down = expanding. Only move screen up if there is transcript to show:
|
||||
int actualShift = Math.max(shiftDownOfTopRow, -mActiveTranscriptRows);
|
||||
if (shiftDownOfTopRow != actualShift) {
|
||||
// The new lines revealed by the resizing are not all from the transcript. Blank the below ones.
|
||||
for (int i = 0; i < actualShift - shiftDownOfTopRow; i++)
|
||||
allocateFullLineIfNecessary((mScreenFirstRow + mScreenRows + i) % mTotalRows).clear(currentStyle);
|
||||
shiftDownOfTopRow = actualShift;
|
||||
}
|
||||
}
|
||||
mScreenFirstRow += shiftDownOfTopRow;
|
||||
mScreenFirstRow = (mScreenFirstRow < 0) ? (mScreenFirstRow + mTotalRows) : (mScreenFirstRow % mTotalRows);
|
||||
mTotalRows = newTotalRows;
|
||||
mActiveTranscriptRows = altScreen ? 0 : Math.max(0, mActiveTranscriptRows + shiftDownOfTopRow);
|
||||
cursor[1] -= shiftDownOfTopRow;
|
||||
mScreenRows = newRows;
|
||||
} else {
|
||||
// Copy away old state and update new:
|
||||
TerminalRow[] oldLines = mLines;
|
||||
mLines = new TerminalRow[newTotalRows];
|
||||
for (int i = 0; i < newTotalRows; i++)
|
||||
mLines[i] = new TerminalRow(newColumns, currentStyle);
|
||||
|
||||
final int oldActiveTranscriptRows = mActiveTranscriptRows;
|
||||
final int oldScreenFirstRow = mScreenFirstRow;
|
||||
final int oldScreenRows = mScreenRows;
|
||||
final int oldTotalRows = mTotalRows;
|
||||
mTotalRows = newTotalRows;
|
||||
mScreenRows = newRows;
|
||||
mActiveTranscriptRows = mScreenFirstRow = 0;
|
||||
mColumns = newColumns;
|
||||
|
||||
int newCursorRow = -1;
|
||||
int newCursorColumn = -1;
|
||||
int oldCursorRow = cursor[1];
|
||||
int oldCursorColumn = cursor[0];
|
||||
boolean newCursorPlaced = false;
|
||||
|
||||
int currentOutputExternalRow = 0;
|
||||
int currentOutputExternalColumn = 0;
|
||||
|
||||
// Loop over every character in the initial state.
|
||||
// Blank lines should be skipped only if at end of transcript (just as is done in the "fast" resize), so we
|
||||
// keep track how many blank lines we have skipped if we later on find a non-blank line.
|
||||
int skippedBlankLines = 0;
|
||||
for (int externalOldRow = -oldActiveTranscriptRows; externalOldRow < oldScreenRows; externalOldRow++) {
|
||||
// Do what externalToInternalRow() does but for the old state:
|
||||
int internalOldRow = oldScreenFirstRow + externalOldRow;
|
||||
internalOldRow = (internalOldRow < 0) ? (oldTotalRows + internalOldRow) : (internalOldRow % oldTotalRows);
|
||||
|
||||
TerminalRow oldLine = oldLines[internalOldRow];
|
||||
boolean cursorAtThisRow = externalOldRow == oldCursorRow;
|
||||
// The cursor may only be on a non-null line, which we should not skip:
|
||||
if (oldLine == null || (!(!newCursorPlaced && cursorAtThisRow)) && oldLine.isBlank()) {
|
||||
skippedBlankLines++;
|
||||
continue;
|
||||
} else if (skippedBlankLines > 0) {
|
||||
// After skipping some blank lines we encounter a non-blank line. Insert the skipped blank lines.
|
||||
for (int i = 0; i < skippedBlankLines; i++) {
|
||||
if (currentOutputExternalRow == mScreenRows - 1) {
|
||||
scrollDownOneLine(0, mScreenRows, currentStyle);
|
||||
} else {
|
||||
currentOutputExternalRow++;
|
||||
}
|
||||
currentOutputExternalColumn = 0;
|
||||
}
|
||||
skippedBlankLines = 0;
|
||||
}
|
||||
|
||||
int lastNonSpaceIndex = 0;
|
||||
boolean justToCursor = false;
|
||||
if (cursorAtThisRow || oldLine.mLineWrap) {
|
||||
// Take the whole line, either because of cursor on it, or if line wrapping.
|
||||
lastNonSpaceIndex = oldLine.getSpaceUsed();
|
||||
if (cursorAtThisRow) justToCursor = true;
|
||||
} else {
|
||||
for (int i = 0; i < oldLine.getSpaceUsed(); i++)
|
||||
// NEWLY INTRODUCED BUG! Should not index oldLine.mStyle with char indices
|
||||
if (oldLine.mText[i] != ' '/* || oldLine.mStyle[i] != currentStyle */)
|
||||
lastNonSpaceIndex = i + 1;
|
||||
}
|
||||
|
||||
int currentOldCol = 0;
|
||||
long styleAtCol = 0;
|
||||
for (int i = 0; i < lastNonSpaceIndex; i++) {
|
||||
// Note that looping over java character, not cells.
|
||||
char c = oldLine.mText[i];
|
||||
int codePoint = (Character.isHighSurrogate(c)) ? Character.toCodePoint(c, oldLine.mText[++i]) : c;
|
||||
int displayWidth = WcWidth.width(codePoint);
|
||||
// Use the last style if this is a zero-width character:
|
||||
if (displayWidth > 0) styleAtCol = oldLine.getStyle(currentOldCol);
|
||||
|
||||
// Line wrap as necessary:
|
||||
if (currentOutputExternalColumn + displayWidth > mColumns) {
|
||||
setLineWrap(currentOutputExternalRow);
|
||||
if (currentOutputExternalRow == mScreenRows - 1) {
|
||||
if (newCursorPlaced) newCursorRow--;
|
||||
scrollDownOneLine(0, mScreenRows, currentStyle);
|
||||
} else {
|
||||
currentOutputExternalRow++;
|
||||
}
|
||||
currentOutputExternalColumn = 0;
|
||||
}
|
||||
|
||||
int offsetDueToCombiningChar = ((displayWidth <= 0 && currentOutputExternalColumn > 0) ? 1 : 0);
|
||||
int outputColumn = currentOutputExternalColumn - offsetDueToCombiningChar;
|
||||
setChar(outputColumn, currentOutputExternalRow, codePoint, styleAtCol);
|
||||
|
||||
if (displayWidth > 0) {
|
||||
if (oldCursorRow == externalOldRow && oldCursorColumn == currentOldCol) {
|
||||
newCursorColumn = currentOutputExternalColumn;
|
||||
newCursorRow = currentOutputExternalRow;
|
||||
newCursorPlaced = true;
|
||||
}
|
||||
currentOldCol += displayWidth;
|
||||
currentOutputExternalColumn += displayWidth;
|
||||
if (justToCursor && newCursorPlaced) break;
|
||||
}
|
||||
}
|
||||
// Old row has been copied. Check if we need to insert newline if old line was not wrapping:
|
||||
if (externalOldRow != (oldScreenRows - 1) && !oldLine.mLineWrap) {
|
||||
if (currentOutputExternalRow == mScreenRows - 1) {
|
||||
if (newCursorPlaced) newCursorRow--;
|
||||
scrollDownOneLine(0, mScreenRows, currentStyle);
|
||||
} else {
|
||||
currentOutputExternalRow++;
|
||||
}
|
||||
currentOutputExternalColumn = 0;
|
||||
}
|
||||
}
|
||||
|
||||
cursor[0] = newCursorColumn;
|
||||
cursor[1] = newCursorRow;
|
||||
}
|
||||
|
||||
// Handle cursor scrolling off screen:
|
||||
if (cursor[0] < 0 || cursor[1] < 0) cursor[0] = cursor[1] = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Block copy lines and associated metadata from one location to another in the circular buffer, taking wraparound
|
||||
* into account.
|
||||
*
|
||||
* @param srcInternal The first line to be copied.
|
||||
* @param len The number of lines to be copied.
|
||||
*/
|
||||
private void blockCopyLinesDown(int srcInternal, int len) {
|
||||
if (len == 0) return;
|
||||
int totalRows = mTotalRows;
|
||||
|
||||
int start = len - 1;
|
||||
// Save away line to be overwritten:
|
||||
TerminalRow lineToBeOverWritten = mLines[(srcInternal + start + 1) % totalRows];
|
||||
// Do the copy from bottom to top.
|
||||
for (int i = start; i >= 0; --i)
|
||||
mLines[(srcInternal + i + 1) % totalRows] = mLines[(srcInternal + i) % totalRows];
|
||||
// Put back overwritten line, now above the block:
|
||||
mLines[(srcInternal) % totalRows] = lineToBeOverWritten;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param style the style for the newly exposed line.
|
||||
*/
|
||||
public void scrollDownOneLine(int topMargin, int bottomMargin, long style) {
|
||||
if (topMargin > bottomMargin - 1 || topMargin < 0 || bottomMargin > mScreenRows)
|
||||
throw new IllegalArgumentException("topMargin=" + topMargin + ", bottomMargin=" + bottomMargin + ", mScreenRows=" + mScreenRows);
|
||||
|
||||
// Copy the fixed topMargin lines one line down so that they remain on screen in same position:
|
||||
blockCopyLinesDown(mScreenFirstRow, topMargin);
|
||||
// Copy the fixed mScreenRows-bottomMargin lines one line down so that they remain on screen in same
|
||||
// position:
|
||||
blockCopyLinesDown(externalToInternalRow(bottomMargin), mScreenRows - bottomMargin);
|
||||
|
||||
// Update the screen location in the ring buffer:
|
||||
mScreenFirstRow = (mScreenFirstRow + 1) % mTotalRows;
|
||||
// Note that the history has grown if not already full:
|
||||
if (mActiveTranscriptRows < mTotalRows - mScreenRows) mActiveTranscriptRows++;
|
||||
|
||||
// Blank the newly revealed line above the bottom margin:
|
||||
int blankRow = externalToInternalRow(bottomMargin - 1);
|
||||
if (mLines[blankRow] == null) {
|
||||
mLines[blankRow] = new TerminalRow(mColumns, style);
|
||||
} else {
|
||||
mLines[blankRow].clear(style);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 InvalidParameterException 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 (w == 0) return;
|
||||
if (sx < 0 || sx + w > mColumns || sy < 0 || sy + h > mScreenRows || dx < 0 || dx + w > mColumns || dy < 0 || dy + h > mScreenRows)
|
||||
throw new IllegalArgumentException();
|
||||
boolean copyingUp = sy > dy;
|
||||
for (int y = 0; y < h; y++) {
|
||||
int y2 = copyingUp ? y : (h - (y + 1));
|
||||
TerminalRow sourceRow = allocateFullLineIfNecessary(externalToInternalRow(sy + y2));
|
||||
allocateFullLineIfNecessary(externalToInternalRow(dy + y2)).copyInterval(sourceRow, sx, sx + w, dx);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public void blockSet(int sx, int sy, int w, int h, int val, long style) {
|
||||
if (sx < 0 || sx + w > mColumns || sy < 0 || sy + h > mScreenRows) {
|
||||
throw new IllegalArgumentException(
|
||||
"Illegal arguments! blockSet(" + sx + ", " + sy + ", " + w + ", " + h + ", " + val + ", " + mColumns + ", " + mScreenRows + ")");
|
||||
}
|
||||
for (int y = 0; y < h; y++)
|
||||
for (int x = 0; x < w; x++)
|
||||
setChar(sx + x, sy + y, val, style);
|
||||
}
|
||||
|
||||
public TerminalRow allocateFullLineIfNecessary(int row) {
|
||||
return (mLines[row] == null) ? (mLines[row] = new TerminalRow(mColumns, 0)) : mLines[row];
|
||||
}
|
||||
|
||||
public void setChar(int column, int row, int codePoint, long style) {
|
||||
if (row >= mScreenRows || column >= mColumns)
|
||||
throw new IllegalArgumentException("row=" + row + ", column=" + column + ", mScreenRows=" + mScreenRows + ", mColumns=" + mColumns);
|
||||
row = externalToInternalRow(row);
|
||||
allocateFullLineIfNecessary(row).setChar(column, codePoint, style);
|
||||
}
|
||||
|
||||
public long getStyleAt(int externalRow, int column) {
|
||||
return allocateFullLineIfNecessary(externalToInternalRow(externalRow)).getStyle(column);
|
||||
}
|
||||
|
||||
/** Support for http://vt100.net/docs/vt510-rm/DECCARA and http://vt100.net/docs/vt510-rm/DECCARA */
|
||||
public void setOrClearEffect(int bits, boolean setOrClear, boolean reverse, boolean rectangular, int leftMargin, int rightMargin, int top, int left,
|
||||
int bottom, int right) {
|
||||
for (int y = top; y < bottom; y++) {
|
||||
TerminalRow line = mLines[externalToInternalRow(y)];
|
||||
int startOfLine = (rectangular || y == top) ? left : leftMargin;
|
||||
int endOfLine = (rectangular || y + 1 == bottom) ? right : rightMargin;
|
||||
for (int x = startOfLine; x < endOfLine; x++) {
|
||||
long currentStyle = line.getStyle(x);
|
||||
int foreColor = TextStyle.decodeForeColor(currentStyle);
|
||||
int backColor = TextStyle.decodeBackColor(currentStyle);
|
||||
int effect = TextStyle.decodeEffect(currentStyle);
|
||||
if (reverse) {
|
||||
// Clear out the bits to reverse and add them back in reversed:
|
||||
effect = (effect & ~bits) | (bits & ~effect);
|
||||
} else if (setOrClear) {
|
||||
effect |= bits;
|
||||
} else {
|
||||
effect &= ~bits;
|
||||
}
|
||||
line.mStyle[x] = TextStyle.encode(foreColor, backColor, effect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,103 @@
|
||||
package com.termux.terminal;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* Color scheme for a terminal with default colors, which may be overridden (and then reset) from the shell using
|
||||
* Operating System Control (OSC) sequences.
|
||||
*
|
||||
* @see TerminalColors
|
||||
*/
|
||||
public final class TerminalColorScheme {
|
||||
|
||||
/** http://upload.wikimedia.org/wikipedia/en/1/15/Xterm_256color_chart.svg, but with blue color brighter. */
|
||||
private static final int[] DEFAULT_COLORSCHEME = {
|
||||
// 16 original colors. First 8 are dim.
|
||||
0xff000000, // black
|
||||
0xffcd0000, // dim red
|
||||
0xff00cd00, // dim green
|
||||
0xffcdcd00, // dim yellow
|
||||
0xff6495ed, // dim blue
|
||||
0xffcd00cd, // dim magenta
|
||||
0xff00cdcd, // dim cyan
|
||||
0xffe5e5e5, // dim white
|
||||
// Second 8 are bright:
|
||||
0xff7f7f7f, // medium grey
|
||||
0xffff0000, // bright red
|
||||
0xff00ff00, // bright green
|
||||
0xffffff00, // bright yellow
|
||||
0xff5c5cff, // light blue
|
||||
0xffff00ff, // bright magenta
|
||||
0xff00ffff, // bright cyan
|
||||
0xffffffff, // bright white
|
||||
|
||||
// 216 color cube, six shades of each color:
|
||||
0xff000000, 0xff00005f, 0xff000087, 0xff0000af, 0xff0000d7, 0xff0000ff, 0xff005f00, 0xff005f5f, 0xff005f87, 0xff005faf, 0xff005fd7, 0xff005fff,
|
||||
0xff008700, 0xff00875f, 0xff008787, 0xff0087af, 0xff0087d7, 0xff0087ff, 0xff00af00, 0xff00af5f, 0xff00af87, 0xff00afaf, 0xff00afd7, 0xff00afff,
|
||||
0xff00d700, 0xff00d75f, 0xff00d787, 0xff00d7af, 0xff00d7d7, 0xff00d7ff, 0xff00ff00, 0xff00ff5f, 0xff00ff87, 0xff00ffaf, 0xff00ffd7, 0xff00ffff,
|
||||
0xff5f0000, 0xff5f005f, 0xff5f0087, 0xff5f00af, 0xff5f00d7, 0xff5f00ff, 0xff5f5f00, 0xff5f5f5f, 0xff5f5f87, 0xff5f5faf, 0xff5f5fd7, 0xff5f5fff,
|
||||
0xff5f8700, 0xff5f875f, 0xff5f8787, 0xff5f87af, 0xff5f87d7, 0xff5f87ff, 0xff5faf00, 0xff5faf5f, 0xff5faf87, 0xff5fafaf, 0xff5fafd7, 0xff5fafff,
|
||||
0xff5fd700, 0xff5fd75f, 0xff5fd787, 0xff5fd7af, 0xff5fd7d7, 0xff5fd7ff, 0xff5fff00, 0xff5fff5f, 0xff5fff87, 0xff5fffaf, 0xff5fffd7, 0xff5fffff,
|
||||
0xff870000, 0xff87005f, 0xff870087, 0xff8700af, 0xff8700d7, 0xff8700ff, 0xff875f00, 0xff875f5f, 0xff875f87, 0xff875faf, 0xff875fd7, 0xff875fff,
|
||||
0xff878700, 0xff87875f, 0xff878787, 0xff8787af, 0xff8787d7, 0xff8787ff, 0xff87af00, 0xff87af5f, 0xff87af87, 0xff87afaf, 0xff87afd7, 0xff87afff,
|
||||
0xff87d700, 0xff87d75f, 0xff87d787, 0xff87d7af, 0xff87d7d7, 0xff87d7ff, 0xff87ff00, 0xff87ff5f, 0xff87ff87, 0xff87ffaf, 0xff87ffd7, 0xff87ffff,
|
||||
0xffaf0000, 0xffaf005f, 0xffaf0087, 0xffaf00af, 0xffaf00d7, 0xffaf00ff, 0xffaf5f00, 0xffaf5f5f, 0xffaf5f87, 0xffaf5faf, 0xffaf5fd7, 0xffaf5fff,
|
||||
0xffaf8700, 0xffaf875f, 0xffaf8787, 0xffaf87af, 0xffaf87d7, 0xffaf87ff, 0xffafaf00, 0xffafaf5f, 0xffafaf87, 0xffafafaf, 0xffafafd7, 0xffafafff,
|
||||
0xffafd700, 0xffafd75f, 0xffafd787, 0xffafd7af, 0xffafd7d7, 0xffafd7ff, 0xffafff00, 0xffafff5f, 0xffafff87, 0xffafffaf, 0xffafffd7, 0xffafffff,
|
||||
0xffd70000, 0xffd7005f, 0xffd70087, 0xffd700af, 0xffd700d7, 0xffd700ff, 0xffd75f00, 0xffd75f5f, 0xffd75f87, 0xffd75faf, 0xffd75fd7, 0xffd75fff,
|
||||
0xffd78700, 0xffd7875f, 0xffd78787, 0xffd787af, 0xffd787d7, 0xffd787ff, 0xffd7af00, 0xffd7af5f, 0xffd7af87, 0xffd7afaf, 0xffd7afd7, 0xffd7afff,
|
||||
0xffd7d700, 0xffd7d75f, 0xffd7d787, 0xffd7d7af, 0xffd7d7d7, 0xffd7d7ff, 0xffd7ff00, 0xffd7ff5f, 0xffd7ff87, 0xffd7ffaf, 0xffd7ffd7, 0xffd7ffff,
|
||||
0xffff0000, 0xffff005f, 0xffff0087, 0xffff00af, 0xffff00d7, 0xffff00ff, 0xffff5f00, 0xffff5f5f, 0xffff5f87, 0xffff5faf, 0xffff5fd7, 0xffff5fff,
|
||||
0xffff8700, 0xffff875f, 0xffff8787, 0xffff87af, 0xffff87d7, 0xffff87ff, 0xffffaf00, 0xffffaf5f, 0xffffaf87, 0xffffafaf, 0xffffafd7, 0xffffafff,
|
||||
0xffffd700, 0xffffd75f, 0xffffd787, 0xffffd7af, 0xffffd7d7, 0xffffd7ff, 0xffffff00, 0xffffff5f, 0xffffff87, 0xffffffaf, 0xffffffd7, 0xffffffff,
|
||||
|
||||
// 24 grey scale ramp:
|
||||
0xff080808, 0xff121212, 0xff1c1c1c, 0xff262626, 0xff303030, 0xff3a3a3a, 0xff444444, 0xff4e4e4e, 0xff585858, 0xff626262, 0xff6c6c6c, 0xff767676,
|
||||
0xff808080, 0xff8a8a8a, 0xff949494, 0xff9e9e9e, 0xffa8a8a8, 0xffb2b2b2, 0xffbcbcbc, 0xffc6c6c6, 0xffd0d0d0, 0xffdadada, 0xffe4e4e4, 0xffeeeeee,
|
||||
|
||||
// COLOR_INDEX_DEFAULT_FOREGROUND, COLOR_INDEX_DEFAULT_BACKGROUND and COLOR_INDEX_DEFAULT_CURSOR:
|
||||
0xffffffff, 0xff000000, 0xffA9AAA9};
|
||||
|
||||
public final int[] mDefaultColors = new int[TextStyle.NUM_INDEXED_COLORS];
|
||||
|
||||
public TerminalColorScheme() {
|
||||
reset();
|
||||
}
|
||||
|
||||
private void reset() {
|
||||
System.arraycopy(DEFAULT_COLORSCHEME, 0, mDefaultColors, 0, TextStyle.NUM_INDEXED_COLORS);
|
||||
}
|
||||
|
||||
public void updateWith(Properties props) {
|
||||
reset();
|
||||
for (Map.Entry<Object, Object> entries : props.entrySet()) {
|
||||
String key = (String) entries.getKey();
|
||||
String value = (String) entries.getValue();
|
||||
int colorIndex;
|
||||
|
||||
if (key.equals("foreground")) {
|
||||
colorIndex = TextStyle.COLOR_INDEX_FOREGROUND;
|
||||
} else if (key.equals("background")) {
|
||||
colorIndex = TextStyle.COLOR_INDEX_BACKGROUND;
|
||||
} else if (key.equals("cursor")) {
|
||||
colorIndex = TextStyle.COLOR_INDEX_CURSOR;
|
||||
} else if (key.startsWith("color")) {
|
||||
try {
|
||||
colorIndex = Integer.parseInt(key.substring(5));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException("Invalid property: '" + key + "'");
|
||||
}
|
||||
} else {
|
||||
throw new IllegalArgumentException("Invalid property: '" + key + "'");
|
||||
}
|
||||
|
||||
int colorValue = TerminalColors.parse(value);
|
||||
if (colorValue == 0)
|
||||
throw new IllegalArgumentException("Property '" + key + "' has invalid color: '" + value + "'");
|
||||
|
||||
mDefaultColors[colorIndex] = colorValue;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,76 @@
|
||||
package com.termux.terminal;
|
||||
|
||||
/** Current terminal colors (if different from default). */
|
||||
public final class TerminalColors {
|
||||
|
||||
/** Static data - a bit ugly but ok for now. */
|
||||
public static final TerminalColorScheme COLOR_SCHEME = new TerminalColorScheme();
|
||||
|
||||
/**
|
||||
* The current terminal colors, which are normally set from the color theme, but may be set dynamically with the OSC
|
||||
* 4 control sequence.
|
||||
*/
|
||||
public final int[] mCurrentColors = new int[TextStyle.NUM_INDEXED_COLORS];
|
||||
|
||||
/** Create a new instance with default colors from the theme. */
|
||||
public TerminalColors() {
|
||||
reset();
|
||||
}
|
||||
|
||||
/** Reset a particular indexed color with the default color from the color theme. */
|
||||
public void reset(int index) {
|
||||
mCurrentColors[index] = COLOR_SCHEME.mDefaultColors[index];
|
||||
}
|
||||
|
||||
/** Reset all indexed colors with the default color from the color theme. */
|
||||
public void reset() {
|
||||
System.arraycopy(COLOR_SCHEME.mDefaultColors, 0, mCurrentColors, 0, TextStyle.NUM_INDEXED_COLORS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse color according to http://manpages.ubuntu.com/manpages/intrepid/man3/XQueryColor.3.html
|
||||
* <p/>
|
||||
* Highest bit is set if successful, so return value is 0xFF${R}${G}${B}. Return 0 if failed.
|
||||
*/
|
||||
static int parse(String c) {
|
||||
try {
|
||||
int skipInitial, skipBetween;
|
||||
if (c.charAt(0) == '#') {
|
||||
// #RGB, #RRGGBB, #RRRGGGBBB or #RRRRGGGGBBBB. Most significant bits.
|
||||
skipInitial = 1;
|
||||
skipBetween = 0;
|
||||
} else if (c.startsWith("rgb:")) {
|
||||
// rgb:<red>/<green>/<blue> where <red>, <green>, <blue> := h | hh | hhh | hhhh. Scaled.
|
||||
skipInitial = 4;
|
||||
skipBetween = 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
int charsForColors = c.length() - skipInitial - 2 * skipBetween;
|
||||
if (charsForColors % 3 != 0) return 0; // Unequal lengths.
|
||||
int componentLength = charsForColors / 3;
|
||||
double mult = 255 / (Math.pow(2, componentLength * 4) - 1);
|
||||
|
||||
int currentPosition = skipInitial;
|
||||
String rString = c.substring(currentPosition, currentPosition + componentLength);
|
||||
currentPosition += componentLength + skipBetween;
|
||||
String gString = c.substring(currentPosition, currentPosition + componentLength);
|
||||
currentPosition += componentLength + skipBetween;
|
||||
String bString = c.substring(currentPosition, currentPosition + componentLength);
|
||||
|
||||
int r = (int) (Integer.parseInt(rString, 16) * mult);
|
||||
int g = (int) (Integer.parseInt(gString, 16) * mult);
|
||||
int b = (int) (Integer.parseInt(bString, 16) * mult);
|
||||
return 0xFF << 24 | r << 16 | g << 8 | b;
|
||||
} catch (NumberFormatException | IndexOutOfBoundsException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/** Try parse a color from a text parameter and into a specified index. */
|
||||
public void tryParseColor(int intoIndex, String textParameter) {
|
||||
int c = parse(textParameter);
|
||||
if (c != 0) mCurrentColors[intoIndex] = c;
|
||||
}
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
||||
package com.termux.terminal;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/** A client which receives callbacks from events triggered by feeding input to a {@link TerminalEmulator}. */
|
||||
public abstract class TerminalOutput {
|
||||
|
||||
/** Write a string using the UTF-8 encoding to the terminal client. */
|
||||
public final void write(String data) {
|
||||
byte[] bytes = data.getBytes(StandardCharsets.UTF_8);
|
||||
write(bytes, 0, bytes.length);
|
||||
}
|
||||
|
||||
/** Write bytes to the terminal client. */
|
||||
public abstract void write(byte[] data, int offset, int count);
|
||||
|
||||
/** Notify the terminal client that the terminal title has changed. */
|
||||
public abstract void titleChanged(String oldTitle, String newTitle);
|
||||
|
||||
/** Notify the terminal client that the terminal title has changed. */
|
||||
public abstract void clipboardText(String text);
|
||||
|
||||
/** Notify the terminal client that a bell character (ASCII 7, bell, BEL, \a, ^G)) has been received. */
|
||||
public abstract void onBell();
|
||||
|
||||
public abstract void onColorsChanged();
|
||||
|
||||
}
|
@@ -0,0 +1,232 @@
|
||||
package com.termux.terminal;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* A row in a terminal, composed of a fixed number of cells.
|
||||
* <p/>
|
||||
* The text in the row is stored in a char[] array, {@link #mText}, for quick access during rendering.
|
||||
*/
|
||||
public final class TerminalRow {
|
||||
|
||||
private static final float SPARE_CAPACITY_FACTOR = 1.5f;
|
||||
|
||||
/** The number of columns in this terminal row. */
|
||||
private final int mColumns;
|
||||
/** The text filling this terminal row. */
|
||||
public char[] mText;
|
||||
/** The number of java char:s used in {@link #mText}. */
|
||||
private short mSpaceUsed;
|
||||
/** If this row has been line wrapped due to text output at the end of line. */
|
||||
boolean mLineWrap;
|
||||
/** The style bits of each cell in the row. See {@link TextStyle}. */
|
||||
final long[] mStyle;
|
||||
|
||||
/** Construct a blank row (containing only whitespace, ' ') with a specified style. */
|
||||
public TerminalRow(int columns, long style) {
|
||||
mColumns = columns;
|
||||
mText = new char[(int) (SPARE_CAPACITY_FACTOR * columns)];
|
||||
mStyle = new long[columns];
|
||||
clear(style);
|
||||
}
|
||||
|
||||
/** NOTE: The sourceX2 is exclusive. */
|
||||
public void copyInterval(TerminalRow line, int sourceX1, int sourceX2, int destinationX) {
|
||||
final int x1 = line.findStartOfColumn(sourceX1);
|
||||
final int x2 = line.findStartOfColumn(sourceX2);
|
||||
boolean startingFromSecondHalfOfWideChar = (sourceX1 > 0 && line.wideDisplayCharacterStartingAt(sourceX1 - 1));
|
||||
final char[] sourceChars = (this == line) ? Arrays.copyOf(line.mText, line.mText.length) : line.mText;
|
||||
int latestNonCombiningWidth = 0;
|
||||
for (int i = x1; i < x2; i++) {
|
||||
char sourceChar = sourceChars[i];
|
||||
int codePoint = Character.isHighSurrogate(sourceChar) ? Character.toCodePoint(sourceChar, sourceChars[++i]) : sourceChar;
|
||||
if (startingFromSecondHalfOfWideChar) {
|
||||
// Just treat copying second half of wide char as copying whitespace.
|
||||
codePoint = ' ';
|
||||
startingFromSecondHalfOfWideChar = false;
|
||||
}
|
||||
int w = WcWidth.width(codePoint);
|
||||
if (w > 0) {
|
||||
destinationX += latestNonCombiningWidth;
|
||||
sourceX1 += latestNonCombiningWidth;
|
||||
latestNonCombiningWidth = w;
|
||||
}
|
||||
setChar(destinationX, codePoint, line.getStyle(sourceX1));
|
||||
}
|
||||
}
|
||||
|
||||
public int getSpaceUsed() {
|
||||
return mSpaceUsed;
|
||||
}
|
||||
|
||||
/** Note that the column may end of second half of wide character. */
|
||||
public int findStartOfColumn(int column) {
|
||||
if (column == mColumns) return getSpaceUsed();
|
||||
|
||||
int currentColumn = 0;
|
||||
int currentCharIndex = 0;
|
||||
while (true) { // 0<2 1 < 2
|
||||
int newCharIndex = currentCharIndex;
|
||||
char c = mText[newCharIndex++]; // cci=1, cci=2
|
||||
boolean isHigh = Character.isHighSurrogate(c);
|
||||
int codePoint = isHigh ? Character.toCodePoint(c, mText[newCharIndex++]) : c;
|
||||
int wcwidth = WcWidth.width(codePoint); // 1, 2
|
||||
if (wcwidth > 0) {
|
||||
currentColumn += wcwidth;
|
||||
if (currentColumn == column) {
|
||||
while (newCharIndex < mSpaceUsed) {
|
||||
// Skip combining chars.
|
||||
if (Character.isHighSurrogate(mText[newCharIndex])) {
|
||||
if (WcWidth.width(Character.toCodePoint(mText[newCharIndex], mText[newCharIndex + 1])) <= 0) {
|
||||
newCharIndex += 2;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else if (WcWidth.width(mText[newCharIndex]) <= 0) {
|
||||
newCharIndex++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return newCharIndex;
|
||||
} else if (currentColumn > column) {
|
||||
// Wide column going past end.
|
||||
return currentCharIndex;
|
||||
}
|
||||
}
|
||||
currentCharIndex = newCharIndex;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean wideDisplayCharacterStartingAt(int column) {
|
||||
for (int currentCharIndex = 0, currentColumn = 0; currentCharIndex < mSpaceUsed; ) {
|
||||
char c = mText[currentCharIndex++];
|
||||
int codePoint = Character.isHighSurrogate(c) ? Character.toCodePoint(c, mText[currentCharIndex++]) : c;
|
||||
int wcwidth = WcWidth.width(codePoint);
|
||||
if (wcwidth > 0) {
|
||||
if (currentColumn == column && wcwidth == 2) return true;
|
||||
currentColumn += wcwidth;
|
||||
if (currentColumn > column) return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void clear(long style) {
|
||||
Arrays.fill(mText, ' ');
|
||||
Arrays.fill(mStyle, style);
|
||||
mSpaceUsed = (short) mColumns;
|
||||
}
|
||||
|
||||
// https://github.com/steven676/Android-Terminal-Emulator/commit/9a47042620bec87617f0b4f5d50568535668fe26
|
||||
public void setChar(int columnToSet, int codePoint, long style) {
|
||||
mStyle[columnToSet] = style;
|
||||
|
||||
final int newCodePointDisplayWidth = WcWidth.width(codePoint);
|
||||
final boolean newIsCombining = newCodePointDisplayWidth <= 0;
|
||||
|
||||
boolean wasExtraColForWideChar = (columnToSet > 0) && wideDisplayCharacterStartingAt(columnToSet - 1);
|
||||
|
||||
if (newIsCombining) {
|
||||
// When standing at second half of wide character and inserting combining:
|
||||
if (wasExtraColForWideChar) columnToSet--;
|
||||
} else {
|
||||
// Check if we are overwriting the second half of a wide character starting at the previous column:
|
||||
if (wasExtraColForWideChar) setChar(columnToSet - 1, ' ', style);
|
||||
// Check if we are overwriting the first half of a wide character starting at the next column:
|
||||
boolean overwritingWideCharInNextColumn = newCodePointDisplayWidth == 2 && wideDisplayCharacterStartingAt(columnToSet + 1);
|
||||
if (overwritingWideCharInNextColumn) setChar(columnToSet + 1, ' ', style);
|
||||
}
|
||||
|
||||
char[] text = mText;
|
||||
final int oldStartOfColumnIndex = findStartOfColumn(columnToSet);
|
||||
final int oldCodePointDisplayWidth = WcWidth.width(text, oldStartOfColumnIndex);
|
||||
|
||||
// Get the number of elements in the mText array this column uses now
|
||||
int oldCharactersUsedForColumn;
|
||||
if (columnToSet + oldCodePointDisplayWidth < mColumns) {
|
||||
oldCharactersUsedForColumn = findStartOfColumn(columnToSet + oldCodePointDisplayWidth) - oldStartOfColumnIndex;
|
||||
} else {
|
||||
// Last character.
|
||||
oldCharactersUsedForColumn = mSpaceUsed - oldStartOfColumnIndex;
|
||||
}
|
||||
|
||||
// Find how many chars this column will need
|
||||
int newCharactersUsedForColumn = Character.charCount(codePoint);
|
||||
if (newIsCombining) {
|
||||
// Combining characters are added to the contents of the column instead of overwriting them, so that they
|
||||
// modify the existing contents.
|
||||
// FIXME: Put a limit of combining characters.
|
||||
// FIXME: Unassigned characters also get width=0.
|
||||
newCharactersUsedForColumn += oldCharactersUsedForColumn;
|
||||
}
|
||||
|
||||
int oldNextColumnIndex = oldStartOfColumnIndex + oldCharactersUsedForColumn;
|
||||
int newNextColumnIndex = oldStartOfColumnIndex + newCharactersUsedForColumn;
|
||||
|
||||
final int javaCharDifference = newCharactersUsedForColumn - oldCharactersUsedForColumn;
|
||||
if (javaCharDifference > 0) {
|
||||
// Shift the rest of the line right.
|
||||
int oldCharactersAfterColumn = mSpaceUsed - oldNextColumnIndex;
|
||||
if (mSpaceUsed + javaCharDifference > text.length) {
|
||||
// We need to grow the array
|
||||
char[] newText = new char[text.length + mColumns];
|
||||
System.arraycopy(text, 0, newText, 0, oldStartOfColumnIndex + oldCharactersUsedForColumn);
|
||||
System.arraycopy(text, oldNextColumnIndex, newText, newNextColumnIndex, oldCharactersAfterColumn);
|
||||
mText = text = newText;
|
||||
} else {
|
||||
System.arraycopy(text, oldNextColumnIndex, text, newNextColumnIndex, oldCharactersAfterColumn);
|
||||
}
|
||||
} else if (javaCharDifference < 0) {
|
||||
// Shift the rest of the line left.
|
||||
System.arraycopy(text, oldNextColumnIndex, text, newNextColumnIndex, mSpaceUsed - oldNextColumnIndex);
|
||||
}
|
||||
mSpaceUsed += javaCharDifference;
|
||||
|
||||
// Store char. A combining character is stored at the end of the existing contents so that it modifies them:
|
||||
//noinspection ResultOfMethodCallIgnored - since we already now how many java chars is used.
|
||||
Character.toChars(codePoint, text, oldStartOfColumnIndex + (newIsCombining ? oldCharactersUsedForColumn : 0));
|
||||
|
||||
if (oldCodePointDisplayWidth == 2 && newCodePointDisplayWidth == 1) {
|
||||
// Replace second half of wide char with a space. Which mean that we actually add a ' ' java character.
|
||||
if (mSpaceUsed + 1 > text.length) {
|
||||
char[] newText = new char[text.length + mColumns];
|
||||
System.arraycopy(text, 0, newText, 0, newNextColumnIndex);
|
||||
System.arraycopy(text, newNextColumnIndex, newText, newNextColumnIndex + 1, mSpaceUsed - newNextColumnIndex);
|
||||
mText = text = newText;
|
||||
} else {
|
||||
System.arraycopy(text, newNextColumnIndex, text, newNextColumnIndex + 1, mSpaceUsed - newNextColumnIndex);
|
||||
}
|
||||
text[newNextColumnIndex] = ' ';
|
||||
|
||||
++mSpaceUsed;
|
||||
} else if (oldCodePointDisplayWidth == 1 && newCodePointDisplayWidth == 2) {
|
||||
if (columnToSet == mColumns - 1) {
|
||||
throw new IllegalArgumentException("Cannot put wide character in last column");
|
||||
} else if (columnToSet == mColumns - 2) {
|
||||
// Truncate the line to the second part of this wide char:
|
||||
mSpaceUsed = (short) newNextColumnIndex;
|
||||
} else {
|
||||
// Overwrite the contents of the next column, which mean we actually remove java characters. Due to the
|
||||
// check at the beginning of this method we know that we are not overwriting a wide char.
|
||||
int newNextNextColumnIndex = newNextColumnIndex + (Character.isHighSurrogate(mText[newNextColumnIndex]) ? 2 : 1);
|
||||
int nextLen = newNextNextColumnIndex - newNextColumnIndex;
|
||||
|
||||
// Shift the array leftwards.
|
||||
System.arraycopy(text, newNextNextColumnIndex, text, newNextColumnIndex, mSpaceUsed - newNextNextColumnIndex);
|
||||
mSpaceUsed -= nextLen;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean isBlank() {
|
||||
for (int charIndex = 0, charLen = getSpaceUsed(); charIndex < charLen; charIndex++)
|
||||
if (mText[charIndex] != ' ') return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public final long getStyle(int column) {
|
||||
return mStyle[column];
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,342 @@
|
||||
package com.termux.terminal;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.system.ErrnoException;
|
||||
import android.system.Os;
|
||||
import android.system.OsConstants;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* A terminal session, consisting of a process coupled to a terminal interface.
|
||||
* <p/>
|
||||
* The subprocess will be executed by the constructor, and when the size is made known by a call to
|
||||
* {@link #updateSize(int, int)} terminal emulation will begin and threads will be spawned to handle the subprocess I/O.
|
||||
* All terminal emulation and callback methods will be performed on the main thread.
|
||||
* <p/>
|
||||
* The child process may be exited forcefully by using the {@link #finishIfRunning()} method.
|
||||
* <p/>
|
||||
* NOTE: The terminal session may outlive the EmulatorView, so be careful with callbacks!
|
||||
*/
|
||||
public final class TerminalSession extends TerminalOutput {
|
||||
|
||||
/** Callback to be invoked when a {@link TerminalSession} changes. */
|
||||
public interface SessionChangedCallback {
|
||||
void onTextChanged(TerminalSession changedSession);
|
||||
|
||||
void onTitleChanged(TerminalSession changedSession);
|
||||
|
||||
void onSessionFinished(TerminalSession finishedSession);
|
||||
|
||||
void onClipboardText(TerminalSession session, String text);
|
||||
|
||||
void onBell(TerminalSession session);
|
||||
|
||||
void onColorsChanged(TerminalSession session);
|
||||
|
||||
}
|
||||
|
||||
private static FileDescriptor wrapFileDescriptor(int fileDescriptor) {
|
||||
FileDescriptor result = new FileDescriptor();
|
||||
try {
|
||||
Field descriptorField;
|
||||
try {
|
||||
descriptorField = FileDescriptor.class.getDeclaredField("descriptor");
|
||||
} catch (NoSuchFieldException e) {
|
||||
// For desktop java:
|
||||
descriptorField = FileDescriptor.class.getDeclaredField("fd");
|
||||
}
|
||||
descriptorField.setAccessible(true);
|
||||
descriptorField.set(result, fileDescriptor);
|
||||
} catch (NoSuchFieldException | IllegalAccessException | IllegalArgumentException e) {
|
||||
Log.wtf(EmulatorDebug.LOG_TAG, "Error accessing FileDescriptor#descriptor private field", e);
|
||||
System.exit(1);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static final int MSG_NEW_INPUT = 1;
|
||||
private static final int MSG_PROCESS_EXITED = 4;
|
||||
|
||||
public final String mHandle = UUID.randomUUID().toString();
|
||||
|
||||
TerminalEmulator mEmulator;
|
||||
|
||||
/**
|
||||
* A queue written to from a separate thread when the process outputs, and read by main thread to process by
|
||||
* terminal emulator.
|
||||
*/
|
||||
final ByteQueue mProcessToTerminalIOQueue = new ByteQueue(4096);
|
||||
/**
|
||||
* A queue written to from the main thread due to user interaction, and read by another thread which forwards by
|
||||
* writing to the {@link #mTerminalFileDescriptor}.
|
||||
*/
|
||||
final ByteQueue mTerminalToProcessIOQueue = new ByteQueue(4096);
|
||||
/** Buffer to write translate code points into utf8 before writing to mTerminalToProcessIOQueue */
|
||||
private final byte[] mUtf8InputBuffer = new byte[5];
|
||||
|
||||
/** Callback which gets notified when a session finishes or changes title. */
|
||||
final SessionChangedCallback mChangeCallback;
|
||||
|
||||
/** The pid of the shell process. 0 if not started and -1 if finished running. */
|
||||
int mShellPid;
|
||||
|
||||
/** The exit status of the shell process. Only valid if ${@link #mShellPid} is -1. */
|
||||
int mShellExitStatus;
|
||||
|
||||
/**
|
||||
* The file descriptor referencing the master half of a pseudo-terminal pair, resulting from calling
|
||||
* {@link JNI#createSubprocess(String, String, String[], String[], int[], int, int)}.
|
||||
*/
|
||||
private int mTerminalFileDescriptor;
|
||||
|
||||
/** Set by the application for user identification of session, not by terminal. */
|
||||
public String mSessionName;
|
||||
|
||||
@SuppressLint("HandlerLeak")
|
||||
final Handler mMainThreadHandler = new Handler() {
|
||||
final byte[] mReceiveBuffer = new byte[4 * 1024];
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
if (msg.what == MSG_NEW_INPUT && isRunning()) {
|
||||
int bytesRead = mProcessToTerminalIOQueue.read(mReceiveBuffer, false);
|
||||
if (bytesRead > 0) {
|
||||
mEmulator.append(mReceiveBuffer, bytesRead);
|
||||
notifyScreenUpdate();
|
||||
}
|
||||
} else if (msg.what == MSG_PROCESS_EXITED) {
|
||||
int exitCode = (Integer) msg.obj;
|
||||
cleanupResources(exitCode);
|
||||
mChangeCallback.onSessionFinished(TerminalSession.this);
|
||||
|
||||
String exitDescription = "\r\n[Process completed";
|
||||
if (exitCode > 0) {
|
||||
// Non-zero process exit.
|
||||
exitDescription += " (code " + exitCode + ")";
|
||||
} else if (exitCode < 0) {
|
||||
// Negated signal.
|
||||
exitDescription += " (signal " + (-exitCode) + ")";
|
||||
}
|
||||
exitDescription += " - press Enter]";
|
||||
|
||||
byte[] bytesToWrite = exitDescription.getBytes(StandardCharsets.UTF_8);
|
||||
mEmulator.append(bytesToWrite, bytesToWrite.length);
|
||||
notifyScreenUpdate();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private final String mShellPath;
|
||||
private final String mCwd;
|
||||
private final String[] mArgs;
|
||||
private final String[] mEnv;
|
||||
|
||||
public TerminalSession(String shellPath, String cwd, String[] args, String[] env, SessionChangedCallback changeCallback) {
|
||||
mChangeCallback = changeCallback;
|
||||
|
||||
this.mShellPath = shellPath;
|
||||
this.mCwd = cwd;
|
||||
this.mArgs = args;
|
||||
this.mEnv = env;
|
||||
}
|
||||
|
||||
/** Inform the attached pty of the new size and reflow or initialize the emulator. */
|
||||
public void updateSize(int columns, int rows) {
|
||||
if (mEmulator == null) {
|
||||
initializeEmulator(columns, rows);
|
||||
} else {
|
||||
JNI.setPtyWindowSize(mTerminalFileDescriptor, rows, columns);
|
||||
mEmulator.resize(columns, rows);
|
||||
}
|
||||
}
|
||||
|
||||
/** The terminal title as set through escape sequences or null if none set. */
|
||||
public String getTitle() {
|
||||
return (mEmulator == null) ? null : mEmulator.getTitle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the terminal emulator's window size and start terminal emulation.
|
||||
*
|
||||
* @param columns The number of columns in the terminal window.
|
||||
* @param rows The number of rows in the terminal window.
|
||||
*/
|
||||
public void initializeEmulator(int columns, int rows) {
|
||||
mEmulator = new TerminalEmulator(this, columns, rows, /* transcript= */2000);
|
||||
|
||||
int[] processId = new int[1];
|
||||
mTerminalFileDescriptor = JNI.createSubprocess(mShellPath, mCwd, mArgs, mEnv, processId, rows, columns);
|
||||
mShellPid = processId[0];
|
||||
|
||||
final FileDescriptor terminalFileDescriptorWrapped = wrapFileDescriptor(mTerminalFileDescriptor);
|
||||
|
||||
new Thread("TermSessionInputReader[pid=" + mShellPid + "]") {
|
||||
@Override
|
||||
public void run() {
|
||||
try (InputStream termIn = new FileInputStream(terminalFileDescriptorWrapped)) {
|
||||
final byte[] buffer = new byte[4096];
|
||||
while (true) {
|
||||
int read = termIn.read(buffer);
|
||||
if (read == -1) return;
|
||||
if (!mProcessToTerminalIOQueue.write(buffer, 0, read)) return;
|
||||
mMainThreadHandler.sendEmptyMessage(MSG_NEW_INPUT);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Ignore, just shutting down.
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
|
||||
new Thread("TermSessionOutputWriter[pid=" + mShellPid + "]") {
|
||||
@Override
|
||||
public void run() {
|
||||
final byte[] buffer = new byte[4096];
|
||||
try (FileOutputStream termOut = new FileOutputStream(terminalFileDescriptorWrapped)) {
|
||||
while (true) {
|
||||
int bytesToWrite = mTerminalToProcessIOQueue.read(buffer, true);
|
||||
if (bytesToWrite == -1) return;
|
||||
termOut.write(buffer, 0, bytesToWrite);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Ignore.
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
|
||||
new Thread("TermSessionWaiter[pid=" + mShellPid + "]") {
|
||||
@Override
|
||||
public void run() {
|
||||
int processExitCode = JNI.waitFor(mShellPid);
|
||||
mMainThreadHandler.sendMessage(mMainThreadHandler.obtainMessage(MSG_PROCESS_EXITED, processExitCode));
|
||||
}
|
||||
}.start();
|
||||
|
||||
}
|
||||
|
||||
/** Write data to the shell process. */
|
||||
@Override
|
||||
public void write(byte[] data, int offset, int count) {
|
||||
if (mShellPid > 0) mTerminalToProcessIOQueue.write(data, offset, count);
|
||||
}
|
||||
|
||||
/** Write the Unicode code point to the terminal encoded in UTF-8. */
|
||||
public void writeCodePoint(boolean prependEscape, int codePoint) {
|
||||
if (codePoint > 1114111 || (codePoint >= 0xD800 && codePoint <= 0xDFFF)) {
|
||||
// 1114111 (= 2**16 + 1024**2 - 1) is the highest code point, [0xD800,0xDFFF] is the surrogate range.
|
||||
throw new IllegalArgumentException("Invalid code point: " + codePoint);
|
||||
}
|
||||
|
||||
int bufferPosition = 0;
|
||||
if (prependEscape) mUtf8InputBuffer[bufferPosition++] = 27;
|
||||
|
||||
if (codePoint <= /* 7 bits */0b1111111) {
|
||||
mUtf8InputBuffer[bufferPosition++] = (byte) codePoint;
|
||||
} else if (codePoint <= /* 11 bits */0b11111111111) {
|
||||
/* 110xxxxx leading byte with leading 5 bits */
|
||||
mUtf8InputBuffer[bufferPosition++] = (byte) (0b11000000 | (codePoint >> 6));
|
||||
/* 10xxxxxx continuation byte with following 6 bits */
|
||||
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | (codePoint & 0b111111));
|
||||
} else if (codePoint <= /* 16 bits */0b1111111111111111) {
|
||||
/* 1110xxxx leading byte with leading 4 bits */
|
||||
mUtf8InputBuffer[bufferPosition++] = (byte) (0b11100000 | (codePoint >> 12));
|
||||
/* 10xxxxxx continuation byte with following 6 bits */
|
||||
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | ((codePoint >> 6) & 0b111111));
|
||||
/* 10xxxxxx continuation byte with following 6 bits */
|
||||
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | (codePoint & 0b111111));
|
||||
} else { /* We have checked codePoint <= 1114111 above, so we have max 21 bits = 0b111111111111111111111 */
|
||||
/* 11110xxx leading byte with leading 3 bits */
|
||||
mUtf8InputBuffer[bufferPosition++] = (byte) (0b11110000 | (codePoint >> 18));
|
||||
/* 10xxxxxx continuation byte with following 6 bits */
|
||||
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | ((codePoint >> 12) & 0b111111));
|
||||
/* 10xxxxxx continuation byte with following 6 bits */
|
||||
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | ((codePoint >> 6) & 0b111111));
|
||||
/* 10xxxxxx continuation byte with following 6 bits */
|
||||
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | (codePoint & 0b111111));
|
||||
}
|
||||
write(mUtf8InputBuffer, 0, bufferPosition);
|
||||
}
|
||||
|
||||
public TerminalEmulator getEmulator() {
|
||||
return mEmulator;
|
||||
}
|
||||
|
||||
/** Notify the {@link #mChangeCallback} that the screen has changed. */
|
||||
protected void notifyScreenUpdate() {
|
||||
mChangeCallback.onTextChanged(this);
|
||||
}
|
||||
|
||||
/** Reset state for terminal emulator state. */
|
||||
public void reset() {
|
||||
mEmulator.reset();
|
||||
notifyScreenUpdate();
|
||||
}
|
||||
|
||||
/** Finish this terminal session by sending SIGKILL to the shell. */
|
||||
public void finishIfRunning() {
|
||||
if (isRunning()) {
|
||||
try {
|
||||
Os.kill(mShellPid, OsConstants.SIGKILL);
|
||||
} catch (ErrnoException e) {
|
||||
Log.w("termux", "Failed sending SIGKILL: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Cleanup resources when the process exits. */
|
||||
void cleanupResources(int exitStatus) {
|
||||
synchronized (this) {
|
||||
mShellPid = -1;
|
||||
mShellExitStatus = exitStatus;
|
||||
}
|
||||
|
||||
// Stop the reader and writer threads, and close the I/O streams
|
||||
mTerminalToProcessIOQueue.close();
|
||||
mProcessToTerminalIOQueue.close();
|
||||
JNI.close(mTerminalFileDescriptor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void titleChanged(String oldTitle, String newTitle) {
|
||||
mChangeCallback.onTitleChanged(this);
|
||||
}
|
||||
|
||||
public synchronized boolean isRunning() {
|
||||
return mShellPid != -1;
|
||||
}
|
||||
|
||||
/** Only valid if not {@link #isRunning()}. */
|
||||
public synchronized int getExitStatus() {
|
||||
return mShellExitStatus;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clipboardText(String text) {
|
||||
mChangeCallback.onClipboardText(this, text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBell() {
|
||||
mChangeCallback.onBell(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onColorsChanged() {
|
||||
mChangeCallback.onColorsChanged(this);
|
||||
}
|
||||
|
||||
public int getPid() {
|
||||
return mShellPid;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,86 @@
|
||||
package com.termux.terminal;
|
||||
|
||||
/**
|
||||
* Encodes effects, foreground and background colors into a 64 bit long, which are stored for each cell in a terminal
|
||||
* row in {@link TerminalRow#mStyle}.
|
||||
* <p/>
|
||||
* The bit layout is:
|
||||
* - 16 flags (11 currently used).
|
||||
* - 24 for foreground color (only 9 first bits if a color index).
|
||||
* - 24 for background color (only 9 first bits if a color index).
|
||||
*/
|
||||
public final class TextStyle {
|
||||
|
||||
public final static int CHARACTER_ATTRIBUTE_BOLD = 1;
|
||||
public final static int CHARACTER_ATTRIBUTE_ITALIC = 1 << 1;
|
||||
public final static int CHARACTER_ATTRIBUTE_UNDERLINE = 1 << 2;
|
||||
public final static int CHARACTER_ATTRIBUTE_BLINK = 1 << 3;
|
||||
public final static int CHARACTER_ATTRIBUTE_INVERSE = 1 << 4;
|
||||
public final static int CHARACTER_ATTRIBUTE_INVISIBLE = 1 << 5;
|
||||
public final static int CHARACTER_ATTRIBUTE_STRIKETHROUGH = 1 << 6;
|
||||
/**
|
||||
* The selective erase control functions (DECSED and DECSEL) can only erase characters defined as erasable.
|
||||
* <p/>
|
||||
* This bit is set if DECSCA (Select Character Protection Attribute) has been used to define the characters that
|
||||
* come after it as erasable from the screen.
|
||||
*/
|
||||
public final static int CHARACTER_ATTRIBUTE_PROTECTED = 1 << 7;
|
||||
/** Dim colors. Also known as faint or half intensity. */
|
||||
public final static int CHARACTER_ATTRIBUTE_DIM = 1 << 8;
|
||||
/** If true (24-bit) color is used for the cell for foreground. */
|
||||
private final static int CHARACTER_ATTRIBUTE_TRUECOLOR_FOREGROUND = 1 << 9;
|
||||
/** If true (24-bit) color is used for the cell for foreground. */
|
||||
private final static int CHARACTER_ATTRIBUTE_TRUECOLOR_BACKGROUND= 1 << 10;
|
||||
|
||||
public final static int COLOR_INDEX_FOREGROUND = 256;
|
||||
public final static int COLOR_INDEX_BACKGROUND = 257;
|
||||
public final static int COLOR_INDEX_CURSOR = 258;
|
||||
|
||||
/** The 256 standard color entries and the three special (foreground, background and cursor) ones. */
|
||||
public final static int NUM_INDEXED_COLORS = 259;
|
||||
|
||||
/** Normal foreground and background colors and no effects. */
|
||||
final static long NORMAL = encode(COLOR_INDEX_FOREGROUND, COLOR_INDEX_BACKGROUND, 0);
|
||||
|
||||
static long encode(int foreColor, int backColor, int effect) {
|
||||
long result = effect & 0b111111111;
|
||||
if ((0xff000000 & foreColor) == 0xff000000) {
|
||||
// 24-bit color.
|
||||
result |= CHARACTER_ATTRIBUTE_TRUECOLOR_FOREGROUND | ((foreColor & 0x00ffffffL) << 40L);
|
||||
} else {
|
||||
// Indexed color.
|
||||
result |= (foreColor & 0b111111111L) << 40;
|
||||
}
|
||||
if ((0xff000000 & backColor) == 0xff000000) {
|
||||
// 24-bit color.
|
||||
result |= CHARACTER_ATTRIBUTE_TRUECOLOR_BACKGROUND | ((backColor & 0x00ffffffL) << 16L);
|
||||
} else {
|
||||
// Indexed color.
|
||||
result |= (backColor & 0b111111111L) << 16L;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static int decodeForeColor(long style) {
|
||||
if ((style & CHARACTER_ATTRIBUTE_TRUECOLOR_FOREGROUND) == 0) {
|
||||
return (int) ((style >>> 40) & 0b111111111L);
|
||||
} else {
|
||||
return 0xff000000 | (int) ((style >>> 40) & 0x00ffffffL);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static int decodeBackColor(long style) {
|
||||
if ((style & CHARACTER_ATTRIBUTE_TRUECOLOR_BACKGROUND) == 0) {
|
||||
return (int) ((style >>> 16) & 0b111111111L);
|
||||
} else {
|
||||
return 0xff000000 | (int) ((style >>> 16) & 0x00ffffffL);
|
||||
}
|
||||
}
|
||||
|
||||
public static int decodeEffect(long style) {
|
||||
return (int) (style & 0b11111111111);
|
||||
}
|
||||
|
||||
}
|
458
terminal-emulator/src/main/java/com/termux/terminal/WcWidth.java
Normal file
458
terminal-emulator/src/main/java/com/termux/terminal/WcWidth.java
Normal file
@@ -0,0 +1,458 @@
|
||||
package com.termux.terminal;
|
||||
|
||||
/**
|
||||
* Implementation of wcwidth(3) for Unicode 9.
|
||||
*
|
||||
* Implementation from https://github.com/jquast/wcwidth but we return 0 for unprintable characters.
|
||||
*/
|
||||
public final class WcWidth {
|
||||
|
||||
// From https://github.com/jquast/wcwidth/blob/master/wcwidth/table_zero.py
|
||||
// t commit 0d7de112202cc8b2ebe9232ff4a5c954f19d561a (2016-07-02):
|
||||
private static final int[][] ZERO_WIDTH = {
|
||||
{0x0300, 0x036f}, // Combining Grave Accent ..Combining Latin Small Le
|
||||
{0x0483, 0x0489}, // Combining Cyrillic Titlo..Combining Cyrillic Milli
|
||||
{0x0591, 0x05bd}, // Hebrew Accent Etnahta ..Hebrew Point Meteg
|
||||
{0x05bf, 0x05bf}, // Hebrew Point Rafe ..Hebrew Point Rafe
|
||||
{0x05c1, 0x05c2}, // Hebrew Point Shin Dot ..Hebrew Point Sin Dot
|
||||
{0x05c4, 0x05c5}, // Hebrew Mark Upper Dot ..Hebrew Mark Lower Dot
|
||||
{0x05c7, 0x05c7}, // Hebrew Point Qamats Qata..Hebrew Point Qamats Qata
|
||||
{0x0610, 0x061a}, // Arabic Sign Sallallahou ..Arabic Small Kasra
|
||||
{0x064b, 0x065f}, // Arabic Fathatan ..Arabic Wavy Hamza Below
|
||||
{0x0670, 0x0670}, // Arabic Letter Superscrip..Arabic Letter Superscrip
|
||||
{0x06d6, 0x06dc}, // Arabic Small High Ligatu..Arabic Small High Seen
|
||||
{0x06df, 0x06e4}, // Arabic Small High Rounde..Arabic Small High Madda
|
||||
{0x06e7, 0x06e8}, // Arabic Small High Yeh ..Arabic Small High Noon
|
||||
{0x06ea, 0x06ed}, // Arabic Empty Centre Low ..Arabic Small Low Meem
|
||||
{0x0711, 0x0711}, // Syriac Letter Superscrip..Syriac Letter Superscrip
|
||||
{0x0730, 0x074a}, // Syriac Pthaha Above ..Syriac Barrekh
|
||||
{0x07a6, 0x07b0}, // Thaana Abafili ..Thaana Sukun
|
||||
{0x07eb, 0x07f3}, // Nko Combining Sh||t High..Nko Combining Double Dot
|
||||
{0x0816, 0x0819}, // Samaritan Mark In ..Samaritan Mark Dagesh
|
||||
{0x081b, 0x0823}, // Samaritan Mark Epentheti..Samaritan Vowel Sign A
|
||||
{0x0825, 0x0827}, // Samaritan Vowel Sign Sho..Samaritan Vowel Sign U
|
||||
{0x0829, 0x082d}, // Samaritan Vowel Sign Lon..Samaritan Mark Nequdaa
|
||||
{0x0859, 0x085b}, // Mandaic Affrication Mark..Mandaic Gemination Mark
|
||||
{0x08d4, 0x08e1}, // (nil) ..
|
||||
{0x08e3, 0x0902}, // Arabic Turned Damma Belo..Devanagari Sign Anusvara
|
||||
{0x093a, 0x093a}, // Devanagari Vowel Sign Oe..Devanagari Vowel Sign Oe
|
||||
{0x093c, 0x093c}, // Devanagari Sign Nukta ..Devanagari Sign Nukta
|
||||
{0x0941, 0x0948}, // Devanagari Vowel Sign U ..Devanagari Vowel Sign Ai
|
||||
{0x094d, 0x094d}, // Devanagari Sign Virama ..Devanagari Sign Virama
|
||||
{0x0951, 0x0957}, // Devanagari Stress Sign U..Devanagari Vowel Sign Uu
|
||||
{0x0962, 0x0963}, // Devanagari Vowel Sign Vo..Devanagari Vowel Sign Vo
|
||||
{0x0981, 0x0981}, // Bengali Sign Candrabindu..Bengali Sign Candrabindu
|
||||
{0x09bc, 0x09bc}, // Bengali Sign Nukta ..Bengali Sign Nukta
|
||||
{0x09c1, 0x09c4}, // Bengali Vowel Sign U ..Bengali Vowel Sign Vocal
|
||||
{0x09cd, 0x09cd}, // Bengali Sign Virama ..Bengali Sign Virama
|
||||
{0x09e2, 0x09e3}, // Bengali Vowel Sign Vocal..Bengali Vowel Sign Vocal
|
||||
{0x0a01, 0x0a02}, // Gurmukhi Sign Adak Bindi..Gurmukhi Sign Bindi
|
||||
{0x0a3c, 0x0a3c}, // Gurmukhi Sign Nukta ..Gurmukhi Sign Nukta
|
||||
{0x0a41, 0x0a42}, // Gurmukhi Vowel Sign U ..Gurmukhi Vowel Sign Uu
|
||||
{0x0a47, 0x0a48}, // Gurmukhi Vowel Sign Ee ..Gurmukhi Vowel Sign Ai
|
||||
{0x0a4b, 0x0a4d}, // Gurmukhi Vowel Sign Oo ..Gurmukhi Sign Virama
|
||||
{0x0a51, 0x0a51}, // Gurmukhi Sign Udaat ..Gurmukhi Sign Udaat
|
||||
{0x0a70, 0x0a71}, // Gurmukhi Tippi ..Gurmukhi Addak
|
||||
{0x0a75, 0x0a75}, // Gurmukhi Sign Yakash ..Gurmukhi Sign Yakash
|
||||
{0x0a81, 0x0a82}, // Gujarati Sign Candrabind..Gujarati Sign Anusvara
|
||||
{0x0abc, 0x0abc}, // Gujarati Sign Nukta ..Gujarati Sign Nukta
|
||||
{0x0ac1, 0x0ac5}, // Gujarati Vowel Sign U ..Gujarati Vowel Sign Cand
|
||||
{0x0ac7, 0x0ac8}, // Gujarati Vowel Sign E ..Gujarati Vowel Sign Ai
|
||||
{0x0acd, 0x0acd}, // Gujarati Sign Virama ..Gujarati Sign Virama
|
||||
{0x0ae2, 0x0ae3}, // Gujarati Vowel Sign Voca..Gujarati Vowel Sign Voca
|
||||
{0x0b01, 0x0b01}, // ||iya Sign Candrabindu ..||iya Sign Candrabindu
|
||||
{0x0b3c, 0x0b3c}, // ||iya Sign Nukta ..||iya Sign Nukta
|
||||
{0x0b3f, 0x0b3f}, // ||iya Vowel Sign I ..||iya Vowel Sign I
|
||||
{0x0b41, 0x0b44}, // ||iya Vowel Sign U ..||iya Vowel Sign Vocalic
|
||||
{0x0b4d, 0x0b4d}, // ||iya Sign Virama ..||iya Sign Virama
|
||||
{0x0b56, 0x0b56}, // ||iya Ai Length Mark ..||iya Ai Length Mark
|
||||
{0x0b62, 0x0b63}, // ||iya Vowel Sign Vocalic..||iya Vowel Sign Vocalic
|
||||
{0x0b82, 0x0b82}, // Tamil Sign Anusvara ..Tamil Sign Anusvara
|
||||
{0x0bc0, 0x0bc0}, // Tamil Vowel Sign Ii ..Tamil Vowel Sign Ii
|
||||
{0x0bcd, 0x0bcd}, // Tamil Sign Virama ..Tamil Sign Virama
|
||||
{0x0c00, 0x0c00}, // Telugu Sign Combining Ca..Telugu Sign Combining Ca
|
||||
{0x0c3e, 0x0c40}, // Telugu Vowel Sign Aa ..Telugu Vowel Sign Ii
|
||||
{0x0c46, 0x0c48}, // Telugu Vowel Sign E ..Telugu Vowel Sign Ai
|
||||
{0x0c4a, 0x0c4d}, // Telugu Vowel Sign O ..Telugu Sign Virama
|
||||
{0x0c55, 0x0c56}, // Telugu Length Mark ..Telugu Ai Length Mark
|
||||
{0x0c62, 0x0c63}, // Telugu Vowel Sign Vocali..Telugu Vowel Sign Vocali
|
||||
{0x0c81, 0x0c81}, // Kannada Sign Candrabindu..Kannada Sign Candrabindu
|
||||
{0x0cbc, 0x0cbc}, // Kannada Sign Nukta ..Kannada Sign Nukta
|
||||
{0x0cbf, 0x0cbf}, // Kannada Vowel Sign I ..Kannada Vowel Sign I
|
||||
{0x0cc6, 0x0cc6}, // Kannada Vowel Sign E ..Kannada Vowel Sign E
|
||||
{0x0ccc, 0x0ccd}, // Kannada Vowel Sign Au ..Kannada Sign Virama
|
||||
{0x0ce2, 0x0ce3}, // Kannada Vowel Sign Vocal..Kannada Vowel Sign Vocal
|
||||
{0x0d01, 0x0d01}, // Malayalam Sign Candrabin..Malayalam Sign Candrabin
|
||||
{0x0d41, 0x0d44}, // Malayalam Vowel Sign U ..Malayalam Vowel Sign Voc
|
||||
{0x0d4d, 0x0d4d}, // Malayalam Sign Virama ..Malayalam Sign Virama
|
||||
{0x0d62, 0x0d63}, // Malayalam Vowel Sign Voc..Malayalam Vowel Sign Voc
|
||||
{0x0dca, 0x0dca}, // Sinhala Sign Al-lakuna ..Sinhala Sign Al-lakuna
|
||||
{0x0dd2, 0x0dd4}, // Sinhala Vowel Sign Ketti..Sinhala Vowel Sign Ketti
|
||||
{0x0dd6, 0x0dd6}, // Sinhala Vowel Sign Diga ..Sinhala Vowel Sign Diga
|
||||
{0x0e31, 0x0e31}, // Thai Character Mai Han-a..Thai Character Mai Han-a
|
||||
{0x0e34, 0x0e3a}, // Thai Character Sara I ..Thai Character Phinthu
|
||||
{0x0e47, 0x0e4e}, // Thai Character Maitaikhu..Thai Character Yamakkan
|
||||
{0x0eb1, 0x0eb1}, // Lao Vowel Sign Mai Kan ..Lao Vowel Sign Mai Kan
|
||||
{0x0eb4, 0x0eb9}, // Lao Vowel Sign I ..Lao Vowel Sign Uu
|
||||
{0x0ebb, 0x0ebc}, // Lao Vowel Sign Mai Kon ..Lao Semivowel Sign Lo
|
||||
{0x0ec8, 0x0ecd}, // Lao Tone Mai Ek ..Lao Niggahita
|
||||
{0x0f18, 0x0f19}, // Tibetan Astrological Sig..Tibetan Astrological Sig
|
||||
{0x0f35, 0x0f35}, // Tibetan Mark Ngas Bzung ..Tibetan Mark Ngas Bzung
|
||||
{0x0f37, 0x0f37}, // Tibetan Mark Ngas Bzung ..Tibetan Mark Ngas Bzung
|
||||
{0x0f39, 0x0f39}, // Tibetan Mark Tsa -phru ..Tibetan Mark Tsa -phru
|
||||
{0x0f71, 0x0f7e}, // Tibetan Vowel Sign Aa ..Tibetan Sign Rjes Su Nga
|
||||
{0x0f80, 0x0f84}, // Tibetan Vowel Sign Rever..Tibetan Mark Halanta
|
||||
{0x0f86, 0x0f87}, // Tibetan Sign Lci Rtags ..Tibetan Sign Yang Rtags
|
||||
{0x0f8d, 0x0f97}, // Tibetan Subjoined Sign L..Tibetan Subjoined Letter
|
||||
{0x0f99, 0x0fbc}, // Tibetan Subjoined Letter..Tibetan Subjoined Letter
|
||||
{0x0fc6, 0x0fc6}, // Tibetan Symbol Padma Gda..Tibetan Symbol Padma Gda
|
||||
{0x102d, 0x1030}, // Myanmar Vowel Sign I ..Myanmar Vowel Sign Uu
|
||||
{0x1032, 0x1037}, // Myanmar Vowel Sign Ai ..Myanmar Sign Dot Below
|
||||
{0x1039, 0x103a}, // Myanmar Sign Virama ..Myanmar Sign Asat
|
||||
{0x103d, 0x103e}, // Myanmar Consonant Sign M..Myanmar Consonant Sign M
|
||||
{0x1058, 0x1059}, // Myanmar Vowel Sign Vocal..Myanmar Vowel Sign Vocal
|
||||
{0x105e, 0x1060}, // Myanmar Consonant Sign M..Myanmar Consonant Sign M
|
||||
{0x1071, 0x1074}, // Myanmar Vowel Sign Geba ..Myanmar Vowel Sign Kayah
|
||||
{0x1082, 0x1082}, // Myanmar Consonant Sign S..Myanmar Consonant Sign S
|
||||
{0x1085, 0x1086}, // Myanmar Vowel Sign Shan ..Myanmar Vowel Sign Shan
|
||||
{0x108d, 0x108d}, // Myanmar Sign Shan Counci..Myanmar Sign Shan Counci
|
||||
{0x109d, 0x109d}, // Myanmar Vowel Sign Aiton..Myanmar Vowel Sign Aiton
|
||||
{0x135d, 0x135f}, // Ethiopic Combining Gemin..Ethiopic Combining Gemin
|
||||
{0x1712, 0x1714}, // Tagalog Vowel Sign I ..Tagalog Sign Virama
|
||||
{0x1732, 0x1734}, // Hanunoo Vowel Sign I ..Hanunoo Sign Pamudpod
|
||||
{0x1752, 0x1753}, // Buhid Vowel Sign I ..Buhid Vowel Sign U
|
||||
{0x1772, 0x1773}, // Tagbanwa Vowel Sign I ..Tagbanwa Vowel Sign U
|
||||
{0x17b4, 0x17b5}, // Khmer Vowel Inherent Aq ..Khmer Vowel Inherent Aa
|
||||
{0x17b7, 0x17bd}, // Khmer Vowel Sign I ..Khmer Vowel Sign Ua
|
||||
{0x17c6, 0x17c6}, // Khmer Sign Nikahit ..Khmer Sign Nikahit
|
||||
{0x17c9, 0x17d3}, // Khmer Sign Muusikatoan ..Khmer Sign Bathamasat
|
||||
{0x17dd, 0x17dd}, // Khmer Sign Atthacan ..Khmer Sign Atthacan
|
||||
{0x180b, 0x180d}, // Mongolian Free Variation..Mongolian Free Variation
|
||||
{0x1885, 0x1886}, // Mongolian Letter Ali Gal..Mongolian Letter Ali Gal
|
||||
{0x18a9, 0x18a9}, // Mongolian Letter Ali Gal..Mongolian Letter Ali Gal
|
||||
{0x1920, 0x1922}, // Limbu Vowel Sign A ..Limbu Vowel Sign U
|
||||
{0x1927, 0x1928}, // Limbu Vowel Sign E ..Limbu Vowel Sign O
|
||||
{0x1932, 0x1932}, // Limbu Small Letter Anusv..Limbu Small Letter Anusv
|
||||
{0x1939, 0x193b}, // Limbu Sign Mukphreng ..Limbu Sign Sa-i
|
||||
{0x1a17, 0x1a18}, // Buginese Vowel Sign I ..Buginese Vowel Sign U
|
||||
{0x1a1b, 0x1a1b}, // Buginese Vowel Sign Ae ..Buginese Vowel Sign Ae
|
||||
{0x1a56, 0x1a56}, // Tai Tham Consonant Sign ..Tai Tham Consonant Sign
|
||||
{0x1a58, 0x1a5e}, // Tai Tham Sign Mai Kang L..Tai Tham Consonant Sign
|
||||
{0x1a60, 0x1a60}, // Tai Tham Sign Sakot ..Tai Tham Sign Sakot
|
||||
{0x1a62, 0x1a62}, // Tai Tham Vowel Sign Mai ..Tai Tham Vowel Sign Mai
|
||||
{0x1a65, 0x1a6c}, // Tai Tham Vowel Sign I ..Tai Tham Vowel Sign Oa B
|
||||
{0x1a73, 0x1a7c}, // Tai Tham Vowel Sign Oa A..Tai Tham Sign Khuen-lue
|
||||
{0x1a7f, 0x1a7f}, // Tai Tham Combining Crypt..Tai Tham Combining Crypt
|
||||
{0x1ab0, 0x1abe}, // Combining Doubled Circum..Combining Parentheses Ov
|
||||
{0x1b00, 0x1b03}, // Balinese Sign Ulu Ricem ..Balinese Sign Surang
|
||||
{0x1b34, 0x1b34}, // Balinese Sign Rerekan ..Balinese Sign Rerekan
|
||||
{0x1b36, 0x1b3a}, // Balinese Vowel Sign Ulu ..Balinese Vowel Sign Ra R
|
||||
{0x1b3c, 0x1b3c}, // Balinese Vowel Sign La L..Balinese Vowel Sign La L
|
||||
{0x1b42, 0x1b42}, // Balinese Vowel Sign Pepe..Balinese Vowel Sign Pepe
|
||||
{0x1b6b, 0x1b73}, // Balinese Musical Symbol ..Balinese Musical Symbol
|
||||
{0x1b80, 0x1b81}, // Sundanese Sign Panyecek ..Sundanese Sign Panglayar
|
||||
{0x1ba2, 0x1ba5}, // Sundanese Consonant Sign..Sundanese Vowel Sign Pan
|
||||
{0x1ba8, 0x1ba9}, // Sundanese Vowel Sign Pam..Sundanese Vowel Sign Pan
|
||||
{0x1bab, 0x1bad}, // Sundanese Sign Virama ..Sundanese Consonant Sign
|
||||
{0x1be6, 0x1be6}, // Batak Sign Tompi ..Batak Sign Tompi
|
||||
{0x1be8, 0x1be9}, // Batak Vowel Sign Pakpak ..Batak Vowel Sign Ee
|
||||
{0x1bed, 0x1bed}, // Batak Vowel Sign Karo O ..Batak Vowel Sign Karo O
|
||||
{0x1bef, 0x1bf1}, // Batak Vowel Sign U F|| S..Batak Consonant Sign H
|
||||
{0x1c2c, 0x1c33}, // Lepcha Vowel Sign E ..Lepcha Consonant Sign T
|
||||
{0x1c36, 0x1c37}, // Lepcha Sign Ran ..Lepcha Sign Nukta
|
||||
{0x1cd0, 0x1cd2}, // Vedic Tone Karshana ..Vedic Tone Prenkha
|
||||
{0x1cd4, 0x1ce0}, // Vedic Sign Yajurvedic Mi..Vedic Tone Rigvedic Kash
|
||||
{0x1ce2, 0x1ce8}, // Vedic Sign Visarga Svari..Vedic Sign Visarga Anuda
|
||||
{0x1ced, 0x1ced}, // Vedic Sign Tiryak ..Vedic Sign Tiryak
|
||||
{0x1cf4, 0x1cf4}, // Vedic Tone Candra Above ..Vedic Tone Candra Above
|
||||
{0x1cf8, 0x1cf9}, // Vedic Tone Ring Above ..Vedic Tone Double Ring A
|
||||
{0x1dc0, 0x1df5}, // Combining Dotted Grave A..Combining Up Tack Above
|
||||
{0x1dfb, 0x1dff}, // (nil) ..Combining Right Arrowhea
|
||||
{0x20d0, 0x20f0}, // Combining Left Harpoon A..Combining Asterisk Above
|
||||
{0x2cef, 0x2cf1}, // Coptic Combining Ni Abov..Coptic Combining Spiritu
|
||||
{0x2d7f, 0x2d7f}, // Tifinagh Consonant Joine..Tifinagh Consonant Joine
|
||||
{0x2de0, 0x2dff}, // Combining Cyrillic Lette..Combining Cyrillic Lette
|
||||
{0x302a, 0x302d}, // Ideographic Level Tone M..Ideographic Entering Ton
|
||||
{0x3099, 0x309a}, // Combining Katakana-hirag..Combining Katakana-hirag
|
||||
{0xa66f, 0xa672}, // Combining Cyrillic Vzmet..Combining Cyrillic Thous
|
||||
{0xa674, 0xa67d}, // Combining Cyrillic Lette..Combining Cyrillic Payer
|
||||
{0xa69e, 0xa69f}, // Combining Cyrillic Lette..Combining Cyrillic Lette
|
||||
{0xa6f0, 0xa6f1}, // Bamum Combining Mark Koq..Bamum Combining Mark Tuk
|
||||
{0xa802, 0xa802}, // Syloti Nagri Sign Dvisva..Syloti Nagri Sign Dvisva
|
||||
{0xa806, 0xa806}, // Syloti Nagri Sign Hasant..Syloti Nagri Sign Hasant
|
||||
{0xa80b, 0xa80b}, // Syloti Nagri Sign Anusva..Syloti Nagri Sign Anusva
|
||||
{0xa825, 0xa826}, // Syloti Nagri Vowel Sign ..Syloti Nagri Vowel Sign
|
||||
{0xa8c4, 0xa8c5}, // Saurashtra Sign Virama ..
|
||||
{0xa8e0, 0xa8f1}, // Combining Devanagari Dig..Combining Devanagari Sig
|
||||
{0xa926, 0xa92d}, // Kayah Li Vowel Ue ..Kayah Li Tone Calya Plop
|
||||
{0xa947, 0xa951}, // Rejang Vowel Sign I ..Rejang Consonant Sign R
|
||||
{0xa980, 0xa982}, // Javanese Sign Panyangga ..Javanese Sign Layar
|
||||
{0xa9b3, 0xa9b3}, // Javanese Sign Cecak Telu..Javanese Sign Cecak Telu
|
||||
{0xa9b6, 0xa9b9}, // Javanese Vowel Sign Wulu..Javanese Vowel Sign Suku
|
||||
{0xa9bc, 0xa9bc}, // Javanese Vowel Sign Pepe..Javanese Vowel Sign Pepe
|
||||
{0xa9e5, 0xa9e5}, // Myanmar Sign Shan Saw ..Myanmar Sign Shan Saw
|
||||
{0xaa29, 0xaa2e}, // Cham Vowel Sign Aa ..Cham Vowel Sign Oe
|
||||
{0xaa31, 0xaa32}, // Cham Vowel Sign Au ..Cham Vowel Sign Ue
|
||||
{0xaa35, 0xaa36}, // Cham Consonant Sign La ..Cham Consonant Sign Wa
|
||||
{0xaa43, 0xaa43}, // Cham Consonant Sign Fina..Cham Consonant Sign Fina
|
||||
{0xaa4c, 0xaa4c}, // Cham Consonant Sign Fina..Cham Consonant Sign Fina
|
||||
{0xaa7c, 0xaa7c}, // Myanmar Sign Tai Laing T..Myanmar Sign Tai Laing T
|
||||
{0xaab0, 0xaab0}, // Tai Viet Mai Kang ..Tai Viet Mai Kang
|
||||
{0xaab2, 0xaab4}, // Tai Viet Vowel I ..Tai Viet Vowel U
|
||||
{0xaab7, 0xaab8}, // Tai Viet Mai Khit ..Tai Viet Vowel Ia
|
||||
{0xaabe, 0xaabf}, // Tai Viet Vowel Am ..Tai Viet Tone Mai Ek
|
||||
{0xaac1, 0xaac1}, // Tai Viet Tone Mai Tho ..Tai Viet Tone Mai Tho
|
||||
{0xaaec, 0xaaed}, // Meetei Mayek Vowel Sign ..Meetei Mayek Vowel Sign
|
||||
{0xaaf6, 0xaaf6}, // Meetei Mayek Virama ..Meetei Mayek Virama
|
||||
{0xabe5, 0xabe5}, // Meetei Mayek Vowel Sign ..Meetei Mayek Vowel Sign
|
||||
{0xabe8, 0xabe8}, // Meetei Mayek Vowel Sign ..Meetei Mayek Vowel Sign
|
||||
{0xabed, 0xabed}, // Meetei Mayek Apun Iyek ..Meetei Mayek Apun Iyek
|
||||
{0xfb1e, 0xfb1e}, // Hebrew Point Judeo-spani..Hebrew Point Judeo-spani
|
||||
{0xfe00, 0xfe0f}, // Variation Select||-1 ..Variation Select||-16
|
||||
{0xfe20, 0xfe2f}, // Combining Ligature Left ..Combining Cyrillic Titlo
|
||||
{0x101fd, 0x101fd}, // Phaistos Disc Sign Combi..Phaistos Disc Sign Combi
|
||||
{0x102e0, 0x102e0}, // Coptic Epact Thousands M..Coptic Epact Thousands M
|
||||
{0x10376, 0x1037a}, // Combining Old Permic Let..Combining Old Permic Let
|
||||
{0x10a01, 0x10a03}, // Kharoshthi Vowel Sign I ..Kharoshthi Vowel Sign Vo
|
||||
{0x10a05, 0x10a06}, // Kharoshthi Vowel Sign E ..Kharoshthi Vowel Sign O
|
||||
{0x10a0c, 0x10a0f}, // Kharoshthi Vowel Length ..Kharoshthi Sign Visarga
|
||||
{0x10a38, 0x10a3a}, // Kharoshthi Sign Bar Abov..Kharoshthi Sign Dot Belo
|
||||
{0x10a3f, 0x10a3f}, // Kharoshthi Virama ..Kharoshthi Virama
|
||||
{0x10ae5, 0x10ae6}, // Manichaean Abbreviation ..Manichaean Abbreviation
|
||||
{0x11001, 0x11001}, // Brahmi Sign Anusvara ..Brahmi Sign Anusvara
|
||||
{0x11038, 0x11046}, // Brahmi Vowel Sign Aa ..Brahmi Virama
|
||||
{0x1107f, 0x11081}, // Brahmi Number Joiner ..Kaithi Sign Anusvara
|
||||
{0x110b3, 0x110b6}, // Kaithi Vowel Sign U ..Kaithi Vowel Sign Ai
|
||||
{0x110b9, 0x110ba}, // Kaithi Sign Virama ..Kaithi Sign Nukta
|
||||
{0x11100, 0x11102}, // Chakma Sign Candrabindu ..Chakma Sign Visarga
|
||||
{0x11127, 0x1112b}, // Chakma Vowel Sign A ..Chakma Vowel Sign Uu
|
||||
{0x1112d, 0x11134}, // Chakma Vowel Sign Ai ..Chakma Maayyaa
|
||||
{0x11173, 0x11173}, // Mahajani Sign Nukta ..Mahajani Sign Nukta
|
||||
{0x11180, 0x11181}, // Sharada Sign Candrabindu..Sharada Sign Anusvara
|
||||
{0x111b6, 0x111be}, // Sharada Vowel Sign U ..Sharada Vowel Sign O
|
||||
{0x111ca, 0x111cc}, // Sharada Sign Nukta ..Sharada Extra Sh||t Vowe
|
||||
{0x1122f, 0x11231}, // Khojki Vowel Sign U ..Khojki Vowel Sign Ai
|
||||
{0x11234, 0x11234}, // Khojki Sign Anusvara ..Khojki Sign Anusvara
|
||||
{0x11236, 0x11237}, // Khojki Sign Nukta ..Khojki Sign Shadda
|
||||
{0x1123e, 0x1123e}, // (nil) ..
|
||||
{0x112df, 0x112df}, // Khudawadi Sign Anusvara ..Khudawadi Sign Anusvara
|
||||
{0x112e3, 0x112ea}, // Khudawadi Vowel Sign U ..Khudawadi Sign Virama
|
||||
{0x11300, 0x11301}, // Grantha Sign Combining A..Grantha Sign Candrabindu
|
||||
{0x1133c, 0x1133c}, // Grantha Sign Nukta ..Grantha Sign Nukta
|
||||
{0x11340, 0x11340}, // Grantha Vowel Sign Ii ..Grantha Vowel Sign Ii
|
||||
{0x11366, 0x1136c}, // Combining Grantha Digit ..Combining Grantha Digit
|
||||
{0x11370, 0x11374}, // Combining Grantha Letter..Combining Grantha Letter
|
||||
{0x11438, 0x1143f}, // (nil) ..
|
||||
{0x11442, 0x11444}, // (nil) ..
|
||||
{0x11446, 0x11446}, // (nil) ..
|
||||
{0x114b3, 0x114b8}, // Tirhuta Vowel Sign U ..Tirhuta Vowel Sign Vocal
|
||||
{0x114ba, 0x114ba}, // Tirhuta Vowel Sign Sh||t..Tirhuta Vowel Sign Sh||t
|
||||
{0x114bf, 0x114c0}, // Tirhuta Sign Candrabindu..Tirhuta Sign Anusvara
|
||||
{0x114c2, 0x114c3}, // Tirhuta Sign Virama ..Tirhuta Sign Nukta
|
||||
{0x115b2, 0x115b5}, // Siddham Vowel Sign U ..Siddham Vowel Sign Vocal
|
||||
{0x115bc, 0x115bd}, // Siddham Sign Candrabindu..Siddham Sign Anusvara
|
||||
{0x115bf, 0x115c0}, // Siddham Sign Virama ..Siddham Sign Nukta
|
||||
{0x115dc, 0x115dd}, // Siddham Vowel Sign Alter..Siddham Vowel Sign Alter
|
||||
{0x11633, 0x1163a}, // Modi Vowel Sign U ..Modi Vowel Sign Ai
|
||||
{0x1163d, 0x1163d}, // Modi Sign Anusvara ..Modi Sign Anusvara
|
||||
{0x1163f, 0x11640}, // Modi Sign Virama ..Modi Sign Ardhacandra
|
||||
{0x116ab, 0x116ab}, // Takri Sign Anusvara ..Takri Sign Anusvara
|
||||
{0x116ad, 0x116ad}, // Takri Vowel Sign Aa ..Takri Vowel Sign Aa
|
||||
{0x116b0, 0x116b5}, // Takri Vowel Sign U ..Takri Vowel Sign Au
|
||||
{0x116b7, 0x116b7}, // Takri Sign Nukta ..Takri Sign Nukta
|
||||
{0x1171d, 0x1171f}, // Ahom Consonant Sign Medi..Ahom Consonant Sign Medi
|
||||
{0x11722, 0x11725}, // Ahom Vowel Sign I ..Ahom Vowel Sign Uu
|
||||
{0x11727, 0x1172b}, // Ahom Vowel Sign Aw ..Ahom Sign Killer
|
||||
{0x11c30, 0x11c36}, // (nil) ..
|
||||
{0x11c38, 0x11c3d}, // (nil) ..
|
||||
{0x11c3f, 0x11c3f}, // (nil) ..
|
||||
{0x11c92, 0x11ca7}, // (nil) ..
|
||||
{0x11caa, 0x11cb0}, // (nil) ..
|
||||
{0x11cb2, 0x11cb3}, // (nil) ..
|
||||
{0x11cb5, 0x11cb6}, // (nil) ..
|
||||
{0x16af0, 0x16af4}, // Bassa Vah Combining High..Bassa Vah Combining High
|
||||
{0x16b30, 0x16b36}, // Pahawh Hmong Mark Cim Tu..Pahawh Hmong Mark Cim Ta
|
||||
{0x16f8f, 0x16f92}, // Miao Tone Right ..Miao Tone Below
|
||||
{0x1bc9d, 0x1bc9e}, // Duployan Thick Letter Se..Duployan Double Mark
|
||||
{0x1d167, 0x1d169}, // Musical Symbol Combining..Musical Symbol Combining
|
||||
{0x1d17b, 0x1d182}, // Musical Symbol Combining..Musical Symbol Combining
|
||||
{0x1d185, 0x1d18b}, // Musical Symbol Combining..Musical Symbol Combining
|
||||
{0x1d1aa, 0x1d1ad}, // Musical Symbol Combining..Musical Symbol Combining
|
||||
{0x1d242, 0x1d244}, // Combining Greek Musical ..Combining Greek Musical
|
||||
{0x1da00, 0x1da36}, // Signwriting Head Rim ..Signwriting Air Sucking
|
||||
{0x1da3b, 0x1da6c}, // Signwriting Mouth Closed..Signwriting Excitement
|
||||
{0x1da75, 0x1da75}, // Signwriting Upper Body T..Signwriting Upper Body T
|
||||
{0x1da84, 0x1da84}, // Signwriting Location Hea..Signwriting Location Hea
|
||||
{0x1da9b, 0x1da9f}, // Signwriting Fill Modifie..Signwriting Fill Modifie
|
||||
{0x1daa1, 0x1daaf}, // Signwriting Rotation Mod..Signwriting Rotation Mod
|
||||
{0x1e000, 0x1e006}, // (nil) ..
|
||||
{0x1e008, 0x1e018}, // (nil) ..
|
||||
{0x1e01b, 0x1e021}, // (nil) ..
|
||||
{0x1e023, 0x1e024}, // (nil) ..
|
||||
{0x1e026, 0x1e02a}, // (nil) ..
|
||||
{0x1e8d0, 0x1e8d6}, // Mende Kikakui Combining ..Mende Kikakui Combining
|
||||
{0x1e944, 0x1e94a}, // (nil) ..
|
||||
{0xe0100, 0xe01ef}, // Variation Select||-17 ..Variation Select||-256
|
||||
};
|
||||
|
||||
// https://github.com/jquast/wcwidth/blob/master/wcwidth/table_wide.py
|
||||
// at commit 0d7de112202cc8b2ebe9232ff4a5c954f19d561a (2016-07-02):
|
||||
private static final int[][] WIDE_EASTASIAN = {
|
||||
{0x1100, 0x115f}, // Hangul Choseong Kiyeok ..Hangul Choseong Filler
|
||||
{0x231a, 0x231b}, // Watch ..Hourglass
|
||||
{0x2329, 0x232a}, // Left-pointing Angle Brac..Right-pointing Angle Bra
|
||||
{0x23e9, 0x23ec}, // Black Right-pointing Dou..Black Down-pointing Doub
|
||||
{0x23f0, 0x23f0}, // Alarm Clock ..Alarm Clock
|
||||
{0x23f3, 0x23f3}, // Hourglass With Flowing S..Hourglass With Flowing S
|
||||
{0x25fd, 0x25fe}, // White Medium Small Squar..Black Medium Small Squar
|
||||
{0x2614, 0x2615}, // Umbrella With Rain Drops..Hot Beverage
|
||||
{0x2648, 0x2653}, // Aries ..Pisces
|
||||
{0x267f, 0x267f}, // Wheelchair Symbol ..Wheelchair Symbol
|
||||
{0x2693, 0x2693}, // Anch|| ..Anch||
|
||||
{0x26a1, 0x26a1}, // High Voltage Sign ..High Voltage Sign
|
||||
{0x26aa, 0x26ab}, // Medium White Circle ..Medium Black Circle
|
||||
{0x26bd, 0x26be}, // Soccer Ball ..Baseball
|
||||
{0x26c4, 0x26c5}, // Snowman Without Snow ..Sun Behind Cloud
|
||||
{0x26ce, 0x26ce}, // Ophiuchus ..Ophiuchus
|
||||
{0x26d4, 0x26d4}, // No Entry ..No Entry
|
||||
{0x26ea, 0x26ea}, // Church ..Church
|
||||
{0x26f2, 0x26f3}, // Fountain ..Flag In Hole
|
||||
{0x26f5, 0x26f5}, // Sailboat ..Sailboat
|
||||
{0x26fa, 0x26fa}, // Tent ..Tent
|
||||
{0x26fd, 0x26fd}, // Fuel Pump ..Fuel Pump
|
||||
{0x2705, 0x2705}, // White Heavy Check Mark ..White Heavy Check Mark
|
||||
{0x270a, 0x270b}, // Raised Fist ..Raised Hand
|
||||
{0x2728, 0x2728}, // Sparkles ..Sparkles
|
||||
{0x274c, 0x274c}, // Cross Mark ..Cross Mark
|
||||
{0x274e, 0x274e}, // Negative Squared Cross M..Negative Squared Cross M
|
||||
{0x2753, 0x2755}, // Black Question Mark ||na..White Exclamation Mark O
|
||||
{0x2757, 0x2757}, // Heavy Exclamation Mark S..Heavy Exclamation Mark S
|
||||
{0x2795, 0x2797}, // Heavy Plus Sign ..Heavy Division Sign
|
||||
{0x27b0, 0x27b0}, // Curly Loop ..Curly Loop
|
||||
{0x27bf, 0x27bf}, // Double Curly Loop ..Double Curly Loop
|
||||
{0x2b1b, 0x2b1c}, // Black Large Square ..White Large Square
|
||||
{0x2b50, 0x2b50}, // White Medium Star ..White Medium Star
|
||||
{0x2b55, 0x2b55}, // Heavy Large Circle ..Heavy Large Circle
|
||||
{0x2e80, 0x2e99}, // Cjk Radical Repeat ..Cjk Radical Rap
|
||||
{0x2e9b, 0x2ef3}, // Cjk Radical Choke ..Cjk Radical C-simplified
|
||||
{0x2f00, 0x2fd5}, // Kangxi Radical One ..Kangxi Radical Flute
|
||||
{0x2ff0, 0x2ffb}, // Ideographic Description ..Ideographic Description
|
||||
{0x3000, 0x303e}, // Ideographic Space ..Ideographic Variation In
|
||||
{0x3041, 0x3096}, // Hiragana Letter Small A ..Hiragana Letter Small Ke
|
||||
{0x3099, 0x30ff}, // Combining Katakana-hirag..Katakana Digraph Koto
|
||||
{0x3105, 0x312d}, // Bopomofo Letter B ..Bopomofo Letter Ih
|
||||
{0x3131, 0x318e}, // Hangul Letter Kiyeok ..Hangul Letter Araeae
|
||||
{0x3190, 0x31ba}, // Ideographic Annotation L..Bopomofo Letter Zy
|
||||
{0x31c0, 0x31e3}, // Cjk Stroke T ..Cjk Stroke Q
|
||||
{0x31f0, 0x321e}, // Katakana Letter Small Ku..Parenthesized K||ean Cha
|
||||
{0x3220, 0x3247}, // Parenthesized Ideograph ..Circled Ideograph Koto
|
||||
{0x3250, 0x32fe}, // Partnership Sign ..Circled Katakana Wo
|
||||
{0x3300, 0x4dbf}, // Square Apaato ..
|
||||
{0x4e00, 0xa48c}, // Cjk Unified Ideograph-4e..Yi Syllable Yyr
|
||||
{0xa490, 0xa4c6}, // Yi Radical Qot ..Yi Radical Ke
|
||||
{0xa960, 0xa97c}, // Hangul Choseong Tikeut-m..Hangul Choseong Ssangyeo
|
||||
{0xac00, 0xd7a3}, // Hangul Syllable Ga ..Hangul Syllable Hih
|
||||
{0xf900, 0xfaff}, // Cjk Compatibility Ideogr..
|
||||
{0xfe10, 0xfe19}, // Presentation F||m F|| Ve..Presentation F||m F|| Ve
|
||||
{0xfe30, 0xfe52}, // Presentation F||m F|| Ve..Small Full Stop
|
||||
{0xfe54, 0xfe66}, // Small Semicolon ..Small Equals Sign
|
||||
{0xfe68, 0xfe6b}, // Small Reverse Solidus ..Small Commercial At
|
||||
{0xff01, 0xff60}, // Fullwidth Exclamation Ma..Fullwidth Right White Pa
|
||||
{0xffe0, 0xffe6}, // Fullwidth Cent Sign ..Fullwidth Won Sign
|
||||
{0x16fe0, 0x16fe0}, // (nil) ..
|
||||
{0x17000, 0x187ec}, // (nil) ..
|
||||
{0x18800, 0x18af2}, // (nil) ..
|
||||
{0x1b000, 0x1b001}, // Katakana Letter Archaic ..Hiragana Letter Archaic
|
||||
{0x1f004, 0x1f004}, // Mahjong Tile Red Dragon ..Mahjong Tile Red Dragon
|
||||
{0x1f0cf, 0x1f0cf}, // Playing Card Black Joker..Playing Card Black Joker
|
||||
{0x1f18e, 0x1f18e}, // Negative Squared Ab ..Negative Squared Ab
|
||||
{0x1f191, 0x1f19a}, // Squared Cl ..Squared Vs
|
||||
{0x1f200, 0x1f202}, // Square Hiragana Hoka ..Squared Katakana Sa
|
||||
{0x1f210, 0x1f23b}, // Squared Cjk Unified Ideo..
|
||||
{0x1f240, 0x1f248}, // T||toise Shell Bracketed..T||toise Shell Bracketed
|
||||
{0x1f250, 0x1f251}, // Circled Ideograph Advant..Circled Ideograph Accept
|
||||
{0x1f300, 0x1f320}, // Cyclone ..Shooting Star
|
||||
{0x1f32d, 0x1f335}, // Hot Dog ..Cactus
|
||||
{0x1f337, 0x1f37c}, // Tulip ..Baby Bottle
|
||||
{0x1f37e, 0x1f393}, // Bottle With Popping C||k..Graduation Cap
|
||||
{0x1f3a0, 0x1f3ca}, // Carousel H||se ..Swimmer
|
||||
{0x1f3cf, 0x1f3d3}, // Cricket Bat And Ball ..Table Tennis Paddle And
|
||||
{0x1f3e0, 0x1f3f0}, // House Building ..European Castle
|
||||
{0x1f3f4, 0x1f3f4}, // Waving Black Flag ..Waving Black Flag
|
||||
{0x1f3f8, 0x1f43e}, // Badminton Racquet And Sh..Paw Prints
|
||||
{0x1f440, 0x1f440}, // Eyes ..Eyes
|
||||
{0x1f442, 0x1f4fc}, // Ear ..Videocassette
|
||||
{0x1f4ff, 0x1f53d}, // Prayer Beads ..Down-pointing Small Red
|
||||
{0x1f54b, 0x1f54e}, // Kaaba ..Men||ah With Nine Branch
|
||||
{0x1f550, 0x1f567}, // Clock Face One Oclock ..Clock Face Twelve-thirty
|
||||
{0x1f57a, 0x1f57a}, // (nil) ..
|
||||
{0x1f595, 0x1f596}, // Reversed Hand With Middl..Raised Hand With Part Be
|
||||
{0x1f5a4, 0x1f5a4}, // (nil) ..
|
||||
{0x1f5fb, 0x1f64f}, // Mount Fuji ..Person With Folded Hands
|
||||
{0x1f680, 0x1f6c5}, // Rocket ..Left Luggage
|
||||
{0x1f6cc, 0x1f6cc}, // Sleeping Accommodation ..Sleeping Accommodation
|
||||
{0x1f6d0, 0x1f6d2}, // Place Of W||ship ..
|
||||
{0x1f6eb, 0x1f6ec}, // Airplane Departure ..Airplane Arriving
|
||||
{0x1f6f4, 0x1f6f6}, // (nil) ..
|
||||
{0x1f910, 0x1f91e}, // Zipper-mouth Face ..
|
||||
{0x1f920, 0x1f927}, // (nil) ..
|
||||
{0x1f930, 0x1f930}, // (nil) ..
|
||||
{0x1f933, 0x1f93e}, // (nil) ..
|
||||
{0x1f940, 0x1f94b}, // (nil) ..
|
||||
{0x1f950, 0x1f95e}, // (nil) ..
|
||||
{0x1f980, 0x1f991}, // Crab ..
|
||||
{0x1f9c0, 0x1f9c0}, // Cheese Wedge ..Cheese Wedge
|
||||
{0x20000, 0x2fffd}, // Cjk Unified Ideograph-20..
|
||||
{0x30000, 0x3fffd}, // (nil) ..
|
||||
};
|
||||
|
||||
|
||||
private static boolean intable(int[][] table, int c) {
|
||||
// First quick check f|| Latin1 etc. characters.
|
||||
if (c < table[0][0]) return false;
|
||||
|
||||
// Binary search in table.
|
||||
int bot = 0;
|
||||
int top = table.length - 1; // (int)(size / sizeof(struct interval) - 1);
|
||||
while (top >= bot) {
|
||||
int mid = (bot + top) / 2;
|
||||
if (table[mid][1] < c) {
|
||||
bot = mid + 1;
|
||||
} else if (table[mid][0] > c) {
|
||||
top = mid - 1;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Return the terminal display width of a code point: 0, 1 || 2. */
|
||||
public static int width(int ucs) {
|
||||
if (ucs == 0 ||
|
||||
ucs == 0x034F ||
|
||||
(0x200B <= ucs && ucs <= 0x200F) ||
|
||||
ucs == 0x2028 ||
|
||||
ucs == 0x2029 ||
|
||||
(0x202A <= ucs && ucs <= 0x202E) ||
|
||||
(0x2060 <= ucs && ucs <= 0x2063)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// C0/C1 control characters
|
||||
// Termux change: Return 0 instead of -1.
|
||||
if (ucs < 32 || (0x07F <= ucs && ucs < 0x0A0)) return 0;
|
||||
|
||||
// combining characters with zero width
|
||||
if (intable(ZERO_WIDTH, ucs)) return 0;
|
||||
|
||||
return intable(WIDE_EASTASIAN, ucs) ? 2 : 1;
|
||||
}
|
||||
|
||||
/** The width at an index position in a java char array. */
|
||||
public static int width(char[] chars, int index) {
|
||||
char c = chars[index];
|
||||
return Character.isHighSurrogate(c) ? width(Character.toCodePoint(c, chars[index + 1])) : width(c);
|
||||
}
|
||||
|
||||
}
|
5
terminal-emulator/src/main/jni/Android.mk
Normal file
5
terminal-emulator/src/main/jni/Android.mk
Normal file
@@ -0,0 +1,5 @@
|
||||
LOCAL_PATH:= $(call my-dir)
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE:= libtermux
|
||||
LOCAL_SRC_FILES:= termux.c
|
||||
include $(BUILD_SHARED_LIBRARY)
|
214
terminal-emulator/src/main/jni/termux.c
Normal file
214
terminal-emulator/src/main/jni/termux.c
Normal file
@@ -0,0 +1,214 @@
|
||||
#include <dirent.h>
|
||||
#include <fcntl.h>
|
||||
#include <jni.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/wait.h>
|
||||
#include <termios.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define TERMUX_UNUSED(x) x __attribute__((__unused__))
|
||||
#ifdef __APPLE__
|
||||
# define LACKS_PTSNAME_R
|
||||
#endif
|
||||
|
||||
static int throw_runtime_exception(JNIEnv* env, char const* message)
|
||||
{
|
||||
jclass exClass = (*env)->FindClass(env, "java/lang/RuntimeException");
|
||||
(*env)->ThrowNew(env, exClass, message);
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int create_subprocess(JNIEnv* env,
|
||||
char const* cmd,
|
||||
char const* cwd,
|
||||
char* const argv[],
|
||||
char** envp,
|
||||
int* pProcessId,
|
||||
jint rows,
|
||||
jint columns)
|
||||
{
|
||||
int ptm = open("/dev/ptmx", O_RDWR | O_CLOEXEC);
|
||||
if (ptm < 0) return throw_runtime_exception(env, "Cannot open /dev/ptmx");
|
||||
|
||||
#ifdef LACKS_PTSNAME_R
|
||||
char* devname;
|
||||
#else
|
||||
char devname[64];
|
||||
#endif
|
||||
if (grantpt(ptm) || unlockpt(ptm) ||
|
||||
#ifdef LACKS_PTSNAME_R
|
||||
(devname = ptsname(ptm)) == NULL
|
||||
#else
|
||||
ptsname_r(ptm, devname, sizeof(devname))
|
||||
#endif
|
||||
) {
|
||||
return throw_runtime_exception(env, "Cannot grantpt()/unlockpt()/ptsname_r() on /dev/ptmx");
|
||||
}
|
||||
|
||||
// Enable UTF-8 mode and disable flow control to prevent Ctrl+S from locking up the display.
|
||||
struct termios tios;
|
||||
tcgetattr(ptm, &tios);
|
||||
tios.c_iflag |= IUTF8;
|
||||
tios.c_iflag &= ~(IXON | IXOFF);
|
||||
tcsetattr(ptm, TCSANOW, &tios);
|
||||
|
||||
/** Set initial winsize. */
|
||||
struct winsize sz = { .ws_row = rows, .ws_col = columns };
|
||||
ioctl(ptm, TIOCSWINSZ, &sz);
|
||||
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
return throw_runtime_exception(env, "Fork failed");
|
||||
} else if (pid > 0) {
|
||||
*pProcessId = (int) pid;
|
||||
return ptm;
|
||||
} else {
|
||||
// Clear signals which the Android java process may have blocked:
|
||||
sigset_t signals_to_unblock;
|
||||
sigfillset(&signals_to_unblock);
|
||||
sigprocmask(SIG_UNBLOCK, &signals_to_unblock, 0);
|
||||
|
||||
close(ptm);
|
||||
setsid();
|
||||
|
||||
int pts = open(devname, O_RDWR);
|
||||
if (pts < 0) exit(-1);
|
||||
|
||||
dup2(pts, 0);
|
||||
dup2(pts, 1);
|
||||
dup2(pts, 2);
|
||||
|
||||
DIR* self_dir = opendir("/proc/self/fd");
|
||||
if (self_dir != NULL) {
|
||||
int self_dir_fd = dirfd(self_dir);
|
||||
struct dirent* entry;
|
||||
while ((entry = readdir(self_dir)) != NULL) {
|
||||
int fd = atoi(entry->d_name);
|
||||
if(fd > 2 && fd != self_dir_fd) close(fd);
|
||||
}
|
||||
closedir(self_dir);
|
||||
}
|
||||
|
||||
clearenv();
|
||||
if (envp) for (; *envp; ++envp) putenv(*envp);
|
||||
|
||||
if (chdir(cwd) != 0) {
|
||||
char* error_message;
|
||||
// No need to free asprintf()-allocated memory since doing execvp() or exit() below.
|
||||
if (asprintf(&error_message, "chdir(\"%s\")", cwd) == -1) error_message = "chdir()";
|
||||
perror(error_message);
|
||||
fflush(stderr);
|
||||
}
|
||||
execvp(cmd, argv);
|
||||
// Show terminal output about failing exec() call:
|
||||
char* error_message;
|
||||
if (asprintf(&error_message, "exec(\"%s\")", cmd) == -1) error_message = "exec()";
|
||||
perror(error_message);
|
||||
_exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL Java_com_termux_terminal_JNI_createSubprocess(
|
||||
JNIEnv* env,
|
||||
jclass TERMUX_UNUSED(clazz),
|
||||
jstring cmd,
|
||||
jstring cwd,
|
||||
jobjectArray args,
|
||||
jobjectArray envVars,
|
||||
jintArray processIdArray,
|
||||
jint rows,
|
||||
jint columns)
|
||||
{
|
||||
jsize size = args ? (*env)->GetArrayLength(env, args) : 0;
|
||||
char** argv = NULL;
|
||||
if (size > 0) {
|
||||
argv = (char**) malloc((size + 1) * sizeof(char*));
|
||||
if (!argv) return throw_runtime_exception(env, "Couldn't allocate argv array");
|
||||
for (int i = 0; i < size; ++i) {
|
||||
jstring arg_java_string = (jstring) (*env)->GetObjectArrayElement(env, args, i);
|
||||
char const* arg_utf8 = (*env)->GetStringUTFChars(env, arg_java_string, NULL);
|
||||
if (!arg_utf8) return throw_runtime_exception(env, "GetStringUTFChars() failed for argv");
|
||||
argv[i] = strdup(arg_utf8);
|
||||
(*env)->ReleaseStringUTFChars(env, arg_java_string, arg_utf8);
|
||||
}
|
||||
argv[size] = NULL;
|
||||
}
|
||||
|
||||
size = envVars ? (*env)->GetArrayLength(env, envVars) : 0;
|
||||
char** envp = NULL;
|
||||
if (size > 0) {
|
||||
envp = (char**) malloc((size + 1) * sizeof(char *));
|
||||
if (!envp) return throw_runtime_exception(env, "malloc() for envp array failed");
|
||||
for (int i = 0; i < size; ++i) {
|
||||
jstring env_java_string = (jstring) (*env)->GetObjectArrayElement(env, envVars, i);
|
||||
char const* env_utf8 = (*env)->GetStringUTFChars(env, env_java_string, 0);
|
||||
if (!env_utf8) return throw_runtime_exception(env, "GetStringUTFChars() failed for env");
|
||||
envp[i] = strdup(env_utf8);
|
||||
(*env)->ReleaseStringUTFChars(env, env_java_string, env_utf8);
|
||||
}
|
||||
envp[size] = NULL;
|
||||
}
|
||||
|
||||
int procId = 0;
|
||||
char const* cmd_cwd = (*env)->GetStringUTFChars(env, cwd, NULL);
|
||||
char const* cmd_utf8 = (*env)->GetStringUTFChars(env, cmd, NULL);
|
||||
int ptm = create_subprocess(env, cmd_utf8, cmd_cwd, argv, envp, &procId, rows, columns);
|
||||
(*env)->ReleaseStringUTFChars(env, cmd, cmd_utf8);
|
||||
(*env)->ReleaseStringUTFChars(env, cmd, cmd_cwd);
|
||||
|
||||
if (argv) {
|
||||
for (char** tmp = argv; *tmp; ++tmp) free(*tmp);
|
||||
free(argv);
|
||||
}
|
||||
if (envp) {
|
||||
for (char** tmp = envp; *tmp; ++tmp) free(*tmp);
|
||||
free(envp);
|
||||
}
|
||||
|
||||
int* pProcId = (int*) (*env)->GetPrimitiveArrayCritical(env, processIdArray, NULL);
|
||||
if (!pProcId) return throw_runtime_exception(env, "JNI call GetPrimitiveArrayCritical(processIdArray, &isCopy) failed");
|
||||
|
||||
*pProcId = procId;
|
||||
(*env)->ReleasePrimitiveArrayCritical(env, processIdArray, pProcId, 0);
|
||||
|
||||
return ptm;
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_com_termux_terminal_JNI_setPtyWindowSize(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint fd, jint rows, jint cols)
|
||||
{
|
||||
struct winsize sz = { .ws_row = rows, .ws_col = cols };
|
||||
ioctl(fd, TIOCSWINSZ, &sz);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_com_termux_terminal_JNI_setPtyUTF8Mode(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint fd)
|
||||
{
|
||||
struct termios tios;
|
||||
tcgetattr(fd, &tios);
|
||||
if ((tios.c_iflag & IUTF8) == 0) {
|
||||
tios.c_iflag |= IUTF8;
|
||||
tcsetattr(fd, TCSANOW, &tios);
|
||||
}
|
||||
}
|
||||
|
||||
JNIEXPORT int JNICALL Java_com_termux_terminal_JNI_waitFor(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint pid)
|
||||
{
|
||||
int status;
|
||||
waitpid(pid, &status, 0);
|
||||
if (WIFEXITED(status)) {
|
||||
return WEXITSTATUS(status);
|
||||
} else if (WIFSIGNALED(status)) {
|
||||
return -WTERMSIG(status);
|
||||
} else {
|
||||
// Should never happen - waitpid(2) says "One of the first three macros will evaluate to a non-zero (true) value".
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_com_termux_terminal_JNI_close(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint fileDescriptor)
|
||||
{
|
||||
close(fileDescriptor);
|
||||
}
|
@@ -0,0 +1,54 @@
|
||||
package com.termux.terminal;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
public class ByteQueueTest extends TestCase {
|
||||
|
||||
private static void assertArrayEquals(byte[] expected, byte[] actual) {
|
||||
if (expected.length != actual.length) {
|
||||
fail("Difference array length");
|
||||
}
|
||||
for (int i = 0; i < expected.length; i++) {
|
||||
if (expected[i] != actual[i]) {
|
||||
fail("Inequals at index=" + i + ", expected=" + (int) expected[i] + ", actual=" + (int) actual[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void testCompleteWrites() throws Exception {
|
||||
ByteQueue q = new ByteQueue(10);
|
||||
assertEquals(true, q.write(new byte[]{1, 2, 3}, 0, 3));
|
||||
|
||||
byte[] arr = new byte[10];
|
||||
assertEquals(3, q.read(arr, true));
|
||||
assertArrayEquals(new byte[]{1, 2, 3}, new byte[]{arr[0], arr[1], arr[2]});
|
||||
|
||||
assertEquals(true, q.write(new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, 0, 10));
|
||||
assertEquals(10, q.read(arr, true));
|
||||
assertArrayEquals(new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, arr);
|
||||
}
|
||||
|
||||
public void testQueueWraparound() throws Exception {
|
||||
ByteQueue q = new ByteQueue(10);
|
||||
|
||||
byte[] origArray = new byte[]{1, 2, 3, 4, 5, 6};
|
||||
byte[] readArray = new byte[origArray.length];
|
||||
for (int i = 0; i < 20; i++) {
|
||||
q.write(origArray, 0, origArray.length);
|
||||
assertEquals(origArray.length, q.read(readArray, true));
|
||||
assertArrayEquals(origArray, readArray);
|
||||
}
|
||||
}
|
||||
|
||||
public void testWriteNotesClosing() throws Exception {
|
||||
ByteQueue q = new ByteQueue(10);
|
||||
q.close();
|
||||
assertEquals(false, q.write(new byte[]{1, 2, 3}, 0, 3));
|
||||
}
|
||||
|
||||
public void testReadNonBlocking() throws Exception {
|
||||
ByteQueue q = new ByteQueue(10);
|
||||
assertEquals(0, q.read(new byte[128], false));
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,32 @@
|
||||
package com.termux.terminal;
|
||||
|
||||
/** "\033[" is the Control Sequence Introducer char sequence (CSI). */
|
||||
public class ControlSequenceIntroducerTest extends TerminalTestCase {
|
||||
|
||||
/** CSI Ps P Scroll down Ps lines (default = 1) (SD). */
|
||||
public void testCsiT() {
|
||||
withTerminalSized(4, 6).enterString("1\r\n2\r\n3\r\nhi\033[2Tyo\r\nA\r\nB").assertLinesAre(" ", " ", "1 ", "2 yo", "A ",
|
||||
"Bi ");
|
||||
// Default value (1):
|
||||
withTerminalSized(4, 6).enterString("1\r\n2\r\n3\r\nhi\033[Tyo\r\nA\r\nB").assertLinesAre(" ", "1 ", "2 ", "3 yo", "Ai ",
|
||||
"B ");
|
||||
}
|
||||
|
||||
/** CSI Ps S Scroll up Ps lines (default = 1) (SU). */
|
||||
public void testCsiS() {
|
||||
// The behaviour here is a bit inconsistent between terminals - this is how the OS X Terminal.app does it:
|
||||
withTerminalSized(3, 4).enterString("1\r\n2\r\n3\r\nhi\033[2Sy").assertLinesAre("3 ", "hi ", " ", " y");
|
||||
// Default value (1):
|
||||
withTerminalSized(3, 4).enterString("1\r\n2\r\n3\r\nhi\033[Sy").assertLinesAre("2 ", "3 ", "hi ", " y");
|
||||
}
|
||||
|
||||
/** CSI Ps X Erase Ps Character(s) (default = 1) (ECH). */
|
||||
public void testCsiX() {
|
||||
// See https://code.google.com/p/chromium/issues/detail?id=212712 where test was extraced from.
|
||||
withTerminalSized(13, 2).enterString("abcdefghijkl\b\b\b\b\b\033[X").assertLinesAre("abcdefg ijkl ", " ");
|
||||
withTerminalSized(13, 2).enterString("abcdefghijkl\b\b\b\b\b\033[1X").assertLinesAre("abcdefg ijkl ", " ");
|
||||
withTerminalSized(13, 2).enterString("abcdefghijkl\b\b\b\b\b\033[2X").assertLinesAre("abcdefg jkl ", " ");
|
||||
withTerminalSized(13, 2).enterString("abcdefghijkl\b\b\b\b\b\033[20X").assertLinesAre("abcdefg ", " ");
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,230 @@
|
||||
package com.termux.terminal;
|
||||
|
||||
import junit.framework.Assert;
|
||||
|
||||
public class CursorAndScreenTest extends TerminalTestCase {
|
||||
|
||||
public void testDeleteLinesKeepsStyles() {
|
||||
int cols = 5, rows = 5;
|
||||
withTerminalSized(cols, rows);
|
||||
for (int row = 0; row < 5; row++) {
|
||||
for (int col = 0; col < 5; col++) {
|
||||
// Foreground color to col, background to row:
|
||||
enterString("\033[38;5;" + col + "m");
|
||||
enterString("\033[48;5;" + row + "m");
|
||||
enterString(Character.toString((char) ('A' + col + row * 5)));
|
||||
}
|
||||
}
|
||||
assertLinesAre("ABCDE", "FGHIJ", "KLMNO", "PQRST", "UVWXY");
|
||||
for (int row = 0; row < 5; row++) {
|
||||
for (int col = 0; col < 5; col++) {
|
||||
long s = getStyleAt(row, col);
|
||||
Assert.assertEquals(col, TextStyle.decodeForeColor(s));
|
||||
Assert.assertEquals(row, TextStyle.decodeBackColor(s));
|
||||
}
|
||||
}
|
||||
// "${CSI}H" - place cursor at 1,1, then "${CSI}2M" to delete two lines.
|
||||
enterString("\033[H\033[2M");
|
||||
assertLinesAre("KLMNO", "PQRST", "UVWXY", " ", " ");
|
||||
for (int row = 0; row < 3; row++) {
|
||||
for (int col = 0; col < 5; col++) {
|
||||
long s = getStyleAt(row, col);
|
||||
Assert.assertEquals(col, TextStyle.decodeForeColor(s));
|
||||
Assert.assertEquals(row + 2, TextStyle.decodeBackColor(s));
|
||||
}
|
||||
}
|
||||
// Set default fg and background for the new blank lines:
|
||||
enterString("\033[38;5;98m");
|
||||
enterString("\033[48;5;99m");
|
||||
// "${CSI}B" to go down one line, then "${CSI}2L" to insert two lines:
|
||||
enterString("\033[B\033[2L");
|
||||
assertLinesAre("KLMNO", " ", " ", "PQRST", "UVWXY");
|
||||
for (int row = 0; row < 5; row++) {
|
||||
for (int col = 0; col < 5; col++) {
|
||||
int wantedForeground = (row == 1 || row == 2) ? 98 : col;
|
||||
int wantedBackground = (row == 1 || row == 2) ? 99 : (row == 0 ? 2 : row);
|
||||
long s = getStyleAt(row, col);
|
||||
Assert.assertEquals(wantedForeground, TextStyle.decodeForeColor(s));
|
||||
Assert.assertEquals(wantedBackground, TextStyle.decodeBackColor(s));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void testDeleteCharacters() {
|
||||
withTerminalSized(5, 2).enterString("枝ce").assertLinesAre("枝ce ", " ");
|
||||
withTerminalSized(5, 2).enterString("a枝ce").assertLinesAre("a枝ce", " ");
|
||||
withTerminalSized(5, 2).enterString("nice").enterString("\033[G\033[P").assertLinesAre("ice ", " ");
|
||||
withTerminalSized(5, 2).enterString("nice").enterString("\033[G\033[2P").assertLinesAre("ce ", " ");
|
||||
withTerminalSized(5, 2).enterString("nice").enterString("\033[2G\033[2P").assertLinesAre("ne ", " ");
|
||||
// "${CSI}${n}P, the delete characters (DCH) sequence should cap characters to delete.
|
||||
withTerminalSized(5, 2).enterString("nice").enterString("\033[G\033[99P").assertLinesAre(" ", " ");
|
||||
// With combining char U+0302.
|
||||
withTerminalSized(5, 2).enterString("n\u0302ice").enterString("\033[G\033[2P").assertLinesAre("ce ", " ");
|
||||
withTerminalSized(5, 2).enterString("n\u0302ice").enterString("\033[G\033[P").assertLinesAre("ice ", " ");
|
||||
withTerminalSized(5, 2).enterString("n\u0302ice").enterString("\033[2G\033[2P").assertLinesAre("n\u0302e ", " ");
|
||||
// With wide 枝 char, checking that putting char at part replaces other with whitespace:
|
||||
withTerminalSized(5, 2).enterString("枝ce").enterString("\033[Ga").assertLinesAre("a ce ", " ");
|
||||
withTerminalSized(5, 2).enterString("枝ce").enterString("\033[2Ga").assertLinesAre(" ace ", " ");
|
||||
// With wide 枝 char, deleting either part replaces other with whitespace:
|
||||
withTerminalSized(5, 2).enterString("枝ce").enterString("\033[G\033[P").assertLinesAre(" ce ", " ");
|
||||
withTerminalSized(5, 2).enterString("枝ce").enterString("\033[2G\033[P").assertLinesAre(" ce ", " ");
|
||||
withTerminalSized(5, 2).enterString("枝ce").enterString("\033[2G\033[2P").assertLinesAre(" e ", " ");
|
||||
withTerminalSized(5, 2).enterString("枝ce").enterString("\033[G\033[2P").assertLinesAre("ce ", " ");
|
||||
withTerminalSized(5, 2).enterString("a枝ce").enterString("\033[G\033[P").assertLinesAre("枝ce ", " ");
|
||||
}
|
||||
|
||||
public void testInsertMode() {
|
||||
// "${CSI}4h" enables insert mode.
|
||||
withTerminalSized(5, 2).enterString("nice").enterString("\033[G\033[4hA").assertLinesAre("Anice", " ");
|
||||
withTerminalSized(5, 2).enterString("nice").enterString("\033[2G\033[4hA").assertLinesAre("nAice", " ");
|
||||
withTerminalSized(5, 2).enterString("nice").enterString("\033[G\033[4hABC").assertLinesAre("ABCni", " ");
|
||||
// With combining char U+0302.
|
||||
withTerminalSized(5, 2).enterString("n\u0302ice").enterString("\033[G\033[4hA").assertLinesAre("An\u0302ice", " ");
|
||||
withTerminalSized(5, 2).enterString("n\u0302ice").enterString("\033[G\033[4hAB").assertLinesAre("ABn\u0302ic", " ");
|
||||
withTerminalSized(5, 2).enterString("n\u0302ic\u0302e").enterString("\033[2G\033[4hA").assertLinesAre("n\u0302Aic\u0302e", " ");
|
||||
// ... but without insert mode, combining char should be overwritten:
|
||||
withTerminalSized(5, 2).enterString("n\u0302ice").enterString("\033[GA").assertLinesAre("Aice ", " ");
|
||||
// ... also with two combining:
|
||||
withTerminalSized(5, 2).enterString("n\u0302\u0302i\u0302ce").enterString("\033[GA").assertLinesAre("Ai\u0302ce ", " ");
|
||||
// ... and in last column:
|
||||
withTerminalSized(5, 2).enterString("n\u0302\u0302ice!\u0302").enterString("\033[5GA").assertLinesAre("n\u0302\u0302iceA", " ");
|
||||
withTerminalSized(5, 2).enterString("nic\u0302e!\u0302").enterString("\033[4G枝").assertLinesAre("nic\u0302枝", " ");
|
||||
withTerminalSized(5, 2).enterString("nic枝\u0302").enterString("\033[3GA").assertLinesAre("niA枝\u0302", " ");
|
||||
withTerminalSized(5, 2).enterString("nic枝\u0302").enterString("\033[3GA").assertLinesAre("niA枝\u0302", " ");
|
||||
// With wide 枝 char.
|
||||
withTerminalSized(5, 2).enterString("nice").enterString("\033[G\033[4h枝").assertLinesAre("枝nic", " ");
|
||||
withTerminalSized(5, 2).enterString("nice").enterString("\033[2G\033[4h枝").assertLinesAre("n枝ic", " ");
|
||||
withTerminalSized(5, 2).enterString("n枝ce").enterString("\033[G\033[4ha").assertLinesAre("an枝c", " ");
|
||||
}
|
||||
|
||||
/** HPA—Horizontal Position Absolute (http://www.vt100.net/docs/vt510-rm/HPA) */
|
||||
public void testCursorHorizontalPositionAbsolute() {
|
||||
withTerminalSized(4, 4).enterString("ABC\033[`").assertCursorAt(0, 0);
|
||||
enterString("\033[1`").assertCursorAt(0, 0).enterString("\033[2`").assertCursorAt(0, 1);
|
||||
enterString("\r\n\033[3`").assertCursorAt(1, 2).enterString("\033[22`").assertCursorAt(1, 3);
|
||||
// Enable and configure right and left margins, first without origin mode:
|
||||
enterString("\033[?69h\033[2;3s\033[`").assertCursorAt(0, 0).enterString("\033[22`").assertCursorAt(0, 3);
|
||||
// .. now with origin mode:
|
||||
enterString("\033[?6h\033[`").assertCursorAt(0, 1).enterString("\033[22`").assertCursorAt(0, 2);
|
||||
}
|
||||
|
||||
public void testCursorForward() {
|
||||
// "${CSI}${N:=1}C" moves cursor forward N columns:
|
||||
withTerminalSized(6, 2).enterString("A\033[CB\033[2CC").assertLinesAre("A B C", " ");
|
||||
// If an attempt is made to move the cursor to the right of the right margin, the cursor stops at the right margin:
|
||||
withTerminalSized(6, 2).enterString("A\033[44CB").assertLinesAre("A B", " ");
|
||||
// Enable right margin and verify that CUF ends at the set right margin:
|
||||
withTerminalSized(6, 2).enterString("\033[?69h\033[1;3s\033[44CAB").assertLinesAre(" A ", "B ");
|
||||
}
|
||||
|
||||
public void testCursorBack() {
|
||||
// "${CSI}${N:=1}D" moves cursor back N columns:
|
||||
withTerminalSized(3, 2).enterString("A\033[DB").assertLinesAre("B ", " ");
|
||||
withTerminalSized(3, 2).enterString("AB\033[2DC").assertLinesAre("CB ", " ");
|
||||
// If an attempt is made to move the cursor to the left of the left margin, the cursor stops at the left margin:
|
||||
withTerminalSized(3, 2).enterString("AB\033[44DC").assertLinesAre("CB ", " ");
|
||||
// Enable left margin and verify that CUB ends at the set left margin:
|
||||
withTerminalSized(6, 2).enterString("ABCD\033[?69h\033[2;6s\033[44DE").assertLinesAre("AECD ", " ");
|
||||
}
|
||||
|
||||
public void testCursorUp() {
|
||||
// "${CSI}${N:=1}A" moves cursor up N rows:
|
||||
withTerminalSized(3, 3).enterString("ABCDEFG\033[AH").assertLinesAre("ABC", "DHF", "G ");
|
||||
withTerminalSized(3, 3).enterString("ABCDEFG\033[2AH").assertLinesAre("AHC", "DEF", "G ");
|
||||
// If an attempt is made to move the cursor above the top margin, the cursor stops at the top margin:
|
||||
withTerminalSized(3, 3).enterString("ABCDEFG\033[44AH").assertLinesAre("AHC", "DEF", "G ");
|
||||
// Set top margin and validate that cursor does not go above it:
|
||||
withTerminalSized(3, 3).enterString("\033[2rABCDEFG\033[44AH").assertLinesAre("ABC", "DHF", "G ");
|
||||
}
|
||||
|
||||
public void testCursorDown() {
|
||||
// "${CSI}${N:=1}B" moves cursor down N rows:
|
||||
withTerminalSized(3, 3).enterString("AB\033[BC").assertLinesAre("AB ", " C", " ");
|
||||
withTerminalSized(3, 3).enterString("AB\033[2BC").assertLinesAre("AB ", " ", " C");
|
||||
// If an attempt is made to move the cursor below the bottom margin, the cursor stops at the bottom margin:
|
||||
withTerminalSized(3, 3).enterString("AB\033[44BC").assertLinesAre("AB ", " ", " C");
|
||||
// Set bottom margin and validate that cursor does not go above it:
|
||||
withTerminalSized(3, 3).enterString("\033[1;2rAB\033[44BC").assertLinesAre("AB ", " C", " ");
|
||||
}
|
||||
|
||||
public void testReportCursorPosition() {
|
||||
withTerminalSized(10, 10);
|
||||
for (int i = 0; i < 10; i++) {
|
||||
for (int j = 0; j < 10; j++) {
|
||||
enterString("\033[" + (i + 1) + ";" + (j + 1) + "H"); // CUP cursor position.
|
||||
assertCursorAt(i, j);
|
||||
// Device Status Report (DSR):
|
||||
assertEnteringStringGivesResponse("\033[6n", "\033[" + (i + 1) + ";" + (j + 1) + "R");
|
||||
// DECXCPR — Extended Cursor Position. Note that http://www.vt100.net/docs/vt510-rm/DECXCPR says
|
||||
// the response is "${CSI}${LINE};${COLUMN};${PAGE}R" while xterm (http://invisible-island.net/xterm/ctlseqs/ctlseqs.html)
|
||||
// drops the question mark. Expect xterm behaviour here.
|
||||
assertEnteringStringGivesResponse("\033[?6n", "\033[?" + (i + 1) + ";" + (j + 1) + ";1R");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* See comments on horizontal tab handling in TerminalEmulator.java.
|
||||
*
|
||||
* We do not want to color already written cells when tabbing over them.
|
||||
*/
|
||||
public void DISABLED_testHorizontalTabColorsBackground() {
|
||||
withTerminalSized(10, 3).enterString("\033[48;5;15m").enterString("\t");
|
||||
assertCursorAt(0, 8);
|
||||
for (int i = 0; i < 10; i++) {
|
||||
int expectedColor = i < 8 ? 15 : TextStyle.COLOR_INDEX_BACKGROUND;
|
||||
assertEquals(expectedColor, TextStyle.decodeBackColor(getStyleAt(0, i)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test interactions between the cursor overflow bit and various escape sequences.
|
||||
* <p/>
|
||||
* Adapted from hterm:
|
||||
* https://chromium.googlesource.com/chromiumos/platform/assets/+/2337afa5c063127d5ce40ec7fec9b602d096df86%5E%21/#F2
|
||||
*/
|
||||
public void testClearingOfAutowrap() {
|
||||
// Fill a row with the last hyphen wrong, then run a command that
|
||||
// modifies the screen, then add a hyphen. The wrap bit should be
|
||||
// cleared, so the extra hyphen can fix the row.
|
||||
withTerminalSized(15, 6);
|
||||
|
||||
enterString("----- 1 ----X");
|
||||
enterString("\033[K-"); // EL
|
||||
|
||||
enterString("----- 2 ----X");
|
||||
enterString("\033[J-"); // ED
|
||||
|
||||
enterString("----- 3 ----X");
|
||||
enterString("\033[@-"); // ICH
|
||||
|
||||
enterString("----- 4 ----X");
|
||||
enterString("\033[P-"); // DCH
|
||||
|
||||
enterString("----- 5 ----X");
|
||||
enterString("\033[X-"); // ECH
|
||||
|
||||
// DL will delete the entire line but clear the wrap bit, so we
|
||||
// expect a hyphen at the end and nothing else.
|
||||
enterString("XXXXXXXXXXXXXXX");
|
||||
enterString("\033[M-"); // DL
|
||||
|
||||
assertLinesAre(
|
||||
"----- 1 -----",
|
||||
"----- 2 -----",
|
||||
"----- 3 -----",
|
||||
"----- 4 -----",
|
||||
"----- 5 -----",
|
||||
" -");
|
||||
}
|
||||
|
||||
public void testBackspaceAcrossWrappedLines() {
|
||||
// Backspace should not go to previous line if not auto-wrapped:
|
||||
withTerminalSized(3, 3).enterString("hi\r\n\b\byou").assertLinesAre("hi ", "you", " ");
|
||||
// Backspace should go to previous line if auto-wrapped:
|
||||
withTerminalSized(3, 3).enterString("hi y").assertLinesAre("hi ", "y ", " ").enterString("\b\b#").assertLinesAre("hi#", "y ", " ");
|
||||
// Initial backspace should do nothing:
|
||||
withTerminalSized(3, 3).enterString("\b\b\b\bhi").assertLinesAre("hi ", " ", " ");
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,78 @@
|
||||
package com.termux.terminal;
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* "CSI ? Pm h", DEC Private Mode Set (DECSET)
|
||||
* </pre>
|
||||
* <p/>
|
||||
* and
|
||||
* <p/>
|
||||
* <pre>
|
||||
* "CSI ? Pm l", DEC Private Mode Reset (DECRST)
|
||||
* </pre>
|
||||
* <p/>
|
||||
* controls various aspects of the terminal
|
||||
*/
|
||||
public class DecSetTest extends TerminalTestCase {
|
||||
|
||||
/** DECSET 25, DECTCEM, controls visibility of the cursor. */
|
||||
public void testShowHideCursor() {
|
||||
withTerminalSized(3, 3);
|
||||
assertTrue("Initially the cursor should be visible", mTerminal.isShowingCursor());
|
||||
enterString("\033[?25l"); // Hide Cursor (DECTCEM).
|
||||
assertFalse(mTerminal.isShowingCursor());
|
||||
enterString("\033[?25h"); // Show Cursor (DECTCEM).
|
||||
assertTrue(mTerminal.isShowingCursor());
|
||||
|
||||
enterString("\033[?25l"); // Hide Cursor (DECTCEM), again.
|
||||
assertFalse(mTerminal.isShowingCursor());
|
||||
mTerminal.reset();
|
||||
assertTrue("Resetting the terminal should show the cursor", mTerminal.isShowingCursor());
|
||||
|
||||
enterString("\033[?25l");
|
||||
assertFalse(mTerminal.isShowingCursor());
|
||||
enterString("\033c"); // RIS resetting should reveal cursor.
|
||||
assertTrue(mTerminal.isShowingCursor());
|
||||
}
|
||||
|
||||
/** DECSET 2004, controls bracketed paste mode. */
|
||||
public void testBracketedPasteMode() {
|
||||
withTerminalSized(3, 3);
|
||||
|
||||
mTerminal.paste("a");
|
||||
assertEquals("Pasting 'a' should output 'a' when bracketed paste mode is disabled", "a", mOutput.getOutputAndClear());
|
||||
|
||||
enterString("\033[?2004h"); // Enable bracketed paste mode.
|
||||
mTerminal.paste("a");
|
||||
assertEquals("Pasting when in bracketed paste mode should be bracketed", "\033[200~a\033[201~", mOutput.getOutputAndClear());
|
||||
|
||||
enterString("\033[?2004l"); // Disable bracketed paste mode.
|
||||
mTerminal.paste("a");
|
||||
assertEquals("Pasting 'a' should output 'a' when bracketed paste mode is disabled", "a", mOutput.getOutputAndClear());
|
||||
|
||||
enterString("\033[?2004h"); // Enable bracketed paste mode, again.
|
||||
mTerminal.paste("a");
|
||||
assertEquals("Pasting when in bracketed paste mode again should be bracketed", "\033[200~a\033[201~", mOutput.getOutputAndClear());
|
||||
|
||||
mTerminal.paste("\033ab\033cd\033");
|
||||
assertEquals("Pasting an escape character should not input it", "\033[200~abcd\033[201~", mOutput.getOutputAndClear());
|
||||
mTerminal.paste("\u0081ab\u0081cd\u009F");
|
||||
assertEquals("Pasting C1 control codes should not input it", "\033[200~abcd\033[201~", mOutput.getOutputAndClear());
|
||||
|
||||
mTerminal.reset();
|
||||
mTerminal.paste("a");
|
||||
assertEquals("Terminal reset() should disable bracketed paste mode", "a", mOutput.getOutputAndClear());
|
||||
}
|
||||
|
||||
/** DECSET 7, DECAWM, controls wraparound mode. */
|
||||
public void testWrapAroundMode() {
|
||||
// Default with wraparound:
|
||||
withTerminalSized(3, 3).enterString("abcd").assertLinesAre("abc", "d ", " ");
|
||||
// With wraparound disabled:
|
||||
withTerminalSized(3, 3).enterString("\033[?7labcd").assertLinesAre("abd", " ", " ");
|
||||
enterString("efg").assertLinesAre("abg", " ", " ");
|
||||
// Re-enabling wraparound:
|
||||
enterString("\033[?7hhij").assertLinesAre("abh", "ij ", " ");
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,53 @@
|
||||
package com.termux.terminal;
|
||||
|
||||
/**
|
||||
* "\033P" is a device control string.
|
||||
*/
|
||||
public class DeviceControlStringTest extends TerminalTestCase {
|
||||
|
||||
private static String hexEncode(String s) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (int i = 0; i < s.length(); i++)
|
||||
result.append(String.format("%02X", (int) s.charAt(i)));
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
private void assertCapabilityResponse(String cap, String expectedResponse) {
|
||||
String input = "\033P+q" + hexEncode(cap) + "\033\\";
|
||||
assertEnteringStringGivesResponse(input, "\033P1+r" + hexEncode(cap) + "=" + hexEncode(expectedResponse) + "\033\\");
|
||||
}
|
||||
|
||||
public void testReportColorsAndName() {
|
||||
// Request Termcap/Terminfo String. The string following the "q" is a list of names encoded in
|
||||
// hexadecimal (2 digits per character) separated by ; which correspond to termcap or terminfo key
|
||||
// names.
|
||||
// Two special features are also recognized, which are not key names: Co for termcap colors (or colors
|
||||
// for terminfo colors), and TN for termcap name (or name for terminfo name).
|
||||
// xterm responds with DCS 1 + r P t ST for valid requests, adding to P t an = , and the value of the
|
||||
// corresponding string that xterm would send, or DCS 0 + r P t ST for invalid requests. The strings are
|
||||
// encoded in hexadecimal (2 digits per character).
|
||||
withTerminalSized(3, 3).enterString("A");
|
||||
assertCapabilityResponse("Co", "256");
|
||||
assertCapabilityResponse("colors", "256");
|
||||
assertCapabilityResponse("TN", "xterm");
|
||||
assertCapabilityResponse("name", "xterm");
|
||||
enterString("B").assertLinesAre("AB ", " ", " ");
|
||||
}
|
||||
|
||||
public void testReportKeys() {
|
||||
withTerminalSized(3, 3);
|
||||
assertCapabilityResponse("kB", "\033[Z");
|
||||
}
|
||||
|
||||
public void testReallyLongDeviceControlString() {
|
||||
withTerminalSized(3, 3).enterString("\033P");
|
||||
for (int i = 0; i < 10000; i++) {
|
||||
enterString("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
||||
}
|
||||
// The terminal should ignore the overlong DCS sequence and continue printing "aaa." and fill at least the first two lines with
|
||||
// them:
|
||||
assertLineIs(0, "aaa");
|
||||
assertLineIs(1, "aaa");
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
package com.termux.terminal;
|
||||
|
||||
|
||||
public class HistoryTest extends TerminalTestCase {
|
||||
|
||||
public void testHistory() {
|
||||
final int rows = 3;
|
||||
final int cols = 3;
|
||||
withTerminalSized(cols, rows).enterString("111222333444555666777888999");
|
||||
assertCursorAt(2, 2);
|
||||
assertLinesAre("777", "888", "999");
|
||||
assertHistoryStartsWith("666", "555");
|
||||
|
||||
mTerminal.resize(cols, 2);
|
||||
assertHistoryStartsWith("777", "666", "555");
|
||||
|
||||
mTerminal.resize(cols, 3);
|
||||
assertHistoryStartsWith("666", "555");
|
||||
}
|
||||
|
||||
public void testHistoryWithScrollRegion() {
|
||||
// "CSI P_s ; P_s r" - set Scrolling Region [top;bottom] (default = full size of window) (DECSTBM).
|
||||
withTerminalSized(3, 4).enterString("111222333444");
|
||||
assertLinesAre("111", "222", "333", "444");
|
||||
enterString("\033[2;3r");
|
||||
// NOTE: "DECSTBM moves the cursor to column 1, line 1 of the page."
|
||||
assertCursorAt(0, 0);
|
||||
enterString("\nCDEFGH").assertLinesAre("111", "CDE", "FGH", "444");
|
||||
enterString("IJK").assertLinesAre("111", "FGH", "IJK", "444").assertHistoryStartsWith("CDE");
|
||||
enterString("LMN").assertLinesAre("111", "IJK", "LMN", "444").assertHistoryStartsWith("FGH", "CDE");
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,191 @@
|
||||
package com.termux.terminal;
|
||||
|
||||
import android.view.KeyEvent;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
public class KeyHandlerTest extends TestCase {
|
||||
|
||||
private static String stringToHex(String s) {
|
||||
if (s == null) return null;
|
||||
StringBuilder buffer = new StringBuilder();
|
||||
for (int i = 0; i < s.length(); i++) {
|
||||
if (buffer.length() > 0) {
|
||||
buffer.append(" ");
|
||||
}
|
||||
buffer.append("0x");
|
||||
buffer.append(Integer.toHexString(s.charAt(i)));
|
||||
}
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
private static void assertKeysEquals(String expected, String actual) {
|
||||
if (!expected.equals(actual)) {
|
||||
assertEquals(stringToHex(expected), stringToHex(actual));
|
||||
}
|
||||
}
|
||||
|
||||
/** See http://pubs.opengroup.org/onlinepubs/7990989799/xcurses/terminfo.html */
|
||||
public void testTermCaps() {
|
||||
// Backspace.
|
||||
assertKeysEquals("\u007f", KeyHandler.getCodeFromTermcap("kb", false, false));
|
||||
|
||||
// Back tab.
|
||||
assertKeysEquals("\033[Z", KeyHandler.getCodeFromTermcap("kB", false, false));
|
||||
|
||||
// Arrow keys (up/down/right/left):
|
||||
assertKeysEquals("\033[A", KeyHandler.getCodeFromTermcap("ku", false, false));
|
||||
assertKeysEquals("\033[B", KeyHandler.getCodeFromTermcap("kd", false, false));
|
||||
assertKeysEquals("\033[C", KeyHandler.getCodeFromTermcap("kr", false, false));
|
||||
assertKeysEquals("\033[D", KeyHandler.getCodeFromTermcap("kl", false, false));
|
||||
// .. shifted:
|
||||
assertKeysEquals("\033[1;2A", KeyHandler.getCodeFromTermcap("kUP", false, false));
|
||||
assertKeysEquals("\033[1;2B", KeyHandler.getCodeFromTermcap("kDN", false, false));
|
||||
assertKeysEquals("\033[1;2C", KeyHandler.getCodeFromTermcap("%i", false, false));
|
||||
assertKeysEquals("\033[1;2D", KeyHandler.getCodeFromTermcap("#4", false, false));
|
||||
|
||||
// Home/end keys:
|
||||
assertKeysEquals("\033[H", KeyHandler.getCodeFromTermcap("kh", false, false));
|
||||
assertKeysEquals("\033[F", KeyHandler.getCodeFromTermcap("@7", false, false));
|
||||
// ... shifted:
|
||||
assertKeysEquals("\033[1;2H", KeyHandler.getCodeFromTermcap("#2", false, false));
|
||||
assertKeysEquals("\033[1;2F", KeyHandler.getCodeFromTermcap("*7", false, false));
|
||||
|
||||
// The traditional keyboard keypad:
|
||||
// [Insert] [Home] [Page Up ]
|
||||
// [Delete] [End] [Page Down]
|
||||
//
|
||||
// Termcap names (with xterm response in parenthesis):
|
||||
// K1=Upper left of keypad (xterm sends same "<ESC>[H" = Home).
|
||||
// K2=Center of keypad (xterm sends invalid response).
|
||||
// K3=Upper right of keypad (xterm sends "<ESC>[5~" = Page Up).
|
||||
// K4=Lower left of keypad (xterm sends "<ESC>[F" = End key).
|
||||
// K5=Lower right of keypad (xterm sends "<ESC>[6~" = Page Down).
|
||||
//
|
||||
// vim/neovim (runtime/doc/term.txt):
|
||||
// t_K1 <kHome> keypad home key
|
||||
// t_K3 <kPageUp> keypad page-up key
|
||||
// t_K4 <kEnd> keypad end key
|
||||
// t_K5 <kPageDown> keypad page-down key
|
||||
//
|
||||
assertKeysEquals("\033[H", KeyHandler.getCodeFromTermcap("K1", false, false));
|
||||
assertKeysEquals("\033OH", KeyHandler.getCodeFromTermcap("K1", true, false));
|
||||
assertKeysEquals("\033[5~", KeyHandler.getCodeFromTermcap("K3", false, false));
|
||||
assertKeysEquals("\033[F", KeyHandler.getCodeFromTermcap("K4", false, false));
|
||||
assertKeysEquals("\033OF", KeyHandler.getCodeFromTermcap("K4", true, false));
|
||||
assertKeysEquals("\033[6~", KeyHandler.getCodeFromTermcap("K5", false, false));
|
||||
|
||||
// Function keys F1-F12:
|
||||
assertKeysEquals("\033OP", KeyHandler.getCodeFromTermcap("k1", false, false));
|
||||
assertKeysEquals("\033OQ", KeyHandler.getCodeFromTermcap("k2", false, false));
|
||||
assertKeysEquals("\033OR", KeyHandler.getCodeFromTermcap("k3", false, false));
|
||||
assertKeysEquals("\033OS", KeyHandler.getCodeFromTermcap("k4", false, false));
|
||||
assertKeysEquals("\033[15~", KeyHandler.getCodeFromTermcap("k5", false, false));
|
||||
assertKeysEquals("\033[17~", KeyHandler.getCodeFromTermcap("k6", false, false));
|
||||
assertKeysEquals("\033[18~", KeyHandler.getCodeFromTermcap("k7", false, false));
|
||||
assertKeysEquals("\033[19~", KeyHandler.getCodeFromTermcap("k8", false, false));
|
||||
assertKeysEquals("\033[20~", KeyHandler.getCodeFromTermcap("k9", false, false));
|
||||
assertKeysEquals("\033[21~", KeyHandler.getCodeFromTermcap("k;", false, false));
|
||||
assertKeysEquals("\033[23~", KeyHandler.getCodeFromTermcap("F1", false, false));
|
||||
assertKeysEquals("\033[24~", KeyHandler.getCodeFromTermcap("F2", false, false));
|
||||
// Function keys F13-F24 (same as shifted F1-F12):
|
||||
assertKeysEquals("\033[1;2P", KeyHandler.getCodeFromTermcap("F3", false, false));
|
||||
assertKeysEquals("\033[1;2Q", KeyHandler.getCodeFromTermcap("F4", false, false));
|
||||
assertKeysEquals("\033[1;2R", KeyHandler.getCodeFromTermcap("F5", false, false));
|
||||
assertKeysEquals("\033[1;2S", KeyHandler.getCodeFromTermcap("F6", false, false));
|
||||
assertKeysEquals("\033[15;2~", KeyHandler.getCodeFromTermcap("F7", false, false));
|
||||
assertKeysEquals("\033[17;2~", KeyHandler.getCodeFromTermcap("F8", false, false));
|
||||
assertKeysEquals("\033[18;2~", KeyHandler.getCodeFromTermcap("F9", false, false));
|
||||
assertKeysEquals("\033[19;2~", KeyHandler.getCodeFromTermcap("FA", false, false));
|
||||
assertKeysEquals("\033[20;2~", KeyHandler.getCodeFromTermcap("FB", false, false));
|
||||
assertKeysEquals("\033[21;2~", KeyHandler.getCodeFromTermcap("FC", false, false));
|
||||
assertKeysEquals("\033[23;2~", KeyHandler.getCodeFromTermcap("FD", false, false));
|
||||
assertKeysEquals("\033[24;2~", KeyHandler.getCodeFromTermcap("FE", false, false));
|
||||
}
|
||||
|
||||
public void testKeyCodes() {
|
||||
// Return sends carriage return (\r), which normally gets translated by the device driver to newline (\n) unless the ICRNL termios
|
||||
// flag has been set.
|
||||
assertKeysEquals("\r", KeyHandler.getCode(KeyEvent.KEYCODE_ENTER, 0, false, false));
|
||||
|
||||
// Backspace.
|
||||
assertKeysEquals("\u007f", KeyHandler.getCode(KeyEvent.KEYCODE_DEL, 0, false, false));
|
||||
|
||||
// Space.
|
||||
assertNull(KeyHandler.getCode(KeyEvent.KEYCODE_SPACE, 0, false, false));
|
||||
assertKeysEquals("\u0000", KeyHandler.getCode(KeyEvent.KEYCODE_SPACE, KeyHandler.KEYMOD_CTRL, false, false));
|
||||
|
||||
// Back tab.
|
||||
assertKeysEquals("\033[Z", KeyHandler.getCode(KeyEvent.KEYCODE_TAB, KeyHandler.KEYMOD_SHIFT, false, false));
|
||||
|
||||
// Arrow keys (up/down/right/left):
|
||||
assertKeysEquals("\033[A", KeyHandler.getCode(KeyEvent.KEYCODE_DPAD_UP, 0, false, false));
|
||||
assertKeysEquals("\033[B", KeyHandler.getCode(KeyEvent.KEYCODE_DPAD_DOWN, 0, false, false));
|
||||
assertKeysEquals("\033[C", KeyHandler.getCode(KeyEvent.KEYCODE_DPAD_RIGHT, 0, false, false));
|
||||
assertKeysEquals("\033[D", KeyHandler.getCode(KeyEvent.KEYCODE_DPAD_LEFT, 0, false, false));
|
||||
// .. shifted:
|
||||
assertKeysEquals("\033[1;2A", KeyHandler.getCode(KeyEvent.KEYCODE_DPAD_UP, KeyHandler.KEYMOD_SHIFT, false, false));
|
||||
assertKeysEquals("\033[1;2B", KeyHandler.getCode(KeyEvent.KEYCODE_DPAD_DOWN, KeyHandler.KEYMOD_SHIFT, false, false));
|
||||
assertKeysEquals("\033[1;2C", KeyHandler.getCode(KeyEvent.KEYCODE_DPAD_RIGHT, KeyHandler.KEYMOD_SHIFT, false, false));
|
||||
assertKeysEquals("\033[1;2D", KeyHandler.getCode(KeyEvent.KEYCODE_DPAD_LEFT, KeyHandler.KEYMOD_SHIFT, false, false));
|
||||
// .. ctrl:ed:
|
||||
assertKeysEquals("\033[1;5A", KeyHandler.getCode(KeyEvent.KEYCODE_DPAD_UP, KeyHandler.KEYMOD_CTRL, false, false));
|
||||
assertKeysEquals("\033[1;5B", KeyHandler.getCode(KeyEvent.KEYCODE_DPAD_DOWN, KeyHandler.KEYMOD_CTRL, false, false));
|
||||
assertKeysEquals("\033[1;5C", KeyHandler.getCode(KeyEvent.KEYCODE_DPAD_RIGHT, KeyHandler.KEYMOD_CTRL, false, false));
|
||||
assertKeysEquals("\033[1;5D", KeyHandler.getCode(KeyEvent.KEYCODE_DPAD_LEFT, KeyHandler.KEYMOD_CTRL, false, false));
|
||||
// .. ctrl:ed and shifted:
|
||||
int mod = KeyHandler.KEYMOD_CTRL | KeyHandler.KEYMOD_SHIFT;
|
||||
assertKeysEquals("\033[1;6A", KeyHandler.getCode(KeyEvent.KEYCODE_DPAD_UP, mod, false, false));
|
||||
assertKeysEquals("\033[1;6B", KeyHandler.getCode(KeyEvent.KEYCODE_DPAD_DOWN, mod, false, false));
|
||||
assertKeysEquals("\033[1;6C", KeyHandler.getCode(KeyEvent.KEYCODE_DPAD_RIGHT, mod, false, false));
|
||||
assertKeysEquals("\033[1;6D", KeyHandler.getCode(KeyEvent.KEYCODE_DPAD_LEFT, mod, false, false));
|
||||
|
||||
// Home/end keys:
|
||||
assertKeysEquals("\033[H", KeyHandler.getCode(KeyEvent.KEYCODE_MOVE_HOME, 0, false, false));
|
||||
assertKeysEquals("\033[F", KeyHandler.getCode(KeyEvent.KEYCODE_MOVE_END, 0, false, false));
|
||||
// ... shifted:
|
||||
assertKeysEquals("\033[1;2H", KeyHandler.getCode(KeyEvent.KEYCODE_MOVE_HOME, KeyHandler.KEYMOD_SHIFT, false, false));
|
||||
assertKeysEquals("\033[1;2F", KeyHandler.getCode(KeyEvent.KEYCODE_MOVE_END, KeyHandler.KEYMOD_SHIFT, false, false));
|
||||
|
||||
// Function keys F1-F12:
|
||||
assertKeysEquals("\033OP", KeyHandler.getCode(KeyEvent.KEYCODE_F1, 0, false, false));
|
||||
assertKeysEquals("\033OQ", KeyHandler.getCode(KeyEvent.KEYCODE_F2, 0, false, false));
|
||||
assertKeysEquals("\033OR", KeyHandler.getCode(KeyEvent.KEYCODE_F3, 0, false, false));
|
||||
assertKeysEquals("\033OS", KeyHandler.getCode(KeyEvent.KEYCODE_F4, 0, false, false));
|
||||
assertKeysEquals("\033[15~", KeyHandler.getCode(KeyEvent.KEYCODE_F5, 0, false, false));
|
||||
assertKeysEquals("\033[17~", KeyHandler.getCode(KeyEvent.KEYCODE_F6, 0, false, false));
|
||||
assertKeysEquals("\033[18~", KeyHandler.getCode(KeyEvent.KEYCODE_F7, 0, false, false));
|
||||
assertKeysEquals("\033[19~", KeyHandler.getCode(KeyEvent.KEYCODE_F8, 0, false, false));
|
||||
assertKeysEquals("\033[20~", KeyHandler.getCode(KeyEvent.KEYCODE_F9, 0, false, false));
|
||||
assertKeysEquals("\033[21~", KeyHandler.getCode(KeyEvent.KEYCODE_F10, 0, false, false));
|
||||
assertKeysEquals("\033[23~", KeyHandler.getCode(KeyEvent.KEYCODE_F11, 0, false, false));
|
||||
assertKeysEquals("\033[24~", KeyHandler.getCode(KeyEvent.KEYCODE_F12, 0, false, false));
|
||||
// Function keys F13-F24 (same as shifted F1-F12):
|
||||
assertKeysEquals("\033[1;2P", KeyHandler.getCode(KeyEvent.KEYCODE_F1, KeyHandler.KEYMOD_SHIFT, false, false));
|
||||
assertKeysEquals("\033[1;2Q", KeyHandler.getCode(KeyEvent.KEYCODE_F2, KeyHandler.KEYMOD_SHIFT, false, false));
|
||||
assertKeysEquals("\033[1;2R", KeyHandler.getCode(KeyEvent.KEYCODE_F3, KeyHandler.KEYMOD_SHIFT, false, false));
|
||||
assertKeysEquals("\033[1;2S", KeyHandler.getCode(KeyEvent.KEYCODE_F4, KeyHandler.KEYMOD_SHIFT, false, false));
|
||||
assertKeysEquals("\033[15;2~", KeyHandler.getCode(KeyEvent.KEYCODE_F5, KeyHandler.KEYMOD_SHIFT, false, false));
|
||||
assertKeysEquals("\033[17;2~", KeyHandler.getCode(KeyEvent.KEYCODE_F6, KeyHandler.KEYMOD_SHIFT, false, false));
|
||||
assertKeysEquals("\033[18;2~", KeyHandler.getCode(KeyEvent.KEYCODE_F7, KeyHandler.KEYMOD_SHIFT, false, false));
|
||||
assertKeysEquals("\033[19;2~", KeyHandler.getCode(KeyEvent.KEYCODE_F8, KeyHandler.KEYMOD_SHIFT, false, false));
|
||||
assertKeysEquals("\033[20;2~", KeyHandler.getCode(KeyEvent.KEYCODE_F9, KeyHandler.KEYMOD_SHIFT, false, false));
|
||||
assertKeysEquals("\033[21;2~", KeyHandler.getCode(KeyEvent.KEYCODE_F10, KeyHandler.KEYMOD_SHIFT, false, false));
|
||||
assertKeysEquals("\033[23;2~", KeyHandler.getCode(KeyEvent.KEYCODE_F11, KeyHandler.KEYMOD_SHIFT, false, false));
|
||||
assertKeysEquals("\033[24;2~", KeyHandler.getCode(KeyEvent.KEYCODE_F12, KeyHandler.KEYMOD_SHIFT, false, false));
|
||||
|
||||
assertKeysEquals("0", KeyHandler.getCode(KeyEvent.KEYCODE_NUMPAD_0, 0, false, false));
|
||||
assertKeysEquals("1", KeyHandler.getCode(KeyEvent.KEYCODE_NUMPAD_1, 0, false, false));
|
||||
assertKeysEquals("2", KeyHandler.getCode(KeyEvent.KEYCODE_NUMPAD_2, 0, false, false));
|
||||
assertKeysEquals("3", KeyHandler.getCode(KeyEvent.KEYCODE_NUMPAD_3, 0, false, false));
|
||||
assertKeysEquals("4", KeyHandler.getCode(KeyEvent.KEYCODE_NUMPAD_4, 0, false, false));
|
||||
assertKeysEquals("5", KeyHandler.getCode(KeyEvent.KEYCODE_NUMPAD_5, 0, false, false));
|
||||
assertKeysEquals("6", KeyHandler.getCode(KeyEvent.KEYCODE_NUMPAD_6, 0, false, false));
|
||||
assertKeysEquals("7", KeyHandler.getCode(KeyEvent.KEYCODE_NUMPAD_7, 0, false, false));
|
||||
assertKeysEquals("8", KeyHandler.getCode(KeyEvent.KEYCODE_NUMPAD_8, 0, false, false));
|
||||
assertKeysEquals("9", KeyHandler.getCode(KeyEvent.KEYCODE_NUMPAD_9, 0, false, false));
|
||||
assertKeysEquals(",", KeyHandler.getCode(KeyEvent.KEYCODE_NUMPAD_COMMA, 0, false, false));
|
||||
assertKeysEquals(".", KeyHandler.getCode(KeyEvent.KEYCODE_NUMPAD_DOT, 0, false, false));
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,196 @@
|
||||
package com.termux.terminal;
|
||||
|
||||
import android.util.Base64;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
/** "ESC ]" is the Operating System Command. */
|
||||
public class OperatingSystemControlTest extends TerminalTestCase {
|
||||
|
||||
public void testSetTitle() throws Exception {
|
||||
List<ChangedTitle> expectedTitleChanges = new ArrayList<>();
|
||||
|
||||
withTerminalSized(10, 10);
|
||||
enterString("\033]0;Hello, world\007");
|
||||
assertEquals("Hello, world", mTerminal.getTitle());
|
||||
expectedTitleChanges.add(new ChangedTitle(null, "Hello, world"));
|
||||
assertEquals(expectedTitleChanges, mOutput.titleChanges);
|
||||
|
||||
enterString("\033]0;Goodbye, world\007");
|
||||
assertEquals("Goodbye, world", mTerminal.getTitle());
|
||||
expectedTitleChanges.add(new ChangedTitle("Hello, world", "Goodbye, world"));
|
||||
assertEquals(expectedTitleChanges, mOutput.titleChanges);
|
||||
|
||||
enterString("\033]0;Goodbye, \u00F1 world\007");
|
||||
assertEquals("Goodbye, \uu00F1 world", mTerminal.getTitle());
|
||||
expectedTitleChanges.add(new ChangedTitle("Goodbye, world", "Goodbye, \uu00F1 world"));
|
||||
assertEquals(expectedTitleChanges, mOutput.titleChanges);
|
||||
|
||||
// 2 should work as well (0 sets both title and icon).
|
||||
enterString("\033]2;Updated\007");
|
||||
assertEquals("Updated", mTerminal.getTitle());
|
||||
expectedTitleChanges.add(new ChangedTitle("Goodbye, \uu00F1 world", "Updated"));
|
||||
assertEquals(expectedTitleChanges, mOutput.titleChanges);
|
||||
|
||||
enterString("\033[22;0t");
|
||||
enterString("\033]0;FIRST\007");
|
||||
expectedTitleChanges.add(new ChangedTitle("Updated", "FIRST"));
|
||||
assertEquals("FIRST", mTerminal.getTitle());
|
||||
assertEquals(expectedTitleChanges, mOutput.titleChanges);
|
||||
|
||||
enterString("\033[22;0t");
|
||||
enterString("\033]0;SECOND\007");
|
||||
assertEquals("SECOND", mTerminal.getTitle());
|
||||
|
||||
expectedTitleChanges.add(new ChangedTitle("FIRST", "SECOND"));
|
||||
assertEquals(expectedTitleChanges, mOutput.titleChanges);
|
||||
|
||||
enterString("\033[23;0t");
|
||||
assertEquals("FIRST", mTerminal.getTitle());
|
||||
|
||||
expectedTitleChanges.add(new ChangedTitle("SECOND", "FIRST"));
|
||||
assertEquals(expectedTitleChanges, mOutput.titleChanges);
|
||||
|
||||
enterString("\033[23;0t");
|
||||
expectedTitleChanges.add(new ChangedTitle("FIRST", "Updated"));
|
||||
assertEquals(expectedTitleChanges, mOutput.titleChanges);
|
||||
|
||||
enterString("\033[22;0t");
|
||||
enterString("\033[22;0t");
|
||||
enterString("\033[22;0t");
|
||||
// Popping to same title should not cause changes.
|
||||
enterString("\033[23;0t");
|
||||
enterString("\033[23;0t");
|
||||
enterString("\033[23;0t");
|
||||
assertEquals(expectedTitleChanges, mOutput.titleChanges);
|
||||
}
|
||||
|
||||
public void testTitleStack() throws Exception {
|
||||
// echo -ne '\e]0;BEFORE\007' # set title
|
||||
// echo -ne '\e[22t' # push to stack
|
||||
// echo -ne '\e]0;AFTER\007' # set new title
|
||||
// echo -ne '\e[23t' # retrieve from stack
|
||||
|
||||
withTerminalSized(10, 10);
|
||||
enterString("\033]0;InitialTitle\007");
|
||||
assertEquals("InitialTitle", mTerminal.getTitle());
|
||||
enterString("\033[22t");
|
||||
assertEquals("InitialTitle", mTerminal.getTitle());
|
||||
enterString("\033]0;UpdatedTitle\007");
|
||||
assertEquals("UpdatedTitle", mTerminal.getTitle());
|
||||
enterString("\033[23t");
|
||||
assertEquals("InitialTitle", mTerminal.getTitle());
|
||||
enterString("\033[23t\033[23t\033[23t");
|
||||
assertEquals("InitialTitle", mTerminal.getTitle());
|
||||
}
|
||||
|
||||
public void testSetColor() throws Exception {
|
||||
// "OSC 4; $INDEX; $COLORSPEC BEL" => Change color $INDEX to the color specified by $COLORSPEC.
|
||||
withTerminalSized(4, 4).enterString("\033]4;5;#00FF00\007");
|
||||
assertEquals(Integer.toHexString(0xFF00FF00), Integer.toHexString(mTerminal.mColors.mCurrentColors[5]));
|
||||
enterString("\033]4;5;#00FFAB\007");
|
||||
assertEquals(mTerminal.mColors.mCurrentColors[5], 0xFF00FFAB);
|
||||
enterString("\033]4;255;#ABFFAB\007");
|
||||
assertEquals(mTerminal.mColors.mCurrentColors[255], 0xFFABFFAB);
|
||||
// Two indexed colors at once:
|
||||
enterString("\033]4;7;#00FF00;8;#0000FF\007");
|
||||
assertEquals(mTerminal.mColors.mCurrentColors[7], 0xFF00FF00);
|
||||
assertEquals(mTerminal.mColors.mCurrentColors[8], 0xFF0000FF);
|
||||
}
|
||||
|
||||
void assertIndexColorsMatch(int[] expected) {
|
||||
for (int i = 0; i < 255; i++)
|
||||
assertEquals("index=" + i, expected[i], mTerminal.mColors.mCurrentColors[i]);
|
||||
}
|
||||
|
||||
public void testResetColor() throws Exception {
|
||||
withTerminalSized(4, 4);
|
||||
int[] initialColors = new int[TextStyle.NUM_INDEXED_COLORS];
|
||||
System.arraycopy(mTerminal.mColors.mCurrentColors, 0, initialColors, 0, initialColors.length);
|
||||
int[] expectedColors = new int[initialColors.length];
|
||||
System.arraycopy(mTerminal.mColors.mCurrentColors, 0, expectedColors, 0, expectedColors.length);
|
||||
Random rand = new Random();
|
||||
for (int endType = 0; endType < 3; endType++) {
|
||||
// Both BEL (7) and ST (ESC \) can end an OSC sequence.
|
||||
String ender = (endType == 0) ? "\007" : "\033\\";
|
||||
for (int i = 0; i < 255; i++) {
|
||||
expectedColors[i] = 0xFF000000 + (rand.nextInt() & 0xFFFFFF);
|
||||
int r = (expectedColors[i] >> 16) & 0xFF;
|
||||
int g = (expectedColors[i] >> 8) & 0xFF;
|
||||
int b = expectedColors[i] & 0xFF;
|
||||
String rgbHex = String.format("%02x", r) + String.format("%02x", g) + String.format("%02x", b);
|
||||
enterString("\033]4;" + i + ";#" + rgbHex + ender);
|
||||
assertEquals(expectedColors[i], mTerminal.mColors.mCurrentColors[i]);
|
||||
}
|
||||
}
|
||||
|
||||
enterString("\033]104;0\007");
|
||||
expectedColors[0] = TerminalColors.COLOR_SCHEME.mDefaultColors[0];
|
||||
assertIndexColorsMatch(expectedColors);
|
||||
enterString("\033]104;1;2\007");
|
||||
expectedColors[1] = TerminalColors.COLOR_SCHEME.mDefaultColors[1];
|
||||
expectedColors[2] = TerminalColors.COLOR_SCHEME.mDefaultColors[2];
|
||||
assertIndexColorsMatch(expectedColors);
|
||||
enterString("\033]104\007"); // Reset all colors.
|
||||
assertIndexColorsMatch(TerminalColors.COLOR_SCHEME.mDefaultColors);
|
||||
}
|
||||
|
||||
public void disabledTestSetClipboard() {
|
||||
// Cannot run this as a unit test since Base64 is a android.util class.
|
||||
enterString("\033]52;c;" + Base64.encodeToString("Hello, world".getBytes(), 0) + "\007");
|
||||
}
|
||||
|
||||
public void testResettingTerminalResetsColor() throws Exception {
|
||||
// "OSC 4; $INDEX; $COLORSPEC BEL" => Change color $INDEX to the color specified by $COLORSPEC.
|
||||
withTerminalSized(4, 4).enterString("\033]4;5;#00FF00\007");
|
||||
enterString("\033]4;5;#00FFAB\007").assertColor(5, 0xFF00FFAB);
|
||||
enterString("\033]4;255;#ABFFAB\007").assertColor(255, 0xFFABFFAB);
|
||||
mTerminal.reset();
|
||||
assertIndexColorsMatch(TerminalColors.COLOR_SCHEME.mDefaultColors);
|
||||
}
|
||||
|
||||
public void testSettingDynamicColors() {
|
||||
// "${OSC}${DYNAMIC};${COLORSPEC}${BEL_OR_STRINGTERMINATOR}" => Change ${DYNAMIC} color to the color specified by $COLORSPEC where:
|
||||
// DYNAMIC=10: Text foreground color.
|
||||
// DYNAMIC=11: Text background color.
|
||||
// DYNAMIC=12: Text cursor color.
|
||||
withTerminalSized(3, 3).enterString("\033]10;#ABCD00\007").assertColor(TextStyle.COLOR_INDEX_FOREGROUND, 0xFFABCD00);
|
||||
enterString("\033]11;#0ABCD0\007").assertColor(TextStyle.COLOR_INDEX_BACKGROUND, 0xFF0ABCD0);
|
||||
enterString("\033]12;#00ABCD\007").assertColor(TextStyle.COLOR_INDEX_CURSOR, 0xFF00ABCD);
|
||||
// Two special colors at once
|
||||
// ("Each successive parameter changes the next color in the list. The value of P s tells the starting point in the list"):
|
||||
enterString("\033]10;#FF0000;#00FF00\007").assertColor(TextStyle.COLOR_INDEX_FOREGROUND, 0xFFFF0000);
|
||||
assertColor(TextStyle.COLOR_INDEX_BACKGROUND, 0xFF00FF00);
|
||||
// Three at once:
|
||||
enterString("\033]10;#0000FF;#00FF00;#FF0000\007").assertColor(TextStyle.COLOR_INDEX_FOREGROUND, 0xFF0000FF);
|
||||
assertColor(TextStyle.COLOR_INDEX_BACKGROUND, 0xFF00FF00).assertColor(TextStyle.COLOR_INDEX_CURSOR, 0xFFFF0000);
|
||||
|
||||
// Without ending semicolon:
|
||||
enterString("\033]10;#FF0000\007").assertColor(TextStyle.COLOR_INDEX_FOREGROUND, 0xFFFF0000);
|
||||
// For background and cursor:
|
||||
enterString("\033]11;#FFFF00;\007").assertColor(TextStyle.COLOR_INDEX_BACKGROUND, 0xFFFFFF00);
|
||||
enterString("\033]12;#00FFFF;\007").assertColor(TextStyle.COLOR_INDEX_CURSOR, 0xFF00FFFF);
|
||||
|
||||
// Using string terminator:
|
||||
String stringTerminator = "\033\\";
|
||||
enterString("\033]10;#FF0000" + stringTerminator).assertColor(TextStyle.COLOR_INDEX_FOREGROUND, 0xFFFF0000);
|
||||
// For background and cursor:
|
||||
enterString("\033]11;#FFFF00;" + stringTerminator).assertColor(TextStyle.COLOR_INDEX_BACKGROUND, 0xFFFFFF00);
|
||||
enterString("\033]12;#00FFFF;" + stringTerminator).assertColor(TextStyle.COLOR_INDEX_CURSOR, 0xFF00FFFF);
|
||||
}
|
||||
|
||||
public void testReportSpecialColors() {
|
||||
// "${OSC}${DYNAMIC};?${BEL}" => Terminal responds with the control sequence which would set the current color.
|
||||
// Both xterm and libvte (gnome-terminal and others) use the longest color representation, which means that
|
||||
// the response is "${OSC}rgb:RRRR/GGGG/BBBB"
|
||||
withTerminalSized(3, 3).enterString("\033]10;#ABCD00\007").assertColor(TextStyle.COLOR_INDEX_FOREGROUND, 0xFFABCD00);
|
||||
assertEnteringStringGivesResponse("\033]10;?\007", "\033]10;rgb:abab/cdcd/0000\007");
|
||||
// Same as above but with string terminator. xterm uses the same string terminator in the response, which
|
||||
// e.g. script posted at http://superuser.com/questions/157563/programmatic-access-to-current-xterm-background-color
|
||||
// relies on:
|
||||
assertEnteringStringGivesResponse("\033]10;?\033\\", "\033]10;rgb:abab/cdcd/0000\033\\");
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,117 @@
|
||||
package com.termux.terminal;
|
||||
|
||||
public class RectangularAreasTest extends TerminalTestCase {
|
||||
|
||||
/** http://www.vt100.net/docs/vt510-rm/DECFRA */
|
||||
public void testFillRectangularArea() {
|
||||
withTerminalSized(3, 3).enterString("\033[88$x").assertLinesAre("XXX", "XXX", "XXX");
|
||||
withTerminalSized(3, 3).enterString("\033[88;1;1;2;10$x").assertLinesAre("XXX", "XXX", " ");
|
||||
withTerminalSized(3, 3).enterString("\033[88;2;1;3;10$x").assertLinesAre(" ", "XXX", "XXX");
|
||||
withTerminalSized(3, 3).enterString("\033[88;1;1;100;1$x").assertLinesAre("X ", "X ", "X ");
|
||||
withTerminalSized(3, 3).enterString("\033[88;1;1;100;2$x").assertLinesAre("XX ", "XX ", "XX ");
|
||||
withTerminalSized(3, 3).enterString("\033[88;100;1;100;2$x").assertLinesAre(" ", " ", " ");
|
||||
}
|
||||
|
||||
/** http://www.vt100.net/docs/vt510-rm/DECERA */
|
||||
public void testEraseRectangularArea() {
|
||||
withTerminalSized(3, 3).enterString("ABCDEFGHI\033[$z").assertLinesAre(" ", " ", " ");
|
||||
withTerminalSized(3, 3).enterString("ABCDEFGHI\033[1;1;2;10$z").assertLinesAre(" ", " ", "GHI");
|
||||
withTerminalSized(3, 3).enterString("ABCDEFGHI\033[2;1;3;10$z").assertLinesAre("ABC", " ", " ");
|
||||
withTerminalSized(3, 3).enterString("ABCDEFGHI\033[1;1;100;1$z").assertLinesAre(" BC", " EF", " HI");
|
||||
withTerminalSized(3, 3).enterString("ABCDEFGHI\033[1;1;100;2$z").assertLinesAre(" C", " F", " I");
|
||||
withTerminalSized(3, 3).enterString("ABCDEFGHI\033[100;1;100;2$z").assertLinesAre("ABC", "DEF", "GHI");
|
||||
|
||||
withTerminalSized(3, 3).enterString("A\033[$zBC").assertLinesAre(" BC", " ", " ");
|
||||
}
|
||||
|
||||
/** http://www.vt100.net/docs/vt510-rm/DECSED */
|
||||
public void testSelectiveEraseInDisplay() {
|
||||
// ${CSI}1"q enables protection, ${CSI}0"q disables it.
|
||||
// ${CSI}?${0,1,2}J" erases (0=cursor to end, 1=start to cursor, 2=complete display).
|
||||
withTerminalSized(3, 3).enterString("ABCDEFGHI\033[?2J").assertLinesAre(" ", " ", " ");
|
||||
withTerminalSized(3, 3).enterString("ABC\033[1\"qDE\033[0\"qFGHI\033[?2J").assertLinesAre(" ", "DE ", " ");
|
||||
withTerminalSized(3, 3).enterString("\033[1\"qABCDE\033[0\"qFGHI\033[?2J").assertLinesAre("ABC", "DE ", " ");
|
||||
}
|
||||
|
||||
/** http://vt100.net/docs/vt510-rm/DECSEL */
|
||||
public void testSelectiveEraseInLine() {
|
||||
// ${CSI}1"q enables protection, ${CSI}0"q disables it.
|
||||
// ${CSI}?${0,1,2}K" erases (0=cursor to end, 1=start to cursor, 2=complete line).
|
||||
withTerminalSized(3, 3).enterString("ABCDEFGHI\033[?2K").assertLinesAre("ABC", "DEF", " ");
|
||||
withTerminalSized(3, 3).enterString("ABCDE\033[?0KFGHI").assertLinesAre("ABC", "DEF", "GHI");
|
||||
withTerminalSized(3, 3).enterString("ABCDE\033[?1KFGHI").assertLinesAre("ABC", " F", "GHI");
|
||||
withTerminalSized(3, 3).enterString("ABCDE\033[?2KFGHI").assertLinesAre("ABC", " F", "GHI");
|
||||
withTerminalSized(3, 3).enterString("ABCDEFGHI\033[2;2H\033[?0K").assertLinesAre("ABC", "D ", "GHI");
|
||||
withTerminalSized(3, 3).enterString("ABC\033[1\"qD\033[0\"qE\033[?2KFGHI").assertLinesAre("ABC", "D F", "GHI");
|
||||
}
|
||||
|
||||
/** http://www.vt100.net/docs/vt510-rm/DECSERA */
|
||||
public void testSelectiveEraseInRectangle() {
|
||||
// ${CSI}1"q enables protection, ${CSI}0"q disables it.
|
||||
// ${CSI}?${TOP};${LEFT};${BOTTOM};${RIGHT}${" erases.
|
||||
withTerminalSized(3, 3).enterString("ABCDEFGHI\033[${").assertLinesAre(" ", " ", " ");
|
||||
withTerminalSized(3, 3).enterString("ABCDEFGHI\033[1;1;2;10${").assertLinesAre(" ", " ", "GHI");
|
||||
withTerminalSized(3, 3).enterString("ABCDEFGHI\033[2;1;3;10${").assertLinesAre("ABC", " ", " ");
|
||||
withTerminalSized(3, 3).enterString("ABCDEFGHI\033[1;1;100;1${").assertLinesAre(" BC", " EF", " HI");
|
||||
withTerminalSized(3, 3).enterString("ABCDEFGHI\033[1;1;100;2${").assertLinesAre(" C", " F", " I");
|
||||
withTerminalSized(3, 3).enterString("ABCDEFGHI\033[100;1;100;2${").assertLinesAre("ABC", "DEF", "GHI");
|
||||
|
||||
withTerminalSized(3, 3).enterString("ABCD\033[1\"qE\033[0\"qFGHI\033[${").assertLinesAre(" ", " E ", " ");
|
||||
withTerminalSized(3, 3).enterString("ABCD\033[1\"qE\033[0\"qFGHI\033[1;1;2;10${").assertLinesAre(" ", " E ", "GHI");
|
||||
}
|
||||
|
||||
/** http://vt100.net/docs/vt510-rm/DECCRA */
|
||||
public void testRectangularCopy() {
|
||||
// "${CSI}${SRC_TOP};${SRC_LEFT};${SRC_BOTTOM};${SRC_RIGHT};${SRC_PAGE};${DST_TOP};${DST_LEFT};${DST_PAGE}\$v"
|
||||
withTerminalSized(7, 3).enterString("ABC\r\nDEF\r\nGHI\033[1;1;2;2;1;2;5;1$v").assertLinesAre("ABC ", "DEF AB ", "GHI DE ");
|
||||
withTerminalSized(7, 3).enterString("ABC\r\nDEF\r\nGHI\033[1;1;3;3;1;1;4;1$v").assertLinesAre("ABCABC ", "DEFDEF ", "GHIGHI ");
|
||||
withTerminalSized(7, 3).enterString("ABC\r\nDEF\r\nGHI\033[1;1;3;3;1;1;3;1$v").assertLinesAre("ABABC ", "DEDEF ", "GHGHI ");
|
||||
withTerminalSized(7, 3).enterString(" ABC\r\n DEF\r\n GHI\033[1;4;3;6;1;1;1;1$v").assertLinesAre("ABCABC ", "DEFDEF ",
|
||||
"GHIGHI ");
|
||||
withTerminalSized(7, 3).enterString(" ABC\r\n DEF\r\n GHI\033[1;4;3;6;1;1;2;1$v").assertLinesAre(" ABCBC ", " DEFEF ",
|
||||
" GHIHI ");
|
||||
withTerminalSized(3, 3).enterString("ABC\r\nDEF\r\nGHI\033[1;1;2;2;1;2;2;1$v").assertLinesAre("ABC", "DAB", "GDE");
|
||||
|
||||
// Enable ${CSI}?6h origin mode (DECOM) and ${CSI}?69h for left/right margin (DECLRMM) enabling, ${CSI}${LEFTMARGIN};${RIGHTMARGIN}s
|
||||
// for DECSLRM margin setting.
|
||||
withTerminalSized(5, 5).enterString("\033[?6h\033[?69h\033[2;4s");
|
||||
enterString("ABCDEFGHIJK").assertLinesAre(" ABC ", " DEF ", " GHI ", " JK ", " ");
|
||||
enterString("\033[1;1;2;2;1;2;2;1$v").assertLinesAre(" ABC ", " DAB ", " GDE ", " JK ", " ");
|
||||
}
|
||||
|
||||
/** http://vt100.net/docs/vt510-rm/DECCARA */
|
||||
public void testChangeAttributesInRectangularArea() {
|
||||
final int b = TextStyle.CHARACTER_ATTRIBUTE_BOLD;
|
||||
// "${CSI}${TOP};${LEFT};${BOTTOM};${RIGHT};${ATTRIBUTES}\$r"
|
||||
withTerminalSized(3, 3).enterString("ABCDEFGHI\033[1;1;2;2;1$r").assertLinesAre("ABC", "DEF", "GHI");
|
||||
assertEffectAttributesSet(effectLine(b, b, b), effectLine(b, b, 0), effectLine(0, 0, 0));
|
||||
|
||||
// Now with http://www.vt100.net/docs/vt510-rm/DECSACE ("${CSI}2*x") specifying rectangle:
|
||||
withTerminalSized(3, 3).enterString("\033[2*xABCDEFGHI\033[1;1;2;2;1$r").assertLinesAre("ABC", "DEF", "GHI");
|
||||
assertEffectAttributesSet(effectLine(b, b, 0), effectLine(b, b, 0), effectLine(0, 0, 0));
|
||||
}
|
||||
|
||||
/** http://vt100.net/docs/vt510-rm/DECCARA */
|
||||
public void testReverseAttributesInRectangularArea() {
|
||||
final int b = TextStyle.CHARACTER_ATTRIBUTE_BOLD;
|
||||
final int u = TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE;
|
||||
final int bu = TextStyle.CHARACTER_ATTRIBUTE_BOLD | TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE;
|
||||
// "${CSI}${TOP};${LEFT};${BOTTOM};${RIGHT};${ATTRIBUTES}\$t"
|
||||
withTerminalSized(3, 3).enterString("ABCDEFGHI\033[1;1;2;2;1$t").assertLinesAre("ABC", "DEF", "GHI");
|
||||
assertEffectAttributesSet(effectLine(b, b, b), effectLine(b, b, 0), effectLine(0, 0, 0));
|
||||
|
||||
// Now with http://www.vt100.net/docs/vt510-rm/DECSACE ("${CSI}2*x") specifying rectangle:
|
||||
withTerminalSized(3, 3).enterString("\033[2*xABCDEFGHI\033[1;1;2;2;1$t").assertLinesAre("ABC", "DEF", "GHI");
|
||||
assertEffectAttributesSet(effectLine(b, b, 0), effectLine(b, b, 0), effectLine(0, 0, 0));
|
||||
|
||||
// Check reversal by initially bolding the B:
|
||||
withTerminalSized(3, 3).enterString("\033[2*xA\033[1mB\033[0mCDEFGHI\033[1;1;2;2;1$t").assertLinesAre("ABC", "DEF", "GHI");
|
||||
assertEffectAttributesSet(effectLine(b, 0, 0), effectLine(b, b, 0), effectLine(0, 0, 0));
|
||||
|
||||
// Check reversal by initially underlining A, bolding B, then reversing both bold and underline:
|
||||
withTerminalSized(3, 3).enterString("\033[2*x\033[4mA\033[0m\033[1mB\033[0mCDEFGHI\033[1;1;2;2;1;4$t").assertLinesAre("ABC", "DEF",
|
||||
"GHI");
|
||||
assertEffectAttributesSet(effectLine(b, u, 0), effectLine(bu, bu, 0), effectLine(0, 0, 0));
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,212 @@
|
||||
package com.termux.terminal;
|
||||
|
||||
public class ResizeTest extends TerminalTestCase {
|
||||
|
||||
public void testResizeWhenHasHistory() {
|
||||
final int cols = 3;
|
||||
withTerminalSized(cols, 3).enterString("111222333444555666777888999").assertCursorAt(2, 2).assertLinesAre("777", "888", "999");
|
||||
resize(cols, 5).assertCursorAt(4, 2).assertLinesAre("555", "666", "777", "888", "999");
|
||||
resize(cols, 3).assertCursorAt(2, 2).assertLinesAre("777", "888", "999");
|
||||
}
|
||||
|
||||
public void testResizeWhenInAltBuffer() {
|
||||
final int rows = 3, cols = 3;
|
||||
withTerminalSized(cols, rows).enterString("a\r\ndef$").assertLinesAre("a ", "def", "$ ").assertCursorAt(2, 1);
|
||||
|
||||
// Resize and back again while in main buffer:
|
||||
resize(cols, 5).assertLinesAre("a ", "def", "$ ", " ", " ").assertCursorAt(2, 1);
|
||||
resize(cols, rows).assertLinesAre("a ", "def", "$ ").assertCursorAt(2, 1);
|
||||
|
||||
// Switch to alt buffer:
|
||||
enterString("\033[?1049h").assertLinesAre(" ", " ", " ").assertCursorAt(2, 1);
|
||||
enterString("h").assertLinesAre(" ", " ", " h ").assertCursorAt(2, 2);
|
||||
|
||||
resize(cols, 5).resize(cols, rows);
|
||||
|
||||
// Switch from alt buffer:
|
||||
enterString("\033[?1049l").assertLinesAre("a ", "def", "$ ").assertCursorAt(2, 1);
|
||||
}
|
||||
|
||||
public void testShrinkingInAltBuffer() {
|
||||
final int rows = 5;
|
||||
final int cols = 3;
|
||||
withTerminalSized(cols, rows).enterString("A\r\nB\r\nC\r\nD\r\nE").assertLinesAre("A ", "B ", "C ", "D ", "E ");
|
||||
enterString("\033[?1049h").assertLinesAre(" ", " ", " ", " ", " ");
|
||||
resize(3, 3).enterString("\033[?1049lF").assertLinesAre("C ", "D ", "EF ");
|
||||
}
|
||||
|
||||
public void testResizeAfterNewlineWhenInAltBuffer() {
|
||||
final int rows = 3;
|
||||
final int cols = 3;
|
||||
withTerminalSized(cols, rows);
|
||||
enterString("a\r\nb\r\nc\r\nd\r\ne\r\nf\r\n").assertLinesAre("e ", "f ", " ").assertCursorAt(2, 0);
|
||||
assertLineWraps(false, false, false);
|
||||
|
||||
// Switch to alt buffer:
|
||||
enterString("\033[?1049h").assertLinesAre(" ", " ", " ").assertCursorAt(2, 0);
|
||||
enterString("h").assertLinesAre(" ", " ", "h ").assertCursorAt(2, 1);
|
||||
|
||||
// Grow by two rows:
|
||||
resize(cols, 5).assertLinesAre(" ", " ", "h ", " ", " ").assertCursorAt(2, 1);
|
||||
resize(cols, rows).assertLinesAre(" ", " ", "h ").assertCursorAt(2, 1);
|
||||
|
||||
// Switch from alt buffer:
|
||||
enterString("\033[?1049l").assertLinesAre("e ", "f ", " ").assertCursorAt(2, 0);
|
||||
}
|
||||
|
||||
public void testResizeAfterHistoryWraparound() {
|
||||
final int rows = 3;
|
||||
final int cols = 10;
|
||||
withTerminalSized(cols, rows);
|
||||
StringBuilder buffer = new StringBuilder();
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
String s = Integer.toString(i);
|
||||
enterString(s);
|
||||
buffer.setLength(0);
|
||||
buffer.append(s);
|
||||
while (buffer.length() < cols)
|
||||
buffer.append(' ');
|
||||
if (i > rows) {
|
||||
assertLineIs(rows - 1, buffer.toString());
|
||||
}
|
||||
enterString("\r\n");
|
||||
}
|
||||
assertLinesAre("998 ", "999 ", " ");
|
||||
mTerminal.resize(cols, 2);
|
||||
assertLinesAre("999 ", " ");
|
||||
mTerminal.resize(cols, 5);
|
||||
assertLinesAre("996 ", "997 ", "998 ", "999 ", " ");
|
||||
mTerminal.resize(cols, rows);
|
||||
assertLinesAre("998 ", "999 ", " ");
|
||||
}
|
||||
|
||||
public void testVerticalResize() {
|
||||
final int rows = 5;
|
||||
final int cols = 3;
|
||||
|
||||
withTerminalSized(cols, rows);
|
||||
// Foreground color to 119:
|
||||
enterString("\033[38;5;119m");
|
||||
// Background color to 129:
|
||||
enterString("\033[48;5;129m");
|
||||
// Clear with ED, Erase in Display:
|
||||
enterString("\033[2J");
|
||||
for (int r = 0; r < rows; r++) {
|
||||
for (int c = 0; c < cols; c++) {
|
||||
long style = getStyleAt(r, c);
|
||||
assertEquals(119, TextStyle.decodeForeColor(style));
|
||||
assertEquals(129, TextStyle.decodeBackColor(style));
|
||||
}
|
||||
}
|
||||
enterString("11\r\n22");
|
||||
assertLinesAre("11 ", "22 ", " ", " ", " ").assertLineWraps(false, false, false, false, false);
|
||||
resize(cols, rows - 2).assertLinesAre("11 ", "22 ", " ");
|
||||
|
||||
// After resize, screen should still be same color:
|
||||
for (int r = 0; r < rows - 2; r++) {
|
||||
for (int c = 0; c < cols; c++) {
|
||||
long style = getStyleAt(r, c);
|
||||
assertEquals(119, TextStyle.decodeForeColor(style));
|
||||
assertEquals(129, TextStyle.decodeBackColor(style));
|
||||
}
|
||||
}
|
||||
|
||||
// Background color to 200 and grow back size (which should be cleared to the new background color):
|
||||
enterString("\033[48;5;200m");
|
||||
resize(cols, rows);
|
||||
for (int r = 0; r < rows; r++) {
|
||||
for (int c = 0; c < cols; c++) {
|
||||
long style = getStyleAt(r, c);
|
||||
assertEquals(119, TextStyle.decodeForeColor(style));
|
||||
assertEquals("wrong at row=" + r, r >= 3 ? 200 : 129, TextStyle.decodeBackColor(style));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void testHorizontalResize() {
|
||||
final int rows = 5;
|
||||
final int cols = 5;
|
||||
|
||||
withTerminalSized(cols, rows);
|
||||
// Background color to 129:
|
||||
// enterString("\033[48;5;129m").assertLinesAre(" ", " ", " ", " ", " ");
|
||||
enterString("1111\r\n2222\r\n3333\r\n4444\r\n5555").assertCursorAt(4, 4);
|
||||
// assertEquals(129, TextStyle.decodeBackColor(getStyleAt(2, 2)));
|
||||
assertLinesAre("1111 ", "2222 ", "3333 ", "4444 ", "5555 ").assertLineWraps(false, false, false, false, false);
|
||||
resize(cols + 2, rows).assertLinesAre("1111 ", "2222 ", "3333 ", "4444 ", "5555 ").assertCursorAt(4, 4);
|
||||
assertLineWraps(false, false, false, false, false);
|
||||
resize(cols, rows).assertLinesAre("1111 ", "2222 ", "3333 ", "4444 ", "5555 ").assertCursorAt(4, 4);
|
||||
assertLineWraps(false, false, false, false, false);
|
||||
resize(cols - 1, rows).assertLinesAre("2222", "3333", "4444", "5555", " ").assertCursorAt(4, 0);
|
||||
assertLineWraps(false, false, false, true, false);
|
||||
resize(cols - 2, rows).assertLinesAre("3 ", "444", "4 ", "555", "5 ").assertCursorAt(4, 1);
|
||||
assertLineWraps(false, true, false, true, false);
|
||||
// Back to original size:
|
||||
resize(cols, rows).assertLinesAre("1111 ", "2222 ", "3333 ", "4444 ", "5555 ").assertCursorAt(4, 4);
|
||||
assertLineWraps(false, false, false, false, false);
|
||||
}
|
||||
|
||||
public void testLineWrap() {
|
||||
final int rows = 3, cols = 5;
|
||||
withTerminalSized(cols, rows).enterString("111111").assertLinesAre("11111", "1 ", " ");
|
||||
assertCursorAt(1, 1).assertLineWraps(true, false, false);
|
||||
|
||||
resize(7, rows).assertCursorAt(0, 6).assertLinesAre("111111 ", " ", " ").assertLineWraps(false, false, false);
|
||||
resize(cols, rows).assertCursorAt(1, 1).assertLinesAre("11111", "1 ", " ").assertLineWraps(true, false, false);
|
||||
|
||||
enterString("2").assertLinesAre("11111", "12 ", " ").assertLineWraps(true, false, false);
|
||||
enterString("123").assertLinesAre("11111", "12123", " ").assertLineWraps(true, false, false);
|
||||
enterString("W").assertLinesAre("11111", "12123", "W ").assertLineWraps(true, true, false);
|
||||
|
||||
withTerminalSized(cols, rows).enterString("1234512345");
|
||||
assertLinesAre("12345", "12345", " ").assertLineWraps(true, false, false);
|
||||
enterString("W").assertLinesAre("12345", "12345", "W ").assertLineWraps(true, true, false);
|
||||
}
|
||||
|
||||
public void testCursorPositionWhenShrinking() {
|
||||
final int rows = 5, cols = 3;
|
||||
withTerminalSized(cols, rows).enterString("$ ").assertLinesAre("$ ", " ", " ", " ", " ").assertCursorAt(0, 2);
|
||||
resize(3, 3).assertLinesAre("$ ", " ", " ").assertCursorAt(0, 2);
|
||||
resize(cols, rows).assertLinesAre("$ ", " ", " ", " ", " ").assertCursorAt(0, 2);
|
||||
}
|
||||
|
||||
public void testResizeWithCombiningCharInLastColumn() {
|
||||
withTerminalSized(3, 3).enterString("ABC\u0302DEF").assertLinesAre("ABC\u0302", "DEF", " ");
|
||||
resize(4, 3).assertLinesAre("ABC\u0302D", "EF ", " ");
|
||||
|
||||
// Same as above but with colors:
|
||||
withTerminalSized(3, 3).enterString("\033[37mA\033[35mB\033[33mC\u0302\033[32mD\033[31mE\033[34mF").assertLinesAre("ABC\u0302",
|
||||
"DEF", " ");
|
||||
resize(4, 3).assertLinesAre("ABC\u0302D", "EF ", " ");
|
||||
assertForegroundIndices(effectLine(7, 5, 3, 2), effectLine(1, 4, 4, 4), effectLine(4, 4, 4, 4));
|
||||
}
|
||||
|
||||
public void testResizeWithLineWrappingContinuing() {
|
||||
withTerminalSized(5, 3).enterString("\r\nAB DE").assertLinesAre(" ", "AB DE", " ");
|
||||
resize(4, 3).assertLinesAre("AB D", "E ", " ");
|
||||
resize(3, 3).assertLinesAre("AB ", "DE ", " ");
|
||||
resize(5, 3).assertLinesAre(" ", "AB DE", " ");
|
||||
}
|
||||
|
||||
public void testResizeWithWideChars() {
|
||||
final int rows = 3, cols = 4;
|
||||
String twoCharsWidthOne = new String(Character.toChars(TerminalRowTest.TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_1));
|
||||
withTerminalSized(cols, rows).enterString(twoCharsWidthOne).enterString("\r\n");
|
||||
enterString(twoCharsWidthOne).assertLinesAre(twoCharsWidthOne + " ", twoCharsWidthOne + " ", " ");
|
||||
resize(3, 3).assertLinesAre(twoCharsWidthOne + " ", twoCharsWidthOne + " ", " ");
|
||||
enterString(twoCharsWidthOne).assertLinesAre(twoCharsWidthOne + " ", twoCharsWidthOne + twoCharsWidthOne + " ", " ");
|
||||
}
|
||||
|
||||
public void testResizeWithMoreWideChars() {
|
||||
final int rows = 4, cols = 5;
|
||||
|
||||
withTerminalSized(cols, rows).enterString("qqrr").assertLinesAre("qqrr ", " ", " ", " ");
|
||||
resize(2, rows).assertLinesAre("qq", "rr", " ", " ");
|
||||
resize(5, rows).assertLinesAre("qqrr ", " ", " ", " ");
|
||||
|
||||
withTerminalSized(cols, rows).enterString("QR").assertLinesAre("QR ", " ", " ", " ");
|
||||
resize(2, rows).assertLinesAre("Q", "R", " ", " ");
|
||||
resize(5, rows).assertLinesAre("QR ", " ", " ", " ");
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,40 @@
|
||||
package com.termux.terminal;
|
||||
|
||||
public class ScreenBufferTest extends TerminalTestCase {
|
||||
|
||||
public void testBasics() {
|
||||
TerminalBuffer screen = new TerminalBuffer(5, 3, 3);
|
||||
assertEquals("", screen.getTranscriptText());
|
||||
screen.setChar(0, 0, 'a', 0);
|
||||
assertEquals("a", screen.getTranscriptText());
|
||||
screen.setChar(0, 0, 'b', 0);
|
||||
assertEquals("b", screen.getTranscriptText());
|
||||
screen.setChar(2, 0, 'c', 0);
|
||||
assertEquals("b c", screen.getTranscriptText());
|
||||
screen.setChar(2, 2, 'f', 0);
|
||||
assertEquals("b c\n\n f", screen.getTranscriptText());
|
||||
screen.blockSet(0, 0, 2, 2, 'X', 0);
|
||||
}
|
||||
|
||||
public void testBlockSet() {
|
||||
TerminalBuffer screen = new TerminalBuffer(5, 3, 3);
|
||||
screen.blockSet(0, 0, 2, 2, 'X', 0);
|
||||
assertEquals("XX\nXX", screen.getTranscriptText());
|
||||
screen.blockSet(1, 1, 2, 2, 'Y', 0);
|
||||
assertEquals("XX\nXYY\n YY", screen.getTranscriptText());
|
||||
}
|
||||
|
||||
public void testGetSelectedText() {
|
||||
withTerminalSized(5, 3).enterString("ABCDEFGHIJ").assertLinesAre("ABCDE", "FGHIJ", " ");
|
||||
assertEquals("AB", mTerminal.getSelectedText(0, 0, 1, 0));
|
||||
assertEquals("BC", mTerminal.getSelectedText(1, 0, 2, 0));
|
||||
assertEquals("CDE", mTerminal.getSelectedText(2, 0, 4, 0));
|
||||
assertEquals("FG", mTerminal.getSelectedText(0, 1, 1, 1));
|
||||
assertEquals("GH", mTerminal.getSelectedText(1, 1, 2, 1));
|
||||
assertEquals("HIJ", mTerminal.getSelectedText(2, 1, 4, 1));
|
||||
|
||||
assertEquals("ABCDEFG", mTerminal.getSelectedText(0, 0, 1, 1));
|
||||
withTerminalSized(5, 3).enterString("ABCDE\r\nFGHIJ").assertLinesAre("ABCDE", "FGHIJ", " ");
|
||||
assertEquals("ABCDE\nFG", mTerminal.getSelectedText(0, 0, 1, 1));
|
||||
}
|
||||
}
|
@@ -0,0 +1,101 @@
|
||||
package com.termux.terminal;
|
||||
|
||||
/**
|
||||
* ${CSI}${top};${bottom}r" - set Scrolling Region [top;bottom] (default = full size of window) (DECSTBM).
|
||||
* <p/>
|
||||
* "DECSTBM moves the cursor to column 1, line 1 of the page" (http://www.vt100.net/docs/vt510-rm/DECSTBM).
|
||||
*/
|
||||
public class ScrollRegionTest extends TerminalTestCase {
|
||||
|
||||
public void testScrollRegionTop() {
|
||||
withTerminalSized(3, 4).enterString("111222333444").assertLinesAre("111", "222", "333", "444");
|
||||
enterString("\033[2r").assertCursorAt(0, 0);
|
||||
enterString("\r\n\r\n\r\n\r\nCDEFGH").assertLinesAre("111", "444", "CDE", "FGH").assertHistoryStartsWith("333");
|
||||
enterString("IJK").assertLinesAre("111", "CDE", "FGH", "IJK").assertHistoryStartsWith("444");
|
||||
// Reset scroll region and enter line:
|
||||
enterString("\033[r").enterString("\r\n\r\n\r\n").enterString("LMNOPQ").assertLinesAre("CDE", "FGH", "LMN", "OPQ");
|
||||
}
|
||||
|
||||
public void testScrollRegionBottom() {
|
||||
withTerminalSized(3, 4).enterString("111222333444");
|
||||
assertLinesAre("111", "222", "333", "444");
|
||||
enterString("\033[1;3r").assertCursorAt(0, 0);
|
||||
enterString("\r\n\r\nCDEFGH").assertLinesAre("222", "CDE", "FGH", "444").assertHistoryStartsWith("111");
|
||||
// Reset scroll region and enter line:
|
||||
enterString("\033[r").enterString("\r\n\r\n\r\n").enterString("IJKLMN").assertLinesAre("CDE", "FGH", "IJK", "LMN");
|
||||
}
|
||||
|
||||
public void testScrollRegionResetWithOriginMode() {
|
||||
withTerminalSized(3, 4).enterString("111222333444");
|
||||
assertLinesAre("111", "222", "333", "444");
|
||||
// "\033[?6h" sets origin mode, so that the later DECSTBM resets cursor to below margin:
|
||||
enterString("\033[?6h\033[2r").assertCursorAt(1, 0);
|
||||
}
|
||||
|
||||
public void testScrollRegionLeft() {
|
||||
// ${CSI}?69h for DECLRMM enabling, ${CSI}${LEFTMARGIN};${RIGHTMARGIN}s for DECSLRM margin setting.
|
||||
withTerminalSized(3, 3).enterString("\033[?69h\033[2sABCDEFG").assertLinesAre("ABC", " DE", " FG");
|
||||
enterString("HI").assertLinesAre("ADE", " FG", " HI").enterString("JK").assertLinesAre("AFG", " HI", " JK");
|
||||
enterString("\n").assertLinesAre("AHI", " JK", " ");
|
||||
}
|
||||
|
||||
public void testScrollRegionRight() {
|
||||
// ${CSI}?69h for DECLRMM enabling, ${CSI}${LEFTMARGIN};${RIGHTMARGIN}s for DECSLRM margin setting.
|
||||
withTerminalSized(3, 3).enterString("YYY\033[?69h\033[1;2sABCDEF").assertLinesAre("ABY", "CD ", "EF ");
|
||||
enterString("GH").assertLinesAre("CDY", "EF ", "GH ").enterString("IJ").assertLinesAre("EFY", "GH ", "IJ ");
|
||||
enterString("\n").assertLinesAre("GHY", "IJ ", " ");
|
||||
}
|
||||
|
||||
public void testScrollRegionOnAllSides() {
|
||||
// ${CSI}?69h for DECLRMM enabling, ${CSI}${LEFTMARGIN};${RIGHTMARGIN}s for DECSLRM margin setting.
|
||||
withTerminalSized(4, 4).enterString("ABCDEFGHIJKLMNOP").assertLinesAre("ABCD", "EFGH", "IJKL", "MNOP");
|
||||
// http://www.vt100.net/docs/vt510-rm/DECOM
|
||||
enterString("\033[?6h\033[2;3r").assertCursorAt(1, 0);
|
||||
enterString("\033[?69h\033[2;3s").assertCursorAt(1, 1);
|
||||
enterString("QRST").assertLinesAre("ABCD", "EQRH", "ISTL", "MNOP");
|
||||
enterString("UV").assertLinesAre("ABCD", "ESTH", "IUVL", "MNOP");
|
||||
}
|
||||
|
||||
public void testDECCOLMResetsScrollMargin() {
|
||||
// DECCOLM — Select 80 or 132 Columns per Page (http://www.vt100.net/docs/vt510-rm/DECCOLM) has the important
|
||||
// side effect to clear scroll margins, which is useful for e.g. the "reset" utility to clear scroll margins.
|
||||
withTerminalSized(3, 4).enterString("111222333444").assertLinesAre("111", "222", "333", "444");
|
||||
enterString("\033[2r\033[?3h\r\nABCDEFGHIJKL").assertLinesAre("ABC", "DEF", "GHI", "JKL");
|
||||
}
|
||||
|
||||
public void testScrollOutsideVerticalRegion() {
|
||||
withTerminalSized(3, 4).enterString("\033[0;2rhi\033[4;0Hyou").assertLinesAre("hi ", " ", " ", "you");
|
||||
//enterString("see").assertLinesAre("hi ", " ", " ", "see");
|
||||
}
|
||||
|
||||
public void testNELRespectsLeftMargin() {
|
||||
// vttest "Menu 11.3.2: VT420 Cursor-Movement Test", select "10. Test other movement (CR/HT/LF/FF) within margins".
|
||||
// The NEL (ESC E) sequence moves cursor to first position on next line, where first position depends on origin mode and margin.
|
||||
withTerminalSized(3, 3).enterString("\033[?69h\033[2sABC\033ED").assertLinesAre("ABC", "D ", " ");
|
||||
withTerminalSized(3, 3).enterString("\033[?69h\033[2sABC\033[?6h\033ED").assertLinesAre("ABC", " D ", " ");
|
||||
}
|
||||
|
||||
public void testBackwardIndex() {
|
||||
// vttest "Menu 11.3.2: VT420 Cursor-Movement Test", test 7.
|
||||
// Without margins:
|
||||
withTerminalSized(3, 3).enterString("ABCDEF\0336H").assertLinesAre("ABC", "DHF", " ");
|
||||
enterString("\0336\0336I").assertLinesAre("ABC", "IHF", " ");
|
||||
enterString("\0336\0336").assertLinesAre(" AB", " IH", " ");
|
||||
// With left margin:
|
||||
withTerminalSized(3, 3).enterString("\033[?69h\033[2sABCDEF\0336\0336").assertLinesAre("A B", " D", " F");
|
||||
}
|
||||
|
||||
public void testForwardIndex() {
|
||||
// vttest "Menu 11.3.2: VT420 Cursor-Movement Test", test 8.
|
||||
// Without margins:
|
||||
withTerminalSized(3, 3).enterString("ABCD\0339E").assertLinesAre("ABC", "D E", " ");
|
||||
enterString("\0339").assertLinesAre("BC ", " E ", " ");
|
||||
// With right margin:
|
||||
withTerminalSized(3, 3).enterString("\033[?69h\033[0;2sABCD\0339").assertLinesAre("B ", "D ", " ");
|
||||
}
|
||||
|
||||
public void testScrollDownWithScrollRegion() {
|
||||
withTerminalSized(2, 5).enterString("1\r\n2\r\n3\r\n4\r\n5").assertLinesAre("1 ", "2 ", "3 ", "4 ", "5 ");
|
||||
enterString("\033[3r").enterString("\033[2T").assertLinesAre("1 ", "2 ", " ", " ", "3 ");
|
||||
}
|
||||
}
|
@@ -0,0 +1,431 @@
|
||||
package com.termux.terminal;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Random;
|
||||
|
||||
public class TerminalRowTest extends TestCase {
|
||||
|
||||
/** The properties of these code points are validated in {@link #testStaticConstants()}. */
|
||||
private static final int ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1 = 0x679C;
|
||||
private static final int ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2 = 0x679D;
|
||||
private static final int TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_1 = 0x2070E;
|
||||
private static final int TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_2 = 0x20731;
|
||||
|
||||
/** Unicode Character 'MUSICAL SYMBOL G CLEF' (U+1D11E). Two java chars required for this. */
|
||||
static final int TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_1 = 0x1D11E;
|
||||
/** Unicode Character 'MUSICAL SYMBOL G CLEF OTTAVA ALTA' (U+1D11F). Two java chars required for this. */
|
||||
private static final int TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_2 = 0x1D11F;
|
||||
|
||||
private final int COLUMNS = 80;
|
||||
|
||||
/** A combining character. */
|
||||
private static final int DIARESIS_CODEPOINT = 0x0308;
|
||||
|
||||
private TerminalRow row;
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
row = new TerminalRow(COLUMNS, TextStyle.NORMAL);
|
||||
}
|
||||
|
||||
private void assertLineStartsWith(int... codePoints) {
|
||||
char[] chars = row.mText;
|
||||
int charIndex = 0;
|
||||
for (int i = 0; i < codePoints.length; i++) {
|
||||
int lineCodePoint = chars[charIndex++];
|
||||
if (Character.isHighSurrogate((char) lineCodePoint)) {
|
||||
lineCodePoint = Character.toCodePoint((char) lineCodePoint, chars[charIndex++]);
|
||||
}
|
||||
assertEquals("Differing a code point index=" + i, codePoints[i], lineCodePoint);
|
||||
}
|
||||
}
|
||||
|
||||
private void assertColumnCharIndicesStartsWith(int... indices) {
|
||||
for (int i = 0; i < indices.length; i++) {
|
||||
int expected = indices[i];
|
||||
int actual = row.findStartOfColumn(i);
|
||||
assertEquals("At index=" + i, expected, actual);
|
||||
}
|
||||
}
|
||||
|
||||
public void testSimpleDiaresis() {
|
||||
row.setChar(0, DIARESIS_CODEPOINT, 0);
|
||||
assertEquals(81, row.getSpaceUsed());
|
||||
row.setChar(0, DIARESIS_CODEPOINT, 0);
|
||||
assertEquals(82, row.getSpaceUsed());
|
||||
assertLineStartsWith(' ', DIARESIS_CODEPOINT, DIARESIS_CODEPOINT, ' ');
|
||||
}
|
||||
|
||||
public void testStaticConstants() {
|
||||
assertEquals(1, Character.charCount(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1));
|
||||
assertEquals(1, Character.charCount(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2));
|
||||
assertEquals(2, WcWidth.width(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1));
|
||||
assertEquals(2, WcWidth.width(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2));
|
||||
|
||||
assertEquals(2, Character.charCount(TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_1));
|
||||
assertEquals(2, Character.charCount(TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_2));
|
||||
assertEquals(1, WcWidth.width(TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_1));
|
||||
assertEquals(1, WcWidth.width(TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_2));
|
||||
|
||||
assertEquals(2, Character.charCount(TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_1));
|
||||
assertEquals(2, Character.charCount(TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_2));
|
||||
assertEquals(2, WcWidth.width(TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_1));
|
||||
assertEquals(2, WcWidth.width(TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_2));
|
||||
|
||||
assertEquals(1, Character.charCount(DIARESIS_CODEPOINT));
|
||||
assertEquals(0, WcWidth.width(DIARESIS_CODEPOINT));
|
||||
}
|
||||
|
||||
public void testOneColumn() {
|
||||
assertEquals(0, row.findStartOfColumn(0));
|
||||
row.setChar(0, 'a', 0);
|
||||
assertEquals(0, row.findStartOfColumn(0));
|
||||
}
|
||||
|
||||
public void testAscii() {
|
||||
assertEquals(0, row.findStartOfColumn(0));
|
||||
row.setChar(0, 'a', 0);
|
||||
assertLineStartsWith('a', ' ', ' ');
|
||||
assertEquals(1, row.findStartOfColumn(1));
|
||||
assertEquals(80, row.getSpaceUsed());
|
||||
row.setChar(0, 'b', 0);
|
||||
assertEquals(1, row.findStartOfColumn(1));
|
||||
assertEquals(2, row.findStartOfColumn(2));
|
||||
assertEquals(80, row.getSpaceUsed());
|
||||
assertColumnCharIndicesStartsWith(0, 1, 2, 3);
|
||||
|
||||
char[] someChars = new char[]{'a', 'c', 'e', '4', '5', '6', '7', '8'};
|
||||
|
||||
char[] rawLine = new char[80];
|
||||
Arrays.fill(rawLine, ' ');
|
||||
Random random = new Random();
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
int lineIndex = random.nextInt(rawLine.length);
|
||||
int charIndex = random.nextInt(someChars.length);
|
||||
rawLine[lineIndex] = someChars[charIndex];
|
||||
row.setChar(lineIndex, someChars[charIndex], 0);
|
||||
}
|
||||
char[] lineChars = row.mText;
|
||||
for (int i = 0; i < rawLine.length; i++) {
|
||||
assertEquals(rawLine[i], lineChars[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public void testUnicode() {
|
||||
assertEquals(0, row.findStartOfColumn(0));
|
||||
assertEquals(80, row.getSpaceUsed());
|
||||
|
||||
row.setChar(0, TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_1, 0);
|
||||
assertEquals(81, row.getSpaceUsed());
|
||||
assertEquals(0, row.findStartOfColumn(0));
|
||||
assertEquals(2, row.findStartOfColumn(1));
|
||||
assertLineStartsWith(TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_1, ' ', ' ');
|
||||
assertColumnCharIndicesStartsWith(0, 2, 3, 4);
|
||||
|
||||
row.setChar(0, 'a', 0);
|
||||
assertEquals(80, row.getSpaceUsed());
|
||||
assertEquals(0, row.findStartOfColumn(0));
|
||||
assertEquals(1, row.findStartOfColumn(1));
|
||||
assertLineStartsWith('a', ' ', ' ');
|
||||
assertColumnCharIndicesStartsWith(0, 1, 2, 3);
|
||||
|
||||
row.setChar(0, TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_1, 0);
|
||||
row.setChar(1, 'a', 0);
|
||||
assertLineStartsWith(TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_1, 'a', ' ');
|
||||
|
||||
row.setChar(0, TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_1, 0);
|
||||
row.setChar(1, TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_2, 0);
|
||||
assertLineStartsWith(TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_1, TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_2, ' ');
|
||||
assertColumnCharIndicesStartsWith(0, 2, 4, 5);
|
||||
assertEquals(82, row.getSpaceUsed());
|
||||
}
|
||||
|
||||
public void testDoubleWidth() {
|
||||
row.setChar(0, 'a', 0);
|
||||
row.setChar(1, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2, 0);
|
||||
assertLineStartsWith('a', ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2, ' ');
|
||||
assertColumnCharIndicesStartsWith(0, 1, 1, 2);
|
||||
row.setChar(0, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, 0);
|
||||
assertLineStartsWith(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, ' ', ' ');
|
||||
assertColumnCharIndicesStartsWith(0, 0, 1, 2);
|
||||
|
||||
row.setChar(0, ' ', 0);
|
||||
assertLineStartsWith(' ', ' ', ' ', ' ');
|
||||
assertColumnCharIndicesStartsWith(0, 1, 2, 3, 4);
|
||||
row.setChar(0, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, 0);
|
||||
row.setChar(2, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2, 0);
|
||||
assertLineStartsWith(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2);
|
||||
assertColumnCharIndicesStartsWith(0, 0, 1, 1, 2);
|
||||
row.setChar(0, 'a', 0);
|
||||
assertLineStartsWith('a', ' ', ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2, ' ');
|
||||
}
|
||||
|
||||
/** Just as {@link #testDoubleWidth()} but requires a surrogate pair. */
|
||||
public void testDoubleWidthSurrogage() {
|
||||
row.setChar(0, 'a', 0);
|
||||
assertColumnCharIndicesStartsWith(0, 1, 2, 3, 4);
|
||||
|
||||
row.setChar(1, TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_2, 0);
|
||||
assertColumnCharIndicesStartsWith(0, 1, 1, 3, 4);
|
||||
assertLineStartsWith('a', TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_2, ' ');
|
||||
row.setChar(0, TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_1, 0);
|
||||
assertColumnCharIndicesStartsWith(0, 0, 2, 3, 4);
|
||||
assertLineStartsWith(TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_1, ' ', ' ', ' ');
|
||||
|
||||
row.setChar(0, ' ', 0);
|
||||
assertLineStartsWith(' ', ' ', ' ', ' ');
|
||||
row.setChar(0, TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_1, 0);
|
||||
row.setChar(1, TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_2, 0);
|
||||
assertLineStartsWith(' ', TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_2, ' ');
|
||||
row.setChar(0, 'a', 0);
|
||||
assertLineStartsWith('a', TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_2, ' ');
|
||||
}
|
||||
|
||||
public void testReplacementChar() {
|
||||
row.setChar(0, TerminalEmulator.UNICODE_REPLACEMENT_CHAR, 0);
|
||||
row.setChar(1, 'Y', 0);
|
||||
assertLineStartsWith(TerminalEmulator.UNICODE_REPLACEMENT_CHAR, 'Y', ' ', ' ');
|
||||
}
|
||||
|
||||
public void testSurrogateCharsWithNormalDisplayWidth() {
|
||||
// These requires a UTF-16 surrogate pair, and has a display width of one.
|
||||
int first = 0x1D306;
|
||||
int second = 0x1D307;
|
||||
// Assert the above statement:
|
||||
assertEquals(2, Character.toChars(first).length);
|
||||
assertEquals(2, Character.toChars(second).length);
|
||||
|
||||
row.setChar(0, second, 0);
|
||||
assertEquals(second, Character.toCodePoint(row.mText[0], row.mText[1]));
|
||||
assertEquals(' ', row.mText[2]);
|
||||
assertEquals(2, row.findStartOfColumn(1));
|
||||
|
||||
row.setChar(0, first, 0);
|
||||
assertEquals(first, Character.toCodePoint(row.mText[0], row.mText[1]));
|
||||
assertEquals(' ', row.mText[2]);
|
||||
assertEquals(2, row.findStartOfColumn(1));
|
||||
|
||||
row.setChar(1, second, 0);
|
||||
row.setChar(2, 'a', 0);
|
||||
assertEquals(first, Character.toCodePoint(row.mText[0], row.mText[1]));
|
||||
assertEquals(second, Character.toCodePoint(row.mText[2], row.mText[3]));
|
||||
assertEquals('a', row.mText[4]);
|
||||
assertEquals(' ', row.mText[5]);
|
||||
assertEquals(0, row.findStartOfColumn(0));
|
||||
assertEquals(2, row.findStartOfColumn(1));
|
||||
assertEquals(4, row.findStartOfColumn(2));
|
||||
assertEquals(5, row.findStartOfColumn(3));
|
||||
assertEquals(6, row.findStartOfColumn(4));
|
||||
|
||||
row.setChar(0, ' ', 0);
|
||||
assertEquals(' ', row.mText[0]);
|
||||
assertEquals(second, Character.toCodePoint(row.mText[1], row.mText[2]));
|
||||
assertEquals('a', row.mText[3]);
|
||||
assertEquals(' ', row.mText[4]);
|
||||
assertEquals(0, row.findStartOfColumn(0));
|
||||
assertEquals(1, row.findStartOfColumn(1));
|
||||
assertEquals(3, row.findStartOfColumn(2));
|
||||
assertEquals(4, row.findStartOfColumn(3));
|
||||
assertEquals(5, row.findStartOfColumn(4));
|
||||
|
||||
for (int i = 0; i < 80; i++) {
|
||||
row.setChar(i, i % 2 == 0 ? first : second, 0);
|
||||
}
|
||||
for (int i = 0; i < 80; i++) {
|
||||
int idx = row.findStartOfColumn(i);
|
||||
assertEquals(i % 2 == 0 ? first : second, Character.toCodePoint(row.mText[idx], row.mText[idx + 1]));
|
||||
}
|
||||
for (int i = 0; i < 80; i++) {
|
||||
row.setChar(i, i % 2 == 0 ? 'a' : 'b', 0);
|
||||
}
|
||||
for (int i = 0; i < 80; i++) {
|
||||
int idx = row.findStartOfColumn(i);
|
||||
assertEquals(i, idx);
|
||||
assertEquals(i % 2 == 0 ? 'a' : 'b', row.mText[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public void testOverwritingDoubleDisplayWidthWithNormalDisplayWidth() {
|
||||
// Initial "OO "
|
||||
row.setChar(0, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, 0);
|
||||
assertEquals(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, row.mText[0]);
|
||||
assertEquals(' ', row.mText[1]);
|
||||
assertEquals(0, row.findStartOfColumn(0));
|
||||
assertEquals(0, row.findStartOfColumn(1));
|
||||
assertEquals(1, row.findStartOfColumn(2));
|
||||
|
||||
// Setting first column to a clears second: "a "
|
||||
row.setChar(0, 'a', 0);
|
||||
assertEquals('a', row.mText[0]);
|
||||
assertEquals(' ', row.mText[1]);
|
||||
assertEquals(0, row.findStartOfColumn(0));
|
||||
assertEquals(1, row.findStartOfColumn(1));
|
||||
assertEquals(2, row.findStartOfColumn(2));
|
||||
|
||||
// Back to initial "OO "
|
||||
row.setChar(0, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, 0);
|
||||
assertEquals(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, row.mText[0]);
|
||||
assertEquals(' ', row.mText[1]);
|
||||
assertEquals(0, row.findStartOfColumn(0));
|
||||
assertEquals(0, row.findStartOfColumn(1));
|
||||
assertEquals(1, row.findStartOfColumn(2));
|
||||
|
||||
// Setting first column to a clears first: " a "
|
||||
row.setChar(1, 'a', 0);
|
||||
assertEquals(' ', row.mText[0]);
|
||||
assertEquals('a', row.mText[1]);
|
||||
assertEquals(' ', row.mText[2]);
|
||||
assertEquals(0, row.findStartOfColumn(0));
|
||||
assertEquals(1, row.findStartOfColumn(1));
|
||||
assertEquals(2, row.findStartOfColumn(2));
|
||||
}
|
||||
|
||||
public void testOverwritingDoubleDisplayWidthWithSelf() {
|
||||
row.setChar(0, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, 0);
|
||||
row.setChar(0, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, 0);
|
||||
assertEquals(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, row.mText[0]);
|
||||
assertEquals(' ', row.mText[1]);
|
||||
assertEquals(0, row.findStartOfColumn(0));
|
||||
assertEquals(0, row.findStartOfColumn(1));
|
||||
assertEquals(1, row.findStartOfColumn(2));
|
||||
}
|
||||
|
||||
public void testNormalCharsWithDoubleDisplayWidth() {
|
||||
// These fit in one java char, and has a display width of two.
|
||||
assertTrue(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1 != ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2);
|
||||
assertEquals(1, Character.charCount(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1));
|
||||
assertEquals(1, Character.charCount(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2));
|
||||
assertEquals(2, WcWidth.width(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1));
|
||||
assertEquals(2, WcWidth.width(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2));
|
||||
|
||||
row.setChar(0, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, 0);
|
||||
assertEquals(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, row.mText[0]);
|
||||
assertEquals(0, row.findStartOfColumn(1));
|
||||
assertEquals(' ', row.mText[1]);
|
||||
|
||||
row.setChar(0, 'a', 0);
|
||||
assertEquals('a', row.mText[0]);
|
||||
assertEquals(' ', row.mText[1]);
|
||||
assertEquals(1, row.findStartOfColumn(1));
|
||||
|
||||
row.setChar(0, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, 0);
|
||||
assertEquals(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, row.mText[0]);
|
||||
// The first character fills both first columns.
|
||||
assertEquals(0, row.findStartOfColumn(1));
|
||||
row.setChar(2, 'a', 0);
|
||||
assertEquals(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, row.mText[0]);
|
||||
assertEquals('a', row.mText[1]);
|
||||
assertEquals(1, row.findStartOfColumn(2));
|
||||
|
||||
row.setChar(0, 'c', 0);
|
||||
assertEquals('c', row.mText[0]);
|
||||
assertEquals(' ', row.mText[1]);
|
||||
assertEquals('a', row.mText[2]);
|
||||
assertEquals(' ', row.mText[3]);
|
||||
assertEquals(0, row.findStartOfColumn(0));
|
||||
assertEquals(1, row.findStartOfColumn(1));
|
||||
assertEquals(2, row.findStartOfColumn(2));
|
||||
}
|
||||
|
||||
public void testNormalCharsWithDoubleDisplayWidthOverlapping() {
|
||||
// These fit in one java char, and has a display width of two.
|
||||
row.setChar(0, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, 0);
|
||||
row.setChar(2, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2, 0);
|
||||
row.setChar(4, 'a', 0);
|
||||
// O = ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO
|
||||
// A = ANOTHER_JAVA_CHAR_DISPLAY_WIDTH_TWO
|
||||
// "OOAAa "
|
||||
assertEquals(0, row.findStartOfColumn(0));
|
||||
assertEquals(0, row.findStartOfColumn(1));
|
||||
assertEquals(1, row.findStartOfColumn(2));
|
||||
assertEquals(1, row.findStartOfColumn(3));
|
||||
assertEquals(2, row.findStartOfColumn(4));
|
||||
assertEquals(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, row.mText[0]);
|
||||
assertEquals(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2, row.mText[1]);
|
||||
assertEquals('a', row.mText[2]);
|
||||
assertEquals(' ', row.mText[3]);
|
||||
|
||||
row.setChar(1, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2, 0);
|
||||
// " AA a "
|
||||
assertEquals(' ', row.mText[0]);
|
||||
assertEquals(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2, row.mText[1]);
|
||||
assertEquals(' ', row.mText[2]);
|
||||
assertEquals('a', row.mText[3]);
|
||||
assertEquals(' ', row.mText[4]);
|
||||
assertEquals(0, row.findStartOfColumn(0));
|
||||
assertEquals(1, row.findStartOfColumn(1));
|
||||
assertEquals(1, row.findStartOfColumn(2));
|
||||
assertEquals(2, row.findStartOfColumn(3));
|
||||
assertEquals(3, row.findStartOfColumn(4));
|
||||
}
|
||||
|
||||
// https://github.com/jackpal/Android-Terminal-Emulator/issues/145
|
||||
public void testCrashATE145() {
|
||||
// 0xC2541 is unassigned, use display width 1 for UNICODE_REPLACEMENT_CHAR.
|
||||
// assertEquals(1, WcWidth.width(0xC2541));
|
||||
assertEquals(2, Character.charCount(0xC2541));
|
||||
|
||||
assertEquals(2, WcWidth.width(0x73EE));
|
||||
assertEquals(1, Character.charCount(0x73EE));
|
||||
|
||||
assertEquals(0, WcWidth.width(0x009F));
|
||||
assertEquals(1, Character.charCount(0x009F));
|
||||
|
||||
int[] points = new int[]{0xC2541, 'a', '8', 0x73EE, 0x009F, 0x881F, 0x8324, 0xD4C9, 0xFFFD, 'B', 0x009B, 0x61C9, 'Z'};
|
||||
// int[] expected = new int[] { TerminalEmulator.UNICODE_REPLACEMENT_CHAR, 'a', '8', 0x73EE, 0x009F, 0x881F, 0x8324, 0xD4C9, 0xFFFD,
|
||||
// 'B', 0x009B, 0x61C9, 'Z' };
|
||||
int currentColumn = 0;
|
||||
for (int point : points) {
|
||||
row.setChar(currentColumn, point, 0);
|
||||
currentColumn += WcWidth.width(point);
|
||||
}
|
||||
// assertLineStartsWith(points);
|
||||
// assertEquals(Character.highSurrogate(0xC2541), line.mText[0]);
|
||||
// assertEquals(Character.lowSurrogate(0xC2541), line.mText[1]);
|
||||
// assertEquals('a', line.mText[2]);
|
||||
// assertEquals('8', line.mText[3]);
|
||||
// assertEquals(Character.highSurrogate(0x73EE), line.mText[4]);
|
||||
// assertEquals(Character.lowSurrogate(0x73EE), line.mText[5]);
|
||||
//
|
||||
// char[] chars = line.mText;
|
||||
// int charIndex = 0;
|
||||
// for (int i = 0; i < points.length; i++) {
|
||||
// char c = chars[charIndex];
|
||||
// charIndex++;
|
||||
// int thisPoint = (int) c;
|
||||
// if (Character.isHighSurrogate(c)) {
|
||||
// thisPoint = Character.toCodePoint(c, chars[charIndex]);
|
||||
// charIndex++;
|
||||
// }
|
||||
// assertEquals("At index=" + i + ", charIndex=" + charIndex + ", char=" + (char) thisPoint, points[i], thisPoint);
|
||||
// }
|
||||
}
|
||||
|
||||
public void testNormalization() {
|
||||
// int lowerCaseN = 0x006E;
|
||||
// int combiningTilde = 0x0303;
|
||||
// int combined = 0x00F1;
|
||||
row.setChar(0, 0x006E, 0);
|
||||
assertEquals(80, row.getSpaceUsed());
|
||||
row.setChar(0, 0x0303, 0);
|
||||
assertEquals(81, row.getSpaceUsed());
|
||||
// assertEquals("\u00F1 ", new String(term.getScreen().getLine(0)));
|
||||
assertLineStartsWith(0x006E, 0x0303, ' ');
|
||||
}
|
||||
|
||||
public void testInsertWideAtLastColumn() {
|
||||
row.setChar(COLUMNS - 2, 'Z', 0);
|
||||
row.setChar(COLUMNS - 1, 'a', 0);
|
||||
assertEquals('Z', row.mText[row.findStartOfColumn(COLUMNS - 2)]);
|
||||
assertEquals('a', row.mText[row.findStartOfColumn(COLUMNS - 1)]);
|
||||
row.setChar(COLUMNS - 1, 'ö', 0);
|
||||
assertEquals('Z', row.mText[row.findStartOfColumn(COLUMNS - 2)]);
|
||||
assertEquals('ö', row.mText[row.findStartOfColumn(COLUMNS - 1)]);
|
||||
// line.setChar(COLUMNS - 1, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1);
|
||||
// assertEquals('Z', line.mText[line.findStartOfColumn(COLUMNS - 2)]);
|
||||
// assertEquals(' ', line.mText[line.findStartOfColumn(COLUMNS - 1)]);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,289 @@
|
||||
package com.termux.terminal;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
||||
public class TerminalTest extends TerminalTestCase {
|
||||
|
||||
public void testCursorPositioning() throws Exception {
|
||||
withTerminalSized(10, 10).placeCursorAndAssert(1, 2).placeCursorAndAssert(3, 5).placeCursorAndAssert(2, 2).enterString("A")
|
||||
.assertCursorAt(2, 3);
|
||||
}
|
||||
|
||||
public void testScreen() throws UnsupportedEncodingException {
|
||||
withTerminalSized(3, 3);
|
||||
assertLinesAre(" ", " ", " ");
|
||||
|
||||
assertEquals("", mTerminal.getScreen().getTranscriptText());
|
||||
enterString("hi").assertLinesAre("hi ", " ", " ");
|
||||
assertEquals("hi", mTerminal.getScreen().getTranscriptText());
|
||||
enterString("\r\nu");
|
||||
assertEquals("hi\nu", mTerminal.getScreen().getTranscriptText());
|
||||
mTerminal.reset();
|
||||
assertEquals("hi\nu", mTerminal.getScreen().getTranscriptText());
|
||||
|
||||
withTerminalSized(3, 3).enterString("hello");
|
||||
assertEquals("hello", mTerminal.getScreen().getTranscriptText());
|
||||
enterString("\r\nworld");
|
||||
assertEquals("hello\nworld", mTerminal.getScreen().getTranscriptText());
|
||||
}
|
||||
|
||||
public void testScrollDownInAltBuffer() {
|
||||
withTerminalSized(3, 3).enterString("\033[?1049h");
|
||||
enterString("\033[38;5;111m1\r\n");
|
||||
enterString("\033[38;5;112m2\r\n");
|
||||
enterString("\033[38;5;113m3\r\n");
|
||||
enterString("\033[38;5;114m4\r\n");
|
||||
enterString("\033[38;5;115m5");
|
||||
assertLinesAre("3 ", "4 ", "5 ");
|
||||
assertForegroundColorAt(0, 0, 113);
|
||||
assertForegroundColorAt(1, 0, 114);
|
||||
assertForegroundColorAt(2, 0, 115);
|
||||
}
|
||||
|
||||
public void testMouseClick() throws Exception {
|
||||
withTerminalSized(10, 10);
|
||||
assertFalse(mTerminal.isMouseTrackingActive());
|
||||
enterString("\033[?1000h");
|
||||
assertTrue(mTerminal.isMouseTrackingActive());
|
||||
enterString("\033[?1000l");
|
||||
assertFalse(mTerminal.isMouseTrackingActive());
|
||||
enterString("\033[?1000h");
|
||||
assertTrue(mTerminal.isMouseTrackingActive());
|
||||
|
||||
enterString("\033[?1006h");
|
||||
mTerminal.sendMouseEvent(TerminalEmulator.MOUSE_LEFT_BUTTON, 3, 4, true);
|
||||
assertEquals("\033[<0;3;4M", mOutput.getOutputAndClear());
|
||||
mTerminal.sendMouseEvent(TerminalEmulator.MOUSE_LEFT_BUTTON, 3, 4, false);
|
||||
assertEquals("\033[<0;3;4m", mOutput.getOutputAndClear());
|
||||
}
|
||||
|
||||
public void testNormalization() throws UnsupportedEncodingException {
|
||||
// int lowerCaseN = 0x006E;
|
||||
// int combiningTilde = 0x0303;
|
||||
// int combined = 0x00F1;
|
||||
withTerminalSized(3, 3).assertLinesAre(" ", " ", " ");
|
||||
enterString("\u006E\u0303");
|
||||
assertEquals(1, WcWidth.width("\u006E\u0303".toCharArray(), 0));
|
||||
// assertEquals("\u00F1 ", new String(mTerminal.getScreen().getLine(0)));
|
||||
assertLinesAre("\u006E\u0303 ", " ", " ");
|
||||
}
|
||||
|
||||
/** On "\e[18t" xterm replies with "\e[8;${HEIGHT};${WIDTH}t" */
|
||||
public void testReportTerminalSize() throws Exception {
|
||||
withTerminalSized(5, 5);
|
||||
assertEnteringStringGivesResponse("\033[18t", "\033[8;5;5t");
|
||||
for (int width = 3; width < 12; width++) {
|
||||
for (int height = 3; height < 12; height++) {
|
||||
mTerminal.resize(width, height);
|
||||
assertEnteringStringGivesResponse("\033[18t", "\033[8;" + height + ";" + width + "t");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Device Status Report (DSR) and Report Cursor Position (CPR). */
|
||||
public void testDeviceStatusReport() throws Exception {
|
||||
withTerminalSized(5, 5);
|
||||
assertEnteringStringGivesResponse("\033[5n", "\033[0n");
|
||||
|
||||
assertEnteringStringGivesResponse("\033[6n", "\033[1;1R");
|
||||
enterString("AB");
|
||||
assertEnteringStringGivesResponse("\033[6n", "\033[1;3R");
|
||||
enterString("\r\n");
|
||||
assertEnteringStringGivesResponse("\033[6n", "\033[2;1R");
|
||||
}
|
||||
|
||||
/** Test the cursor shape changes using DECSCUSR. */
|
||||
public void testSetCursorStyle() throws Exception {
|
||||
withTerminalSized(5, 5);
|
||||
assertEquals(TerminalEmulator.CURSOR_STYLE_BLOCK, mTerminal.getCursorStyle());
|
||||
enterString("\033[3 q");
|
||||
assertEquals(TerminalEmulator.CURSOR_STYLE_UNDERLINE, mTerminal.getCursorStyle());
|
||||
enterString("\033[5 q");
|
||||
assertEquals(TerminalEmulator.CURSOR_STYLE_BAR, mTerminal.getCursorStyle());
|
||||
enterString("\033[0 q");
|
||||
assertEquals(TerminalEmulator.CURSOR_STYLE_BLOCK, mTerminal.getCursorStyle());
|
||||
enterString("\033[6 q");
|
||||
assertEquals(TerminalEmulator.CURSOR_STYLE_BAR, mTerminal.getCursorStyle());
|
||||
enterString("\033[4 q");
|
||||
assertEquals(TerminalEmulator.CURSOR_STYLE_UNDERLINE, mTerminal.getCursorStyle());
|
||||
enterString("\033[1 q");
|
||||
assertEquals(TerminalEmulator.CURSOR_STYLE_BLOCK, mTerminal.getCursorStyle());
|
||||
enterString("\033[4 q");
|
||||
assertEquals(TerminalEmulator.CURSOR_STYLE_UNDERLINE, mTerminal.getCursorStyle());
|
||||
enterString("\033[2 q");
|
||||
assertEquals(TerminalEmulator.CURSOR_STYLE_BLOCK, mTerminal.getCursorStyle());
|
||||
}
|
||||
|
||||
public void testPaste() {
|
||||
withTerminalSized(5, 5);
|
||||
mTerminal.paste("hi");
|
||||
assertEquals("hi", mOutput.getOutputAndClear());
|
||||
|
||||
enterString("\033[?2004h");
|
||||
mTerminal.paste("hi");
|
||||
assertEquals("\033[200~" + "hi" + "\033[201~", mOutput.getOutputAndClear());
|
||||
|
||||
enterString("\033[?2004l");
|
||||
mTerminal.paste("hi");
|
||||
assertEquals("hi", mOutput.getOutputAndClear());
|
||||
}
|
||||
|
||||
public void testSelectGraphics() {
|
||||
withTerminalSized(5, 5);
|
||||
enterString("\033[31m");
|
||||
assertEquals(mTerminal.mForeColor, 1);
|
||||
enterString("\033[32m");
|
||||
assertEquals(mTerminal.mForeColor, 2);
|
||||
enterString("\033[43m");
|
||||
assertEquals(2, mTerminal.mForeColor);
|
||||
assertEquals(3, mTerminal.mBackColor);
|
||||
|
||||
// SGR 0 should reset both foreground and background color.
|
||||
enterString("\033[0m");
|
||||
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
|
||||
assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
|
||||
|
||||
// 256 colors:
|
||||
enterString("\033[38;5;119m");
|
||||
assertEquals(119, mTerminal.mForeColor);
|
||||
assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
|
||||
enterString("\033[48;5;129m");
|
||||
assertEquals(119, mTerminal.mForeColor);
|
||||
assertEquals(129, mTerminal.mBackColor);
|
||||
|
||||
// Invalid parameter:
|
||||
enterString("\033[48;8;129m");
|
||||
assertEquals(119, mTerminal.mForeColor);
|
||||
assertEquals(129, mTerminal.mBackColor);
|
||||
|
||||
// Multiple parameters at once:
|
||||
enterString("\033[38;5;178;48;5;179;m");
|
||||
assertEquals(178, mTerminal.mForeColor);
|
||||
assertEquals(179, mTerminal.mBackColor);
|
||||
|
||||
// 24 bit colors:
|
||||
enterString(("\033[0m")); // Reset fg and bg colors.
|
||||
enterString("\033[38;2;255;127;2m");
|
||||
int expectedForeground = 0xff000000 | (255 << 16) | (127 << 8) | 2;
|
||||
assertEquals(expectedForeground, mTerminal.mForeColor);
|
||||
assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
|
||||
enterString("\033[48;2;1;2;254m");
|
||||
int expectedBackground = 0xff000000 | (1 << 16) | (2 << 8) | 254;
|
||||
assertEquals(expectedForeground, mTerminal.mForeColor);
|
||||
assertEquals(expectedBackground, mTerminal.mBackColor);
|
||||
|
||||
// 24 bit colors, set fg and bg at once:
|
||||
enterString(("\033[0m")); // Reset fg and bg colors.
|
||||
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
|
||||
assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
|
||||
enterString("\033[38;2;255;127;2;48;2;1;2;254m");
|
||||
assertEquals(expectedForeground, mTerminal.mForeColor);
|
||||
assertEquals(expectedBackground, mTerminal.mBackColor);
|
||||
|
||||
// 24 bit colors, invalid input:
|
||||
enterString("\033[38;2;300;127;2;48;2;1;300;254m");
|
||||
assertEquals(expectedForeground, mTerminal.mForeColor);
|
||||
assertEquals(expectedBackground, mTerminal.mBackColor);
|
||||
}
|
||||
|
||||
public void testBackgroundColorErase() {
|
||||
final int rows = 3;
|
||||
final int cols = 3;
|
||||
withTerminalSized(cols, rows);
|
||||
for (int r = 0; r < rows; r++) {
|
||||
for (int c = 0; c < cols; c++) {
|
||||
long style = getStyleAt(r, c);
|
||||
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, TextStyle.decodeForeColor(style));
|
||||
assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, TextStyle.decodeBackColor(style));
|
||||
}
|
||||
}
|
||||
// Foreground color to 119:
|
||||
enterString("\033[38;5;119m");
|
||||
// Background color to 129:
|
||||
enterString("\033[48;5;129m");
|
||||
// Clear with ED, Erase in Display:
|
||||
enterString("\033[2J");
|
||||
for (int r = 0; r < rows; r++) {
|
||||
for (int c = 0; c < cols; c++) {
|
||||
long style = getStyleAt(r, c);
|
||||
assertEquals(119, TextStyle.decodeForeColor(style));
|
||||
assertEquals(129, TextStyle.decodeBackColor(style));
|
||||
}
|
||||
}
|
||||
// Background color to 139:
|
||||
enterString("\033[48;5;139m");
|
||||
// Insert two blank lines.
|
||||
enterString("\033[2L");
|
||||
for (int r = 0; r < rows; r++) {
|
||||
for (int c = 0; c < cols; c++) {
|
||||
long style = getStyleAt(r, c);
|
||||
assertEquals((r == 0 || r == 1) ? 139 : 129, TextStyle.decodeBackColor(style));
|
||||
}
|
||||
}
|
||||
|
||||
withTerminalSized(cols, rows);
|
||||
// Background color to 129:
|
||||
enterString("\033[48;5;129m");
|
||||
// Erase two characters, filling them with background color:
|
||||
enterString("\033[2X");
|
||||
assertEquals(129, TextStyle.decodeBackColor(getStyleAt(0, 0)));
|
||||
assertEquals(129, TextStyle.decodeBackColor(getStyleAt(0, 1)));
|
||||
assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, TextStyle.decodeBackColor(getStyleAt(0, 2)));
|
||||
}
|
||||
|
||||
public void testParseColor() {
|
||||
assertEquals(0xFF0000FA, TerminalColors.parse("#0000FA"));
|
||||
assertEquals(0xFF000000, TerminalColors.parse("#000000"));
|
||||
assertEquals(0xFF000000, TerminalColors.parse("#000"));
|
||||
assertEquals(0xFF000000, TerminalColors.parse("#000000000"));
|
||||
assertEquals(0xFF53186f, TerminalColors.parse("#53186f"));
|
||||
|
||||
assertEquals(0xFFFF00FF, TerminalColors.parse("rgb:F/0/F"));
|
||||
assertEquals(0xFF0000FA, TerminalColors.parse("rgb:00/00/FA"));
|
||||
assertEquals(0xFF53186f, TerminalColors.parse("rgb:53/18/6f"));
|
||||
|
||||
assertEquals(0, TerminalColors.parse("invalid_0000FA"));
|
||||
assertEquals(0, TerminalColors.parse("#3456"));
|
||||
}
|
||||
|
||||
/** The ncurses library still uses this. */
|
||||
public void testLineDrawing() {
|
||||
// 016 - shift out / G1. 017 - shift in / G0. "ESC ) 0" - use line drawing for G1
|
||||
withTerminalSized(4, 2).enterString("q\033)0q\016q\017q").assertLinesAre("qq─q", " ");
|
||||
// "\0337", saving cursor should save G0, G1 and invoked charset and "ESC 8" should restore.
|
||||
withTerminalSized(4, 2).enterString("\033)0\016qqq\0337\017\0338q").assertLinesAre("────", " ");
|
||||
}
|
||||
|
||||
public void testSoftTerminalReset() {
|
||||
// See http://vt100.net/docs/vt510-rm/DECSTR and https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=650304
|
||||
// "\033[?7l" is DECRST to disable wrap-around, and DECSTR ("\033[!p") should reset it.
|
||||
withTerminalSized(3, 3).enterString("\033[?7lABCD").assertLinesAre("ABD", " ", " ");
|
||||
enterString("\033[!pEF").assertLinesAre("ABE", "F ", " ");
|
||||
}
|
||||
|
||||
public void testBel() {
|
||||
withTerminalSized(3, 3);
|
||||
assertEquals(0, mOutput.bellsRung);
|
||||
enterString("\07");
|
||||
assertEquals(1, mOutput.bellsRung);
|
||||
enterString("hello\07");
|
||||
assertEquals(2, mOutput.bellsRung);
|
||||
enterString("\07hello");
|
||||
assertEquals(3, mOutput.bellsRung);
|
||||
enterString("hello\07world");
|
||||
assertEquals(4, mOutput.bellsRung);
|
||||
}
|
||||
|
||||
public void testAutomargins() throws UnsupportedEncodingException {
|
||||
withTerminalSized(3, 3).enterString("abc").assertLinesAre("abc", " ", " ").assertCursorAt(0, 2);
|
||||
enterString("d").assertLinesAre("abc", "d ", " ").assertCursorAt(1, 1);
|
||||
|
||||
withTerminalSized(3, 3).enterString("abc\r ").assertLinesAre(" bc", " ", " ").assertCursorAt(0, 1);
|
||||
}
|
||||
|
||||
public void testTab() {
|
||||
withTerminalSized(11, 2).enterString("01234567890\r\tXX").assertLinesAre("01234567XX0", " ");
|
||||
withTerminalSized(11, 2).enterString("01234567890\033[44m\r\tXX").assertLinesAre("01234567XX0", " ");
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,310 @@
|
||||
package com.termux.terminal;
|
||||
|
||||
import junit.framework.AssertionFailedError;
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
public abstract class TerminalTestCase extends TestCase {
|
||||
|
||||
public static class MockTerminalOutput extends TerminalOutput {
|
||||
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
public final List<ChangedTitle> titleChanges = new ArrayList<>();
|
||||
public final List<String> clipboardPuts = new ArrayList<>();
|
||||
public int bellsRung = 0;
|
||||
public int colorsChanged = 0;
|
||||
|
||||
@Override
|
||||
public void write(byte[] data, int offset, int count) {
|
||||
baos.write(data, offset, count);
|
||||
}
|
||||
|
||||
public String getOutputAndClear() {
|
||||
try {
|
||||
String result = new String(baos.toByteArray(), "UTF-8");
|
||||
baos.reset();
|
||||
return result;
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void titleChanged(String oldTitle, String newTitle) {
|
||||
titleChanges.add(new ChangedTitle(oldTitle, newTitle));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clipboardText(String text) {
|
||||
clipboardPuts.add(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBell() {
|
||||
bellsRung++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onColorsChanged() {
|
||||
colorsChanged++;
|
||||
}
|
||||
}
|
||||
|
||||
public TerminalEmulator mTerminal;
|
||||
public MockTerminalOutput mOutput;
|
||||
|
||||
public static final class ChangedTitle {
|
||||
final String oldTitle;
|
||||
final String newTitle;
|
||||
|
||||
public ChangedTitle(String oldTitle, String newTitle) {
|
||||
this.oldTitle = oldTitle;
|
||||
this.newTitle = newTitle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof ChangedTitle)) return false;
|
||||
ChangedTitle other = (ChangedTitle) o;
|
||||
return Objects.equals(oldTitle, other.oldTitle) && Objects.equals(newTitle, other.newTitle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(oldTitle, newTitle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ChangedTitle[oldTitle=" + oldTitle + ", newTitle=" + newTitle + "]";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public TerminalTestCase enterString(String s) {
|
||||
byte[] bytes = s.getBytes(StandardCharsets.UTF_8);
|
||||
mTerminal.append(bytes, bytes.length);
|
||||
assertInvariants();
|
||||
return this;
|
||||
}
|
||||
|
||||
public void assertEnteringStringGivesResponse(String input, String expectedResponse) {
|
||||
enterString(input);
|
||||
String response = mOutput.getOutputAndClear();
|
||||
assertEquals(expectedResponse, response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
mOutput = new MockTerminalOutput();
|
||||
}
|
||||
|
||||
protected TerminalTestCase withTerminalSized(int columns, int rows) {
|
||||
mTerminal = new TerminalEmulator(mOutput, columns, rows, rows * 2);
|
||||
return this;
|
||||
}
|
||||
|
||||
public void assertHistoryStartsWith(String... rows) {
|
||||
assertTrue("About to check " + rows.length + " lines, but only " + mTerminal.getScreen().getActiveTranscriptRows() + " in history",
|
||||
mTerminal.getScreen().getActiveTranscriptRows() >= rows.length);
|
||||
for (int i = 0; i < rows.length; i++) {
|
||||
assertLineIs(-i - 1, rows[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class LineWrapper {
|
||||
final TerminalRow mLine;
|
||||
|
||||
public LineWrapper(TerminalRow line) {
|
||||
mLine = line;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return System.identityHashCode(mLine);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
return o instanceof LineWrapper && ((LineWrapper) o).mLine == mLine;
|
||||
}
|
||||
}
|
||||
|
||||
protected TerminalTestCase assertInvariants() {
|
||||
TerminalBuffer screen = mTerminal.getScreen();
|
||||
TerminalRow[] lines = screen.mLines;
|
||||
|
||||
Set<LineWrapper> linesSet = new HashSet<>();
|
||||
for (int i = 0; i < lines.length; i++) {
|
||||
if (lines[i] == null) continue;
|
||||
assertTrue("Line exists at multiple places: " + i, linesSet.add(new LineWrapper(lines[i])));
|
||||
char[] text = lines[i].mText;
|
||||
int usedChars = lines[i].getSpaceUsed();
|
||||
int currentColumn = 0;
|
||||
for (int j = 0; j < usedChars; j++) {
|
||||
char c = text[j];
|
||||
int codePoint;
|
||||
if (Character.isHighSurrogate(c)) {
|
||||
char lowSurrogate = text[++j];
|
||||
assertTrue("High surrogate without following low surrogate", Character.isLowSurrogate(lowSurrogate));
|
||||
codePoint = Character.toCodePoint(c, lowSurrogate);
|
||||
} else {
|
||||
assertFalse("Low surrogate without preceding high surrogate", Character.isLowSurrogate(c));
|
||||
codePoint = c;
|
||||
}
|
||||
assertFalse("Screen should never contain unassigned characters", Character.getType(codePoint) == Character.UNASSIGNED);
|
||||
int width = WcWidth.width(codePoint);
|
||||
assertFalse("The first column should not start with combining character", currentColumn == 0 && width < 0);
|
||||
if (width > 0) currentColumn += width;
|
||||
}
|
||||
assertEquals("Line whose width does not match screens. line=" + new String(lines[i].mText, 0, lines[i].getSpaceUsed()),
|
||||
screen.mColumns, currentColumn);
|
||||
}
|
||||
|
||||
assertEquals("The alt buffer should have have no history", mTerminal.mAltBuffer.mTotalRows, mTerminal.mAltBuffer.mScreenRows);
|
||||
if (mTerminal.isAlternateBufferActive()) {
|
||||
assertEquals("The alt buffer should be the same size as the screen", mTerminal.mRows, mTerminal.mAltBuffer.mTotalRows);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
protected void assertLineIs(int line, String expected) {
|
||||
TerminalRow l = mTerminal.getScreen().allocateFullLineIfNecessary(mTerminal.getScreen().externalToInternalRow(line));
|
||||
char[] chars = l.mText;
|
||||
int textLen = l.getSpaceUsed();
|
||||
if (textLen != expected.length()) fail("Expected '" + expected + "' (len=" + expected.length() + "), was='"
|
||||
+ new String(chars, 0, textLen) + "' (len=" + textLen + ")");
|
||||
for (int i = 0; i < textLen; i++) {
|
||||
if (expected.charAt(i) != chars[i])
|
||||
fail("Expected '" + expected + "', was='" + new String(chars, 0, textLen) + "' - first different at index=" + i);
|
||||
}
|
||||
}
|
||||
|
||||
public TerminalTestCase assertLinesAre(String... lines) {
|
||||
assertEquals(lines.length, mTerminal.getScreen().mScreenRows);
|
||||
for (int i = 0; i < lines.length; i++)
|
||||
try {
|
||||
assertLineIs(i, lines[i]);
|
||||
} catch (AssertionFailedError e) {
|
||||
throw new AssertionFailedError("Line: " + i + " - " + e.getMessage());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public TerminalTestCase resize(int cols, int rows) {
|
||||
mTerminal.resize(cols, rows);
|
||||
assertInvariants();
|
||||
return this;
|
||||
}
|
||||
|
||||
public TerminalTestCase assertLineWraps(boolean... lines) {
|
||||
for (int i = 0; i < lines.length; i++)
|
||||
assertEquals("line=" + i, lines[i], mTerminal.getScreen().mLines[mTerminal.getScreen().externalToInternalRow(i)].mLineWrap);
|
||||
return this;
|
||||
}
|
||||
|
||||
protected TerminalTestCase assertLineStartsWith(int line, int... codePoints) {
|
||||
char[] chars = mTerminal.getScreen().mLines[mTerminal.getScreen().externalToInternalRow(line)].mText;
|
||||
int charIndex = 0;
|
||||
for (int i = 0; i < codePoints.length; i++) {
|
||||
int lineCodePoint = chars[charIndex++];
|
||||
if (Character.isHighSurrogate((char) lineCodePoint)) {
|
||||
lineCodePoint = Character.toCodePoint((char) lineCodePoint, chars[charIndex++]);
|
||||
}
|
||||
assertEquals("Differing a code point index=" + i, codePoints[i], lineCodePoint);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
protected TerminalTestCase placeCursorAndAssert(int row, int col) {
|
||||
// +1 due to escape sequence being one based.
|
||||
enterString("\033[" + (row + 1) + ";" + (col + 1) + "H");
|
||||
assertCursorAt(row, col);
|
||||
return this;
|
||||
}
|
||||
|
||||
public TerminalTestCase assertCursorAt(int row, int col) {
|
||||
int actualRow = mTerminal.getCursorRow();
|
||||
int actualCol = mTerminal.getCursorCol();
|
||||
if (!(row == actualRow && col == actualCol))
|
||||
fail("Expected cursor at (row,col)=(" + row + ", " + col + ") but was (" + actualRow + ", " + actualCol + ")");
|
||||
return this;
|
||||
}
|
||||
|
||||
/** For testing only. Encoded style according to {@link TextStyle}. */
|
||||
public long getStyleAt(int externalRow, int column) {
|
||||
return mTerminal.getScreen().getStyleAt(externalRow, column);
|
||||
}
|
||||
|
||||
public static class EffectLine {
|
||||
final int[] styles;
|
||||
|
||||
public EffectLine(int[] styles) {
|
||||
this.styles = styles;
|
||||
}
|
||||
}
|
||||
|
||||
protected EffectLine effectLine(int... bits) {
|
||||
return new EffectLine(bits);
|
||||
}
|
||||
|
||||
public TerminalTestCase assertEffectAttributesSet(EffectLine... lines) {
|
||||
assertEquals(lines.length, mTerminal.getScreen().mScreenRows);
|
||||
for (int i = 0; i < lines.length; i++) {
|
||||
int[] line = lines[i].styles;
|
||||
for (int j = 0; j < line.length; j++) {
|
||||
int effectsAtCell = TextStyle.decodeEffect(getStyleAt(i, j));
|
||||
int attributes = line[j];
|
||||
if ((effectsAtCell & attributes) != attributes) fail("Line=" + i + ", column=" + j + ", expected "
|
||||
+ describeStyle(attributes) + " set, was " + describeStyle(effectsAtCell));
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public TerminalTestCase assertForegroundIndices(EffectLine... lines) {
|
||||
assertEquals(lines.length, mTerminal.getScreen().mScreenRows);
|
||||
for (int i = 0; i < lines.length; i++) {
|
||||
int[] line = lines[i].styles;
|
||||
for (int j = 0; j < line.length; j++) {
|
||||
int actualColor = TextStyle.decodeForeColor(getStyleAt(i, j));
|
||||
int expectedColor = line[j];
|
||||
if (actualColor != expectedColor) fail("Line=" + i + ", column=" + j + ", expected color "
|
||||
+ Integer.toHexString(expectedColor) + " set, was " + Integer.toHexString(actualColor));
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private static String describeStyle(int styleBits) {
|
||||
return "'" + ((styleBits & TextStyle.CHARACTER_ATTRIBUTE_BLINK) != 0 ? ":BLINK:" : "")
|
||||
+ ((styleBits & TextStyle.CHARACTER_ATTRIBUTE_BOLD) != 0 ? ":BOLD:" : "")
|
||||
+ ((styleBits & TextStyle.CHARACTER_ATTRIBUTE_INVERSE) != 0 ? ":INVERSE:" : "")
|
||||
+ ((styleBits & TextStyle.CHARACTER_ATTRIBUTE_INVISIBLE) != 0 ? ":INVISIBLE:" : "")
|
||||
+ ((styleBits & TextStyle.CHARACTER_ATTRIBUTE_ITALIC) != 0 ? ":ITALIC:" : "")
|
||||
+ ((styleBits & TextStyle.CHARACTER_ATTRIBUTE_PROTECTED) != 0 ? ":PROTECTED:" : "")
|
||||
+ ((styleBits & TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH) != 0 ? ":STRIKETHROUGH:" : "")
|
||||
+ ((styleBits & TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE) != 0 ? ":UNDERLINE:" : "") + "'";
|
||||
}
|
||||
|
||||
public void assertForegroundColorAt(int externalRow, int column, int color) {
|
||||
long style = mTerminal.getScreen().mLines[mTerminal.getScreen().externalToInternalRow(externalRow)].getStyle(column);
|
||||
assertEquals(color, TextStyle.decodeForeColor(style));
|
||||
}
|
||||
|
||||
public TerminalTestCase assertColor(int colorIndex, int expected) {
|
||||
int actual = mTerminal.mColors.mCurrentColors[colorIndex];
|
||||
if (expected != actual) {
|
||||
fail("Color index=" + colorIndex + ", expected=" + Integer.toHexString(expected) + ", was=" + Integer.toHexString(actual));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
@@ -0,0 +1,65 @@
|
||||
package com.termux.terminal;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
public class TextStyleTest extends TestCase {
|
||||
|
||||
private static final int[] ALL_EFFECTS = new int[]{0, TextStyle.CHARACTER_ATTRIBUTE_BOLD, TextStyle.CHARACTER_ATTRIBUTE_ITALIC,
|
||||
TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE, TextStyle.CHARACTER_ATTRIBUTE_BLINK, TextStyle.CHARACTER_ATTRIBUTE_INVERSE,
|
||||
TextStyle.CHARACTER_ATTRIBUTE_INVISIBLE, TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH, TextStyle.CHARACTER_ATTRIBUTE_PROTECTED,
|
||||
TextStyle.CHARACTER_ATTRIBUTE_DIM};
|
||||
|
||||
public void testEncodingSingle() {
|
||||
for (int fx : ALL_EFFECTS) {
|
||||
for (int fg = 0; fg < TextStyle.NUM_INDEXED_COLORS; fg++) {
|
||||
for (int bg = 0; bg < TextStyle.NUM_INDEXED_COLORS; bg++) {
|
||||
long encoded = TextStyle.encode(fg, bg, fx);
|
||||
assertEquals(fg, TextStyle.decodeForeColor(encoded));
|
||||
assertEquals(bg, TextStyle.decodeBackColor(encoded));
|
||||
assertEquals(fx, TextStyle.decodeEffect(encoded));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void testEncoding24Bit() {
|
||||
int[] values = {255, 240, 127, 1, 0};
|
||||
for (int red : values) {
|
||||
for (int green : values) {
|
||||
for (int blue : values) {
|
||||
int argb = 0xFF000000 | (red << 16) | (green << 8) | blue;
|
||||
long encoded = TextStyle.encode(argb, 0, 0);
|
||||
assertEquals(argb, TextStyle.decodeForeColor(encoded));
|
||||
encoded = TextStyle.encode(0, argb, 0);
|
||||
assertEquals(argb, TextStyle.decodeBackColor(encoded));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void testEncodingCombinations() {
|
||||
for (int f1 : ALL_EFFECTS) {
|
||||
for (int f2 : ALL_EFFECTS) {
|
||||
int combined = f1 | f2;
|
||||
assertEquals(combined, TextStyle.decodeEffect(TextStyle.encode(0, 0, combined)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void testEncodingStrikeThrough() {
|
||||
long encoded = TextStyle.encode(TextStyle.COLOR_INDEX_FOREGROUND, TextStyle.COLOR_INDEX_BACKGROUND,
|
||||
TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH);
|
||||
assertTrue((TextStyle.decodeEffect(encoded) & TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH) != 0);
|
||||
}
|
||||
|
||||
public void testEncodingProtected() {
|
||||
long encoded = TextStyle.encode(TextStyle.COLOR_INDEX_FOREGROUND, TextStyle.COLOR_INDEX_BACKGROUND,
|
||||
TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH);
|
||||
assertTrue((TextStyle.decodeEffect(encoded) & TextStyle.CHARACTER_ATTRIBUTE_PROTECTED) == 0);
|
||||
encoded = TextStyle.encode(TextStyle.COLOR_INDEX_FOREGROUND, TextStyle.COLOR_INDEX_BACKGROUND,
|
||||
TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH | TextStyle.CHARACTER_ATTRIBUTE_PROTECTED);
|
||||
assertTrue((TextStyle.decodeEffect(encoded) & TextStyle.CHARACTER_ATTRIBUTE_PROTECTED) != 0);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,136 @@
|
||||
package com.termux.terminal;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
||||
public class UnicodeInputTest extends TerminalTestCase {
|
||||
|
||||
public void testIllFormedUtf8SuccessorByteNotConsumed() throws Exception {
|
||||
// The Unicode Standard Version 6.2 – Core Specification (http://www.unicode.org/versions/Unicode6.2.0/ch03.pdf):
|
||||
// "If the converter encounters an ill-formed UTF-8 code unit sequence which starts with a valid first byte, but which does not
|
||||
// continue with valid successor bytes (see Table 3-7), it must not consume the successor bytes as part of the ill-formed
|
||||
// subsequence whenever those successor bytes themselves constitute part of a well-formed UTF-8 code unit subsequence."
|
||||
withTerminalSized(5, 5);
|
||||
mTerminal.append(new byte[]{(byte) 0b11101111, (byte) 'a'}, 2);
|
||||
assertLineIs(0, ((char) TerminalEmulator.UNICODE_REPLACEMENT_CHAR) + "a ");
|
||||
|
||||
// https://code.google.com/p/chromium/issues/detail?id=212704
|
||||
byte[] input = new byte[]{
|
||||
(byte) 0x61, (byte) 0xF1,
|
||||
(byte) 0x80, (byte) 0x80,
|
||||
(byte) 0xe1, (byte) 0x80,
|
||||
(byte) 0xc2, (byte) 0x62,
|
||||
(byte) 0x80, (byte) 0x63,
|
||||
(byte) 0x80, (byte) 0xbf,
|
||||
(byte) 0x64
|
||||
};
|
||||
withTerminalSized(10, 2);
|
||||
mTerminal.append(input, input.length);
|
||||
assertLinesAre("a\uFFFD\uFFFD\uFFFDb\uFFFDc\uFFFD\uFFFDd", " ");
|
||||
|
||||
// Surrogate pairs.
|
||||
withTerminalSized(5, 2);
|
||||
input = new byte[]{
|
||||
(byte) 0xed, (byte) 0xa0,
|
||||
(byte) 0x80, (byte) 0xed,
|
||||
(byte) 0xad, (byte) 0xbf,
|
||||
(byte) 0xed, (byte) 0xae,
|
||||
(byte) 0x80, (byte) 0xed,
|
||||
(byte) 0xbf, (byte) 0xbf
|
||||
};
|
||||
mTerminal.append(input, input.length);
|
||||
assertLinesAre("\uFFFD\uFFFD\uFFFD\uFFFD ", " ");
|
||||
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=746900: "with this patch 0xe0 0x80 is decoded as two U+FFFDs,
|
||||
// but 0xe0 0xa0 is decoded as a single U+FFFD, and this is correct according to the "Best Practices", but IE
|
||||
// and Chrome (Version 22.0.1229.94) decode both of them as two U+FFFDs. Opera 12.11 decodes both of them as
|
||||
// one U+FFFD".
|
||||
withTerminalSized(5, 2);
|
||||
input = new byte[]{(byte) 0xe0, (byte) 0xa0, ' '};
|
||||
mTerminal.append(input, input.length);
|
||||
assertLinesAre("\uFFFD ", " ");
|
||||
|
||||
// withTerminalSized(5, 2);
|
||||
// input = new byte[]{(byte) 0xe0, (byte) 0x80, 'a'};
|
||||
// mTerminal.append(input, input.length);
|
||||
// assertLinesAre("\uFFFD\uFFFDa ", " ");
|
||||
}
|
||||
|
||||
public void testUnassignedCodePoint() throws UnsupportedEncodingException {
|
||||
withTerminalSized(3, 3);
|
||||
// UTF-8 for U+C2541, an unassigned code point:
|
||||
byte[] b = new byte[]{(byte) 0xf3, (byte) 0x82, (byte) 0x95, (byte) 0x81};
|
||||
mTerminal.append(b, b.length);
|
||||
enterString("Y");
|
||||
assertEquals(1, Character.charCount(TerminalEmulator.UNICODE_REPLACEMENT_CHAR));
|
||||
assertLineStartsWith(0, TerminalEmulator.UNICODE_REPLACEMENT_CHAR, (int) 'Y', ' ');
|
||||
}
|
||||
|
||||
public void testStuff() {
|
||||
withTerminalSized(80, 24);
|
||||
byte[] b = new byte[]{(byte) 0xf3, (byte) 0x82, (byte) 0x95, (byte) 0x81, (byte) 0x61, (byte) 0x38, (byte) 0xe7, (byte) 0x8f,
|
||||
(byte) 0xae, (byte) 0xc2, (byte) 0x9f, (byte) 0xe8, (byte) 0xa0, (byte) 0x9f, (byte) 0xe8, (byte) 0x8c, (byte) 0xa4,
|
||||
(byte) 0xed, (byte) 0x93, (byte) 0x89, (byte) 0xef, (byte) 0xbf, (byte) 0xbd, (byte) 0x42, (byte) 0xc2, (byte) 0x9b,
|
||||
(byte) 0xe6, (byte) 0x87, (byte) 0x89, (byte) 0x5a};
|
||||
mTerminal.append(b, b.length);
|
||||
}
|
||||
|
||||
public void testSimpleCombining() throws Exception {
|
||||
withTerminalSized(3, 2).enterString(" a\u0302 ").assertLinesAre(" a\u0302 ", " ");
|
||||
}
|
||||
|
||||
public void testCombiningCharacterInFirstColumn() throws Exception {
|
||||
withTerminalSized(5, 3).enterString("test\r\nhi\r\n").assertLinesAre("test ", "hi ", " ");
|
||||
|
||||
// U+0302 is COMBINING CIRCUMFLEX ACCENT. Test case from mosh (http://mosh.mit.edu/).
|
||||
withTerminalSized(5, 5).enterString("test\r\nabc\r\n\u0302\r\ndef\r\n");
|
||||
assertLinesAre("test ", "abc ", " \u0302 ", "def ", " ");
|
||||
}
|
||||
|
||||
public void testCombiningCharacterInLastColumn() throws Exception {
|
||||
withTerminalSized(3, 2).enterString(" a\u0302").assertLinesAre(" a\u0302", " ");
|
||||
withTerminalSized(3, 2).enterString(" à̲").assertLinesAre(" à̲", " ");
|
||||
withTerminalSized(3, 2).enterString("Aà̲F").assertLinesAre("Aà̲F", " ");
|
||||
}
|
||||
|
||||
public void testWideCharacterInLastColumn() throws Exception {
|
||||
withTerminalSized(3, 2).enterString(" 枝\u0302").assertLinesAre(" ", "枝\u0302 ");
|
||||
|
||||
withTerminalSized(3, 2).enterString(" 枝").assertLinesAre(" 枝", " ").assertCursorAt(0, 2);
|
||||
enterString("a").assertLinesAre(" 枝", "a ");
|
||||
}
|
||||
|
||||
public void testWideCharacterDeletion() throws Exception {
|
||||
// CSI Ps D Cursor Backward Ps Times
|
||||
withTerminalSized(3, 2).enterString("枝\033[Da").assertLinesAre(" a ", " ");
|
||||
withTerminalSized(3, 2).enterString("枝\033[2Da").assertLinesAre("a ", " ");
|
||||
withTerminalSized(3, 2).enterString("枝\033[2D枝").assertLinesAre("枝 ", " ");
|
||||
withTerminalSized(3, 2).enterString("枝\033[1D枝").assertLinesAre(" 枝", " ");
|
||||
withTerminalSized(5, 2).enterString(" 枝 \033[Da").assertLinesAre(" 枝a ", " ");
|
||||
withTerminalSized(5, 2).enterString("a \033[D\u0302").assertLinesAre("a\u0302 ", " ");
|
||||
withTerminalSized(5, 2).enterString("枝 \033[D\u0302").assertLinesAre("枝\u0302 ", " ");
|
||||
enterString("Z").assertLinesAre("枝\u0302Z ", " ");
|
||||
enterString("\033[D ").assertLinesAre("枝\u0302 ", " ");
|
||||
// Go back two columns, standing at the second half of the wide character:
|
||||
enterString("\033[2DU").assertLinesAre(" U ", " ");
|
||||
}
|
||||
|
||||
public void testWideCharOverwriting() {
|
||||
withTerminalSized(3, 2).enterString("abc\033[3D枝").assertLinesAre("枝c", " ");
|
||||
}
|
||||
|
||||
public void testOverlongUtf8Encoding() throws Exception {
|
||||
// U+0020 should be encoded as 0x20, 0xc0 0xa0 is an overlong encoding
|
||||
// so should be replaced with the replacement char U+FFFD.
|
||||
withTerminalSized(5, 5).mTerminal.append(new byte[]{(byte) 0xc0, (byte) 0xa0, 'Y'}, 3);
|
||||
assertLineIs(0, "\uFFFDY ");
|
||||
}
|
||||
|
||||
public void testWideCharacterWithoutWrapping() throws Exception {
|
||||
// With wraparound disabled. The behaviour when a wide character is output with cursor in
|
||||
// the last column when autowrap is disabled is not obvious, but we expect the wide
|
||||
// character to be ignored here.
|
||||
withTerminalSized(3, 3).enterString("\033[?7l").enterString("枝枝枝").assertLinesAre("枝 ", " ", " ");
|
||||
enterString("a枝").assertLinesAre("枝a", " ", " ");
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,85 @@
|
||||
package com.termux.terminal;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
public class WcWidthTest extends TestCase {
|
||||
|
||||
private static void assertWidthIs(int expectedWidth, int codePoint) {
|
||||
int wcWidth = WcWidth.width(codePoint);
|
||||
assertEquals(expectedWidth, wcWidth);
|
||||
}
|
||||
|
||||
public void testPrintableAscii() {
|
||||
for (int i = 0x20; i <= 0x7E; i++) {
|
||||
assertWidthIs(1, i);
|
||||
}
|
||||
}
|
||||
|
||||
public void testSomeWidthOne() {
|
||||
assertWidthIs(1, 'å');
|
||||
assertWidthIs(1, 'ä');
|
||||
assertWidthIs(1, 'ö');
|
||||
assertWidthIs(1, 0x23F2);
|
||||
}
|
||||
|
||||
public void testSomeWide() {
|
||||
assertWidthIs(2, 'A');
|
||||
assertWidthIs(2, 'B');
|
||||
assertWidthIs(2, 'C');
|
||||
assertWidthIs(2, '中');
|
||||
assertWidthIs(2, '文');
|
||||
|
||||
assertWidthIs(2, 0x679C);
|
||||
assertWidthIs(2, 0x679D);
|
||||
|
||||
assertWidthIs(2, 0x2070E);
|
||||
assertWidthIs(2, 0x20731);
|
||||
|
||||
assertWidthIs(1, 0x1F781);
|
||||
}
|
||||
|
||||
public void testSomeNonWide() {
|
||||
assertWidthIs(1, 0x1D11E);
|
||||
assertWidthIs(1, 0x1D11F);
|
||||
}
|
||||
|
||||
public void testCombining() {
|
||||
assertWidthIs(0, 0x0302);
|
||||
assertWidthIs(0, 0x0308);
|
||||
assertWidthIs(0, 0xFE0F);
|
||||
}
|
||||
|
||||
public void testWordJoiner() {
|
||||
// https://en.wikipedia.org/wiki/Word_joiner
|
||||
// The word joiner (WJ) is a code point in Unicode used to separate words when using scripts
|
||||
// that do not use explicit spacing. It is encoded since Unicode version 3.2
|
||||
// (released in 2002) as U+2060 WORD JOINER (HTML ⁠).
|
||||
// The word joiner does not produce any space, and prohibits a line break at its position.
|
||||
assertWidthIs(0, 0x2060);
|
||||
}
|
||||
|
||||
public void testWatch() {
|
||||
|
||||
}
|
||||
|
||||
public void testSofthyphen() {
|
||||
// http://osdir.com/ml/internationalization.linux/2003-05/msg00006.html:
|
||||
// "Existing implementation practice in terminals is that the SOFT HYPHEN is
|
||||
// a spacing graphical character, and the purpose of my wcwidth() was to
|
||||
// predict the advancement of the cursor position after a string is sent to
|
||||
// a terminal. Hence, I have no choice but to keep wcwidth(SOFT HYPHEN) = 1.
|
||||
// VT100-style terminals do not hyphenate."
|
||||
assertWidthIs(1, 0x00AD);
|
||||
}
|
||||
|
||||
public void testHangul() {
|
||||
assertWidthIs(1, 0x11A3);
|
||||
}
|
||||
|
||||
public void testEmojis() {
|
||||
assertWidthIs(2, 0x1F428); // KOALA.
|
||||
assertWidthIs(2, 0x231a); // WATCH.
|
||||
assertWidthIs(2, 0x1F643); // UPSIDE-DOWN FACE (Unicode 8).
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user