Compare commits

...

38 Commits
v0.26 ... v0.32

Author SHA1 Message Date
Fredrik Fornwall
d69485b70b Match less files for file receiving (fixes #66) 2016-03-25 00:22:49 +01:00
Fredrik Fornwall
421dfcca39 Do not fail with NPE when scheme is null
Also remove some debug logging left by mistake.
2016-03-23 18:31:03 +01:00
Fredrik Fornwall
3aaa0ab267 Remove unused imports 2016-03-21 15:24:02 +01:00
Fredrik Fornwall
e7f9647beb Remove unused variable 2016-03-21 15:19:08 +01:00
Fredrik Fornwall
5558f371b4 Bump version to 0.31 2016-03-21 00:01:59 +01:00
Fredrik Fornwall
0882ed6470 Keep the EXTERNAL_STORAGE environment variable
The EXTERNAL_STORAGE environment variable is needed on at least the
Samsung Galaxy S7 for /system/bin/am to function.
2016-03-20 23:57:34 +01:00
Fredrik Fornwall
5c02448521 Install 64-bit arm packages on capable devices
This will only affect new installations. Existing users wishing to
install 64-bit packages can re-install the app completely, or just
'rm -Rf $PREFIX' and exit all sessions, which will cause Termux to
re-install all packages at next startup.
2016-03-20 22:24:05 +01:00
Fredrik Fornwall
17382fb190 Do not have /system/bin in the PATH
By appending the old system PATH environment variable to the paths
setup by Termux system binaries are found as a fallback.

This causes problems with system binaries not working (due to
LD_LIBRARY_PATH) and causing a lot of confusion for new users when
e.g. an Android system provides a system version of e.g. curl, ssh
and other programs. It's better for these users to be prompted to
install the proper Termux package, and advanced users can still
add /system/bin to the PATH themselves.

Certain programs such as 'am' and 'pm' are already setup in
$PREFIX/bin to clear LD_LIBRARY_PATH and launch the binaries in
/system/bin - if there are some more popular ones they could be
added in the same way.
2016-03-20 22:17:21 +01:00
Fredrik Fornwall
d6eea83bfc Make it possible to receive files
The files are saved to $HOME/downloads/, after which the user
may choose to open the downloads/ folder or edit the file with
the $HOME/bin/termux-file-editor program.

It's also possible to receive URL:s, in which case the
$HOME/bin/termux-url-opener program will be called.
2016-03-19 00:17:38 +01:00
Fredrik Fornwall
51181c2d49 Fix method reference in javadoc 2016-03-17 12:31:45 +01:00
Fredrik Fornwall
480b8a4f7e Recycle a TypedArray after usage
Also add two suppress lint annotations.
2016-03-17 12:29:30 +01:00
Fredrik Fornwall
f989157f10 Extract constants 2016-03-16 23:10:44 +01:00
Fredrik Fornwall
0e942f90a6 Update gradle from 2.10 to 2.12 2016-03-15 00:26:36 +01:00
Fredrik Fornwall
5b8eca46a1 Set 4 space indentation in .editorconfig 2016-03-15 00:09:10 +01:00
Fredrik Fornwall
493900d60b Add PATH environemnt variable in failsafe mode 2016-03-15 00:08:31 +01:00
Fredrik Fornwall
c6d6a63637 Extract variable for clarity 2016-03-11 01:20:54 +01:00
Fredrik Fornwall
ca71265f23 Handle backspace across wrapped lines (closes #59) 2016-03-07 23:45:02 +01:00
Fredrik Fornwall
46c9c4b80e Catch IllegalArgumentException from startActivity 2016-03-01 16:33:22 +01:00
Fredrik Fornwall
6ca055bb25 Fix tabs to not overwrite cells 2016-02-25 16:33:00 +01:00
Fredrik Fornwall
ce7ad530cd TermuxFilePickerProvider: Small improvements
1. Return true from onCreate().
2. Implement getType().
2016-02-14 00:49:27 +01:00
Fredrik Fornwall
d0015cbe82 Bump version to 0.29 2016-02-14 00:45:53 +01:00
Fredrik Fornwall
9e19217f8f Force refresh when returning in onStart()
This makes sure that terminal session changes that has happened
while away are visible when returning.
2016-02-14 00:44:33 +01:00
Fredrik Fornwall
048af64093 Map everything starting with x86 to i686
This fixes CPU detection for ARC welder which reports x86.
2016-02-14 00:42:34 +01:00
Fredrik Fornwall
a8f7bf1b6e Clarify how to find the Help menu entry 2016-02-13 22:01:18 +01:00
Fredrik Fornwall
62e229e184 Fix LOG_KEY_EVENTS=true committed by mistake 2016-02-13 00:27:58 +01:00
Fredrik Fornwall
36e4d94093 Add *.so to .gitignore 2016-02-09 11:33:57 +01:00
Fredrik Fornwall
d2c9c5a0f0 Bump version to 0.28 2016-02-09 11:25:58 +01:00
Fredrik Fornwall
6405180cb8 Wait for terminal size before starting process
This fixes https://github.com/termux/termux-widget/issues/2, which
was caused by the terminal launching the terminal session process
before the terminal size was known.

Also remove the built JNI libraries from source control.
2016-02-09 11:24:05 +01:00
Fredrik Fornwall
1b6919bb23 Add test comment 2016-01-28 16:45:45 +01:00
Fredrik Fornwall
e52cd2dd41 Bump version to 0.27 2016-01-21 11:53:37 +01:00
Fredrik Fornwall
54857d5fd4 Replace surrogate chars with U+FFFD
Also add some more unicode input tests.
2016-01-19 23:11:20 +01:00
Fredrik Fornwall
38dd99e827 Add wcwidth test for U+2060 2016-01-19 23:08:28 +01:00
Fredrik Fornwall
7256b04317 Clear autowrap bit at some escape sequences
Add test adapted from chromiums hterm.
2016-01-19 17:24:18 +01:00
Fredrik Fornwall
01a1c6de0f Change default behaviour of back key to back
It's still possible to set it to escape using configuration
2016-01-19 17:22:57 +01:00
Fredrik Fornwall
497fc3ecd0 Add VolumeUp+V to show volume control 2016-01-19 17:21:27 +01:00
Fredrik Fornwall
b2b39abacd Recognize '\033c' - RIS, reset terminal state 2016-01-19 12:05:38 +01:00
Fredrik Fornwall
bee305e53f Whitespace/tabs consistency in AndroidManifest.xml 2016-01-19 12:01:35 +01:00
Fredrik Fornwall
c8d2f28ed8 Terminal emulation: Test "CSI X"/ECH processing 2016-01-18 15:15:34 +01:00
29 changed files with 608 additions and 140 deletions

View File

@@ -12,3 +12,5 @@ root = true
end_of_line = lf
insert_final_newline = true
charset = utf-8
indent_style = space
indent_size = 4

1
.gitignore vendored
View File

@@ -5,6 +5,7 @@
# Built application files
build/
*.apk
*.so
# Crashlytics configuations
com_crashlytics_export_strings.xml

View File

@@ -20,7 +20,7 @@ Released under [the GPLv3 license](https://www.gnu.org/licenses/gpl.html). Conta
Building JNI libraries
======================
For ease of use, the JNI libraries are checked into version control. Execute the `build-jnilibs.sh` script to rebuild them.
Execute the `build-jnilibs.sh` script to build the required JNI libraries.
Terminal resources
==================

View File

@@ -18,8 +18,8 @@ android {
applicationId "com.termux"
minSdkVersion 21
targetSdkVersion 23
versionCode 26
versionName "0.26"
versionCode 32
versionName "0.32"
}
buildTypes {

View File

@@ -4,8 +4,8 @@
android:sharedUserId="com.termux"
android:sharedUserLabel="@string/shared_user_label" >
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
<uses-feature android:name="android.software.leanback" android:required="false" />
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
<uses-feature android:name="android.software.leanback" android:required="false" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
@@ -30,10 +30,10 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
<activity
@@ -43,6 +43,26 @@
android:parentActivityName=".app.TermuxActivity"
android:label="@string/application_name" />
<activity
android:name="com.termux.filepicker.TermuxFileReceiverActivity"
android:label="@string/application_name"
android:taskAffinity="com.termux.filereceiver"
android:excludeFromRecents="true"
android:noHistory="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/*" />
<data android:mimeType="audio/*" />
<data android:mimeType="image/*" />
<data android:mimeType="message/*" />
<data android:mimeType="multipart/*" />
<data android:mimeType="text/*" />
<data android:mimeType="video/*" />
</intent-filter>
</activity>
<activity
android:name="com.termux.filepicker.TermuxFilePickerActivity"
android:label="@string/application_name"

View File

@@ -3,24 +3,25 @@ package com.termux.app;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.res.Configuration;
import android.text.Selection;
import android.util.TypedValue;
import android.view.KeyEvent;
import android.view.ViewGroup.LayoutParams;
import android.view.WindowManager;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
final class DialogUtils {
public final class DialogUtils {
public interface TextSetListener {
void onTextSet(String text);
}
static void textInput(Activity activity, int titleText, int positiveButtonText, String initialText, final TextSetListener onPositive,
int neutralButtonText, final TextSetListener onNeutral) {
public static void textInput(Activity activity, int titleText, String initialText,
int positiveButtonText, final TextSetListener onPositive,
int neutralButtonText, final TextSetListener onNeutral,
int negativeButtonText, final TextSetListener onNegative,
final DialogInterface.OnDismissListener onDismiss) {
final EditText input = new EditText(activity);
input.setSingleLine();
if (initialText != null) {
@@ -57,23 +58,32 @@ final class DialogUtils {
public void onClick(DialogInterface d, int whichButton) {
onPositive.onTextSet(input.getText().toString());
}
})
.setNegativeButton(android.R.string.cancel, null);
if (onNeutral != null) {
builder.setNeutralButton(neutralButtonText, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
onNeutral.onTextSet(input.getText().toString());
}
});
}
if (onNeutral != null) {
builder.setNeutralButton(neutralButtonText, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
onNeutral.onTextSet(input.getText().toString());
}
});
}
if (onNegative == null) {
builder.setNegativeButton(android.R.string.cancel, null);
} else {
builder.setNegativeButton(negativeButtonText, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
onNegative.onTextSet(input.getText().toString());
}
});
}
if (onDismiss != null) builder.setOnDismissListener(onDismiss);
dialogHolder[0] = builder.create();
if ((activity.getResources().getConfiguration().hardKeyboardHidden & Configuration.HARDKEYBOARDHIDDEN_YES) == 0) {
// Show soft keyboard unless hardware keyboard available.
dialogHolder[0].getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
}
dialogHolder[0].setCanceledOnTouchOutside(false);
dialogHolder[0].show();
}

View File

@@ -287,28 +287,27 @@ public final class TermuxActivity extends Activity implements ServiceConnection
}
});
newSessionButton.setOnLongClickListener(new OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
Resources res = getResources();
DialogUtils.textInput(TermuxActivity.this, R.string.session_new_named_title, R.string.session_new_named_positive_button, null,
new DialogUtils.TextSetListener() {
@Override
public void onTextSet(String text) {
addNewSession(false, text);
}
}, R.string.new_session_failsafe, new DialogUtils.TextSetListener() {
@Override
public void onTextSet(String text) {
addNewSession(true, text);
}
}
);
return true;
}
});
newSessionButton.setOnLongClickListener(new OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
DialogUtils.textInput(TermuxActivity.this, R.string.session_new_named_title, null, R.string.session_new_named_positive_button,
new DialogUtils.TextSetListener() {
@Override
public void onTextSet(String text) {
addNewSession(false, text);
}
}, R.string.new_session_failsafe, new DialogUtils.TextSetListener() {
@Override
public void onTextSet(String text) {
addNewSession(true, text);
}
}
, -1, null, null);
return true;
}
});
findViewById(R.id.toggle_keyboard_button).setOnClickListener(new OnClickListener() {
findViewById(R.id.toggle_keyboard_button).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
@@ -492,14 +491,13 @@ public final class TermuxActivity extends Activity implements ServiceConnection
@SuppressLint("InflateParams")
void renameSession(final TerminalSession sessionToRename) {
DialogUtils.textInput(this, R.string.session_rename_title, R.string.session_rename_positive_button, sessionToRename.mSessionName,
new DialogUtils.TextSetListener() {
@Override
public void onTextSet(String text) {
sessionToRename.mSessionName = text;
}
}, -1, null);
}
DialogUtils.textInput(this, R.string.session_rename_title, sessionToRename.mSessionName, R.string.session_rename_positive_button, new DialogUtils.TextSetListener() {
@Override
public void onTextSet(String text) {
sessionToRename.mSessionName = text;
}
}, -1, null, -1, null, null);
}
@Override
public void onServiceDisconnected(ComponentName name) {
@@ -525,6 +523,10 @@ public final class TermuxActivity extends Activity implements ServiceConnection
}
registerReceiver(mBroadcastReceiever, new IntentFilter(RELOAD_STYLE_ACTION));
// The current terminal session may have changed while being away, force
// a refresh of the displayed terminal:
mTerminalView.onScreenUpdated();
}
@Override
@@ -727,21 +729,23 @@ public final class TermuxActivity extends Activity implements ServiceConnection
}
return true;
}
case CONTEXTMENU_STYLING_ID: {
Intent stylingIntent = new Intent();
stylingIntent.setClassName("com.termux.styling", "com.termux.styling.TermuxStyleActivity");
try {
startActivity(stylingIntent);
} catch (ActivityNotFoundException e) {
new AlertDialog.Builder(this).setMessage(R.string.styling_not_installed)
.setPositiveButton(R.string.styling_install, new android.content.DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://play.google.com/store/apps/details?id=com.termux.styling")));
}
}).setNegativeButton(android.R.string.cancel, null).show();
}
}
case CONTEXTMENU_STYLING_ID: {
Intent stylingIntent = new Intent();
stylingIntent.setClassName("com.termux.styling", "com.termux.styling.TermuxStyleActivity");
try {
startActivity(stylingIntent);
} catch (ActivityNotFoundException | IllegalArgumentException e) {
// The startActivity() call is not documented to throw IllegalArgumentException.
// However, crash reporting shows that it sometimes does, so catch it here.
new AlertDialog.Builder(this).setMessage(R.string.styling_not_installed)
.setPositiveButton(R.string.styling_install, new android.content.DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://play.google.com/store/apps/details?id=com.termux.styling")));
}
}).setNegativeButton(android.R.string.cancel, null).show();
}
}
return true;
case CONTEXTMENU_TOGGLE_FULLSCREEN_ID:
toggleImmersive();

View File

@@ -176,17 +176,19 @@ final class TermuxInstaller {
}.start();
}
/** Get bootstrap zip url for this systems cpu architecture. */
static URL determineZipUrl() throws MalformedURLException {
String arch = System.getProperty("os.arch");
if (arch.startsWith("arm") || arch.equals("aarch64")) {
// Handle different arm variants such as armv7l:
arch = "arm";
} else if (arch.equals("x86_64")) {
arch = "i686";
}
return new URL("https://termux.net/bootstrap/bootstrap-" + arch + ".zip");
}
/** Get bootstrap zip url for this systems cpu architecture. */
static URL determineZipUrl() throws MalformedURLException {
String arch = System.getProperty("os.arch");
if (arch.startsWith("armv8")) {
arch = "aarch64";
} else if (arch.startsWith("arm")) {
// Handle different arm variants such as armv7l:
arch = "arm";
} else if (arch.startsWith("x86")) { // "x86" on arcwelder, "x86_64" on 64-bit android.
arch = "i686";
}
return new URL("https://termux.net/bootstrap/bootstrap-" + arch + ".zip");
}
/** Delete a folder and all its content or throw. */
static void deleteFolder(File fileOrDirectory) {

View File

@@ -132,7 +132,7 @@ final class TermuxPreferences {
break;
}
mBackIsEscape = "escape".equals(props.getProperty("back-key", "escape"));
mBackIsEscape = "escape".equals(props.getProperty("back-key", "back"));
} catch (Exception e) {
Toast.makeText(context, "Error loading properties: " + e.getMessage(), Toast.LENGTH_LONG).show();
Log.e("termux", "Error loading props", e);

View File

@@ -55,7 +55,11 @@ public final class TermuxService extends Service implements SessionChangedCallba
/** Intent action to toggle the wifi lock, {@link #mWifiLock}, which this service may hold. */
private static final String ACTION_LOCK_WIFI = "com.termux.service_toggle_wifi_lock";
/** Intent action to launch a new terminal session. Executed from TermuxWidgetProvider. */
private static final String ACTION_EXECUTE = "com.termux.service_execute";
public static final String ACTION_EXECUTE = "com.termux.service_execute";
public static final String EXTRA_ARGUMENTS = "com.termux.execute.arguments";
public static final String EXTRA_CURRENT_WORKING_DIRECTORY = "com.termux.execute.cwd";
/** This service is only bound from inside the same process and never uses IPC. */
class LocalBinder extends Binder {
@@ -113,8 +117,8 @@ public final class TermuxService extends Service implements SessionChangedCallba
} else if (ACTION_EXECUTE.equals(action)) {
Uri executableUri = intent.getData();
String executablePath = (executableUri == null ? null : executableUri.getPath());
String[] arguments = (executableUri == null ? null : intent.getStringArrayExtra("com.termux.execute.arguments"));
String cwd = intent.getStringExtra("com.termux.execute.cwd");
String[] arguments = (executableUri == null ? null : intent.getStringArrayExtra(EXTRA_ARGUMENTS));
String cwd = intent.getStringExtra(EXTRA_CURRENT_WORKING_DIRECTORY);
TerminalSession newSession = createTermSession(executablePath, arguments, cwd, false);
// Transform executable path to session name, e.g. "/bin/do-something.sh" => "do something.sh".
@@ -237,17 +241,22 @@ public final class TermuxService extends Service implements SessionChangedCallba
final String prefixEnv = "PREFIX=" + PREFIX_PATH;
final String androidRootEnv = "ANDROID_ROOT=" + System.getenv("ANDROID_ROOT");
final String androidDataEnv = "ANDROID_DATA=" + System.getenv("ANDROID_DATA");
// EXTERNAL_STORAGE is needed for /system/bin/am to work on at least
// Samsung S7 - see https://plus.google.com/110070148244138185604/posts/gp8Lk3aCGp3.
final String externalStorageEnv = "EXTERNAL_STORAGE=" + System.getenv("EXTERNAL_STORAGE");
String[] env;
if (failSafe) {
env = new String[] { termEnv, homeEnv, prefixEnv, androidRootEnv, androidDataEnv };
// Keep the default path so that system binaries can be used in the failsafe session.
final String pathEnv = "PATH=" + System.getenv("PATH");
env = new String[] { termEnv, homeEnv, prefixEnv, androidRootEnv, androidDataEnv, pathEnv, externalStorageEnv };
} else {
final String ps1Env = "PS1=$ ";
final String ldEnv = "LD_LIBRARY_PATH=" + PREFIX_PATH + "/lib";
final String langEnv = "LANG=en_US.UTF-8";
final String pathEnv = "PATH=" + PREFIX_PATH + "/bin:" + PREFIX_PATH + "/bin/applets:" + System.getenv("PATH");
final String pathEnv = "PATH=" + PREFIX_PATH + "/bin:" + PREFIX_PATH + "/bin/applets";
final String pwdEnv = "PWD=" + cwd;
env = new String[] { termEnv, homeEnv, prefixEnv, ps1Env, ldEnv, langEnv, pathEnv, pwdEnv, androidRootEnv, androidDataEnv };
env = new String[] { termEnv, homeEnv, prefixEnv, ps1Env, ldEnv, langEnv, pathEnv, pwdEnv, androidRootEnv, androidDataEnv, externalStorageEnv };
}
String shellName;

View File

@@ -7,6 +7,7 @@ import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.support.annotation.NonNull;
import android.webkit.MimeTypeMap;
import java.io.File;
import java.io.FileNotFoundException;
@@ -15,7 +16,7 @@ import java.io.FileNotFoundException;
public class TermuxFilePickerProvider extends ContentProvider {
@Override
public boolean onCreate() {
return false;
return true;
}
@Override
@@ -25,7 +26,20 @@ public class TermuxFilePickerProvider extends ContentProvider {
@Override
public String getType(@NonNull Uri uri) {
return null;
String contentType = null;
String path = uri.getPath();
int lastDotIndex = path.lastIndexOf('.');
String possibleFileExtension = path.substring(lastDotIndex + 1, path.length());
if (possibleFileExtension.contains("/")) {
// The dot was in the path, so not a file extension.
} else {
MimeTypeMap mimeTypes = MimeTypeMap.getSingleton();
// Lower casing makes it work with e.g. "JPG":
contentType = mimeTypes.getMimeTypeFromExtension(possibleFileExtension.toLowerCase());
}
if (contentType == null) contentType = "application/octet-stream";
return contentType;
}
@Override

View File

@@ -0,0 +1,215 @@
package com.termux.filepicker;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.provider.OpenableColumns;
import android.util.Log;
import android.util.Patterns;
import com.termux.R;
import com.termux.app.DialogUtils;
import com.termux.app.TermuxService;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
public class TermuxFileReceiverActivity extends Activity {
static final String TERMUX_RECEIVEDIR = TermuxService.FILES_PATH + "/home/downloads";
static final String EDITOR_PROGRAM = TermuxService.HOME_PATH + "/bin/termux-file-editor";
static final String URL_OPENER_PROGRAM = TermuxService.HOME_PATH + "/bin/termux-url-opener";
/**
* If the activity should be finished when the name input dialog is dismissed. This is disabled
* before showing an error dialog, since the act of showing the error dialog will cause the
* name input dialog to be implicitly dismissed, and we do not want to finish the activity directly
* when showing the error dialog.
*/
private boolean mFinishOnDismissNameDialog = true;
@Override
protected void onResume() {
super.onResume();
final Intent intent = getIntent();
final String action = intent.getAction();
final String type = intent.getType();
final String scheme = intent.getScheme();
if (Intent.ACTION_SEND.equals(action) && type != null) {
final String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
final Uri sharedUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
if (sharedText != null) {
if (Patterns.WEB_URL.matcher(sharedText).matches()) {
handleUrlAndFinish(sharedText);
} else {
String subject = intent.getStringExtra(Intent.EXTRA_SUBJECT);
if (subject == null) subject = intent.getStringExtra(Intent.EXTRA_TITLE);
if (subject != null) subject += ".txt";
promptNameAndSave(new ByteArrayInputStream(sharedText.getBytes(StandardCharsets.UTF_8)), subject);
}
} else if (sharedUri != null) {
handleContentUri(sharedUri, intent.getStringExtra(Intent.EXTRA_TITLE));
} else {
showErrorDialogAndQuit("Send action without content - nothing to save.");
}
} else if ("content".equals(scheme)) {
handleContentUri(intent.getData(), intent.getStringExtra(Intent.EXTRA_TITLE));
} else if ("file".equals(scheme)) {
// When e.g. clicking on a downloaded apk:
String path = intent.getData().getPath();
File file = new File(path);
try {
FileInputStream in = new FileInputStream(file);
promptNameAndSave(in, file.getName());
} catch (FileNotFoundException e) {
showErrorDialogAndQuit("Cannot open file: " + e.getMessage() + ".");
}
} else {
showErrorDialogAndQuit("Unable to receive any file or URL.");
}
}
void showErrorDialogAndQuit(String message) {
mFinishOnDismissNameDialog = false;
new AlertDialog.Builder(this).setMessage(message).setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog) {
finish();
}
}).setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
finish();
}
}).show();
}
void handleContentUri(final Uri uri, String subjectFromIntent) {
try {
String attachmentFileName = null;
String[] projection = new String[]{OpenableColumns.DISPLAY_NAME};
try (Cursor c = getContentResolver().query(uri, projection, null, null, null)) {
if (c != null && c.moveToFirst()) {
final int fileNameColumnId = c.getColumnIndex(OpenableColumns.DISPLAY_NAME);
if (fileNameColumnId >= 0) attachmentFileName = c.getString(fileNameColumnId);
}
}
if (attachmentFileName == null) attachmentFileName = subjectFromIntent;
InputStream in = getContentResolver().openInputStream(uri);
promptNameAndSave(in, attachmentFileName);
} catch (Exception e) {
showErrorDialogAndQuit("Unable to handle shared content:\n\n" + e.getMessage());
Log.e("termux", "handleContentUri(uri=" + uri + ") failed", e);
}
}
void promptNameAndSave(final InputStream in, final String attachmentFileName) {
DialogUtils.textInput(this, R.string.file_received_title, attachmentFileName
, android.R.string.ok, new DialogUtils.TextSetListener() {
@Override
public void onTextSet(final String text) {
if (saveStreamWithName(in, text) == null) return;
finish();
}
}, R.string.file_received_open_folder_button, new DialogUtils.TextSetListener() {
@Override
public void onTextSet(String text) {
if (saveStreamWithName(in, text) == null) return;
Intent executeIntent = new Intent(TermuxService.ACTION_EXECUTE);
executeIntent.putExtra(TermuxService.EXTRA_CURRENT_WORKING_DIRECTORY, TERMUX_RECEIVEDIR);
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
startService(executeIntent);
finish();
}
}, R.string.file_received_edit_button, new DialogUtils.TextSetListener() {
@Override
public void onTextSet(String text) {
File outFile = saveStreamWithName(in, text);
if (outFile == null) return;
final File editorProgramFile = new File(EDITOR_PROGRAM);
if (!editorProgramFile.isFile()) {
showErrorDialogAndQuit("The following file does not exist:\n$HOME/bin/termux-file-editor\n\n"
+ "Create this file as a script or a symlink - it will be called with the received file as only argument.");
return;
}
// Do this for the user if necessary:
editorProgramFile.setExecutable(true);
final Uri scriptUri = new Uri.Builder().scheme("file").path(EDITOR_PROGRAM).build();
Intent executeIntent = new Intent(TermuxService.ACTION_EXECUTE, scriptUri);
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
executeIntent.putExtra(TermuxService.EXTRA_ARGUMENTS, new String[]{outFile.getAbsolutePath()});
startService(executeIntent);
finish();
}
}, new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog) {
if (mFinishOnDismissNameDialog) finish();
}
});
}
public File saveStreamWithName(InputStream in, String attachmentFileName) {
File receiveDir = new File(TERMUX_RECEIVEDIR);
if (!receiveDir.isDirectory() && !receiveDir.mkdirs()) {
showErrorDialogAndQuit("Cannot create directory: " + receiveDir.getAbsolutePath());
return null;
}
try {
final File outFile = new File(receiveDir, attachmentFileName);
try (FileOutputStream f = new FileOutputStream(outFile)) {
byte[] buffer = new byte[4096];
int readBytes;
while ((readBytes = in.read(buffer)) > 0) {
f.write(buffer, 0, readBytes);
}
}
return outFile;
} catch (IOException e) {
showErrorDialogAndQuit("Error saving file:\n\n" + e);
Log.e("termux", "Error saving file", e);
return null;
}
}
void handleUrlAndFinish(final String url) {
final File urlOpenerProgramFile = new File(URL_OPENER_PROGRAM);
if (!urlOpenerProgramFile.isFile()) {
showErrorDialogAndQuit("The following file does not exist:\n$HOME/bin/termux-url-opener\n\n"
+ "Create this file as a script or a symlink - it will be called with the shared URL as only argument.");
return;
}
// Do this for the user if necessary:
urlOpenerProgramFile.setExecutable(true);
final Uri urlOpenerProgramUri = new Uri.Builder().scheme("file").path(URL_OPENER_PROGRAM).build();
Intent executeIntent = new Intent(TermuxService.ACTION_EXECUTE, urlOpenerProgramUri);
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
executeIntent.putExtra(TermuxService.EXTRA_ARGUMENTS, new String[]{url});
startService(executeIntent);
finish();
}
}

View File

@@ -28,7 +28,7 @@ final class JNI {
* @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);
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);

View File

@@ -127,10 +127,14 @@ public final class TerminalBuffer {
mLines[externalToInternalRow(row)].mLineWrap = true;
}
private boolean getLineWrap(int row) {
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).

View File

@@ -218,7 +218,7 @@ public final class TerminalEmulator {
*/
private int mScrollCounter = 0;
private int mUtf8ToFollow, mUtf8Index;
private byte mUtf8ToFollow, mUtf8Index;
private final byte[] mUtf8InputBuffer = new byte[4];
public final TerminalColors mColors = new TerminalColors();
@@ -424,7 +424,11 @@ public final class TerminalEmulator {
processCodePoint(/* escape (hexadecimal=0x1B, octal=033): */27);
processCodePoint((codePoint & 0x7F) + 0x40);
} else {
if (Character.UNASSIGNED == Character.getType(codePoint)) codePoint = UNICODE_REPLACEMENT_CHAR;
switch (Character.getType(codePoint)) {
case Character.UNASSIGNED:
case Character.SURROGATE:
codePoint = UNICODE_REPLACEMENT_CHAR;
}
processCodePoint(codePoint);
}
}
@@ -470,16 +474,28 @@ public final class TerminalEmulator {
else
mSession.onBell();
break;
case 8: // BS
setCursorCol(Math.max(mLeftMargin, mCursorCol - 1));
break;
case 9: // Horizontal tab - move to next tab stop, but not past edge of screen
int nextTabStop = nextTabStop(1);
while (mCursorCol < nextTabStop) {
// Emit newlines to get background color right.
processCodePoint(' ');
}
break;
case 8: // Backspace (BS, ^H).
if (mLeftMargin == mCursorCol) {
// Jump to previous line if it was auto-wrapped.
int previousRow = mCursorRow - 1;
if (previousRow >= 0 && mScreen.getLineWrap(previousRow)) {
mScreen.clearLineWrap(previousRow);
setCursorRowCol(previousRow, mRightMargin - 1);
}
} else {
setCursorCol(mCursorCol - 1);
}
break;
case 9: // Horizontal tab (HT, \t) - move to next tab stop, but not past edge of screen
// XXX: Should perhaps use color if writing to new cells. Try with
// printf "\033[41m\tXX\033[0m\n"
// The OSX Terminal.app colors the spaces from the tab red, but xterm does not.
// Note that Terminal.app only colors on new cells, in e.g.
// printf "\033[41m\t\r\033[42m\tXX\033[0m\n"
// the first cells are created with a red background, but when tabbing over
// them again with a green background they are not overwritten.
mCursorCol = nextTabStop(1);
break;
case 10: // Line feed (LF, \n).
case 11: // Vertical tab (VT, \v).
case 12: // Form feed (FF, \f).
@@ -907,8 +923,9 @@ public final class TerminalEmulator {
/** Process byte while in the {@link #ESC_CSI_QUESTIONMARK} escape state. */
private void doCsiQuestionMark(int b) {
switch (b) {
case 'J': // Selective erase in display (DECSED - http://www.vt100.net/docs/vt510-rm/DECSED).
case 'K': // Selective erase in line (DECSEL - http://vt100.net/docs/vt510-rm/DECSEL).
case 'J': // Selective erase in display (DECSED) - http://www.vt100.net/docs/vt510-rm/DECSED.
case 'K': // Selective erase in line (DECSEL) - http://vt100.net/docs/vt510-rm/DECSEL.
mAboutToAutoWrap = false;
int fillChar = ' ';
int startCol = -1;
int startRow = -1;
@@ -1229,6 +1246,11 @@ public final class TerminalEmulator {
mScreen.blockSet(mRightMargin - 1, mTopMargin, 1, rows, ' ', TextStyle.encode(mForeColor, mBackColor, 0));
}
break;
case 'c': // RIS - Reset to Initial State (http://vt100.net/docs/vt510-rm/RIS).
reset();
blockClear(0, 0, mColumns, mRows);
setCursorPosition(0, 0);
break;
case 'D': // INDEX
doLinefeed();
break;
@@ -1321,9 +1343,8 @@ public final class TerminalEmulator {
continueSequence(ESC_CSI_ARGS_ASTERIX);
break;
case '@': {
// ESC [ Pn @ - ICH Insert Characters.
// "This control function inserts one or more space (SP) characters starting at the cursor position."
// http://www.vt100.net/docs/vt510-rm/ICH
// "CSI{n}@" - Insert ${n} space characters (ICH) - http://www.vt100.net/docs/vt510-rm/ICH.
mAboutToAutoWrap = false;
int columnsAfterCursor = mColumns - mCursorCol;
int spacesToInsert = Math.min(getArg0(1), columnsAfterCursor);
int charsToMove = columnsAfterCursor - spacesToInsert;
@@ -1360,7 +1381,7 @@ public final class TerminalEmulator {
case 'I': // Cursor Horizontal Forward Tabulation (CHT). Move the active position n tabs forward.
setCursorCol(nextTabStop(getArg0(1)));
break;
case 'J': // ESC [ Pn J - ED - Erase in Display
case 'J': // "${CSI}${0,1,2}J" - Erase in Display (ED)
// ED ignores the scrolling margins.
switch (getArg0(0)) {
case 0: // Erase from the active position to the end of the screen, inclusive (default).
@@ -1377,8 +1398,9 @@ public final class TerminalEmulator {
break;
default:
unknownSequence(b);
break;
return;
}
mAboutToAutoWrap = false;
break;
case 'K': // "CSI{n}K" - Erase in line (EL).
switch (getArg0(0)) {
@@ -1393,8 +1415,9 @@ public final class TerminalEmulator {
break;
default:
unknownSequence(b);
break;
return;
}
mAboutToAutoWrap = false;
break;
case 'L': // "${CSI}{N}L" - insert ${N} lines (IL).
{
@@ -1407,6 +1430,7 @@ public final class TerminalEmulator {
break;
case 'M': // "${CSI}${N}M" - delete N lines (DL).
{
mAboutToAutoWrap = false;
int linesAfterCursor = mBottomMargin - mCursorRow;
int linesToDelete = Math.min(getArg0(1), linesAfterCursor);
int linesToMove = linesAfterCursor - linesToDelete;
@@ -1421,6 +1445,7 @@ public final class TerminalEmulator {
// As characters are deleted, the remaining characters between the cursor and right margin move to the left.
// Character attributes move with the characters. The terminal adds blank spaces with no visual character
// attributes at the right margin. DCH has no effect outside the scrolling margins."
mAboutToAutoWrap = false;
int cellsAfterCursor = mColumns - mCursorCol;
int cellsToDelete = Math.min(getArg0(1), cellsAfterCursor);
int cellsToMove = cellsAfterCursor - cellsToDelete;
@@ -1451,6 +1476,7 @@ public final class TerminalEmulator {
}
break;
case 'X': // "${CSI}${N}X" - Erase ${N:=1} character(s) (ECH). FIXME: Clears character attributes?
mAboutToAutoWrap = false;
mScreen.blockSet(mCursorCol, mCursorRow, Math.min(getArg0(1), mColumns - mCursorCol), 1, ' ', getStyle());
break;
case 'Z': // Cursor Backward Tabulation (CBT). Move the active position n tabs backward.

View File

@@ -82,14 +82,17 @@ public final class TerminalSession extends TerminalOutput {
/** Callback which gets notified when a session finishes or changes title. */
final SessionChangedCallback mChangeCallback;
/** The pid of the shell process or -1 if not running. */
/** The pid of the shell process. 0 if not started and -1 if finished running. */
int mShellPid;
int mShellExitStatus = -1;
/** 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[])}.
* {@link JNI#createSubprocess(String, String, String[], String[], int[], int, int)}.
*/
final int mTerminalFileDescriptor;
private int mTerminalFileDescriptor;
/** Set by the application for user identification of session, not by terminal. */
public String mSessionName;
@@ -128,20 +131,26 @@ public final class TerminalSession extends TerminalOutput {
}
};
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;
int[] processId = new int[1];
mTerminalFileDescriptor = JNI.createSubprocess(shellPath, cwd, args, env, processId);
mShellPid = processId[0];
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) {
JNI.setPtyWindowSize(mTerminalFileDescriptor, rows, columns);
if (mEmulator == null) {
initializeEmulator(columns, rows);
} else {
JNI.setPtyWindowSize(mTerminalFileDescriptor, rows, columns);
mEmulator.resize(columns, rows);
}
}
@@ -161,6 +170,11 @@ public final class TerminalSession extends TerminalOutput {
*/
public void initializeEmulator(int columns, int rows) {
mEmulator = new TerminalEmulator(this, columns, rows, /* transcript= */5000);
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 + "]") {
@@ -204,7 +218,7 @@ public final class TerminalSession extends TerminalOutput {
/** Write data to the shell process. */
@Override
public void write(byte[] data, int offset, int count) {
mTerminalToProcessIOQueue.write(data, offset, count);
if (mShellPid > 0) mTerminalToProcessIOQueue.write(data, offset, count);
}
/** Write the Unicode code point to the terminal encoded in UTF-8. */

View File

@@ -10,6 +10,7 @@ import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.graphics.drawable.BitmapDrawable;
import android.media.AudioManager;
import android.os.Build;
import android.text.InputType;
import android.text.TextUtils;
@@ -46,7 +47,7 @@ import java.util.Properties;
public final class TerminalView extends View {
/** Log view key and IME events. */
private static final boolean LOG_KEY_EVENTS = true;
private static final boolean LOG_KEY_EVENTS = false;
/** The currently displayed terminal session, whose emulator is {@link #mEmulator}. */
TerminalSession mTermSession;
@@ -692,6 +693,10 @@ public final class TerminalView extends View {
// As left alt+b, jumping forward in readline:
codePoint = 'b';
leftAltDownFromEvent = true;
} else if (codePoint == 'v' || codePoint == 'V') {
codePoint = -1;
AudioManager audio = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
audio.adjustSuggestedStreamVolume(AudioManager.ADJUST_SAME, AudioManager.USE_DEFAULT_STREAM_TYPE, AudioManager.FLAG_SHOW_UI);
}
}
@@ -774,8 +779,9 @@ public final class TerminalView extends View {
public void checkForFontAndColors() {
try {
File fontFile = new File("/data/data/com.termux/files/home/.termux/font.ttf");
File colorsFile = new File("/data/data/com.termux/files/home/.termux/colors.properties");
// Hard-coded paths since this file is used also in Termux:Float.
@SuppressLint("SdCardPath") File fontFile = new File("/data/data/com.termux/files/home/.termux/font.ttf");
@SuppressLint("SdCardPath") File colorsFile = new File("/data/data/com.termux/files/home/.termux/colors.properties");
final Properties props = new Properties();
if (colorsFile.isFile()) {
@@ -891,12 +897,16 @@ public final class TerminalView extends View {
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
final int[] ACTION_MODE_ATTRS = { android.R.attr.actionModeCopyDrawable, android.R.attr.actionModePasteDrawable, };
TypedArray styledAttributes = getContext().obtainStyledAttributes(ACTION_MODE_ATTRS);
int show = MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT;
try {
int show = MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT;
ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
menu.add(Menu.NONE, 1, Menu.NONE, R.string.copy_text).setIcon(styledAttributes.getResourceId(0, 0)).setShowAsAction(show);
menu.add(Menu.NONE, 2, Menu.NONE, R.string.paste_text).setIcon(styledAttributes.getResourceId(1, 0)).setEnabled(clipboard.hasPrimaryClip()).setShowAsAction(show);
menu.add(Menu.NONE, 3, Menu.NONE, R.string.text_selection_more);
ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
menu.add(Menu.NONE, 1, Menu.NONE, R.string.copy_text).setIcon(styledAttributes.getResourceId(0, 0)).setShowAsAction(show);
menu.add(Menu.NONE, 2, Menu.NONE, R.string.paste_text).setIcon(styledAttributes.getResourceId(1, 0)).setEnabled(clipboard.hasPrimaryClip()).setShowAsAction(show);
menu.add(Menu.NONE, 3, Menu.NONE, R.string.text_selection_more);
} finally {
styledAttributes.recycle();
}
return true;
}

View File

@@ -22,7 +22,14 @@ static int throw_runtime_exception(JNIEnv* env, char const* message)
return -1;
}
static int create_subprocess(JNIEnv* env, char const* cmd, char const* cwd, char* const argv[], char** envp, int* pProcessId)
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");
@@ -49,8 +56,8 @@ static int create_subprocess(JNIEnv* env, char const* cmd, char const* cwd, char
tios.c_iflag &= ~(IXON | IXOFF);
tcsetattr(ptm, TCSANOW, &tios);
/** Set initial winsize (better too small than too large). */
struct winsize sz = { .ws_row = 20, .ws_col = 20 };
/** Set initial winsize. */
struct winsize sz = { .ws_row = rows, .ws_col = columns };
ioctl(ptm, TIOCSWINSZ, &sz);
pid_t pid = fork();
@@ -105,7 +112,16 @@ static int create_subprocess(JNIEnv* env, char const* cmd, char const* cwd, char
}
}
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)
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;
@@ -140,7 +156,7 @@ JNIEXPORT jint JNICALL Java_com_termux_terminal_JNI_createSubprocess(JNIEnv* env
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);
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);

View File

@@ -12,7 +12,7 @@
<string name="help">Help</string>
<string name="welcome_dialog_title">Welcome to Termux</string>
<string name="welcome_dialog_body">Long press anywhere on the terminal for a context menu where Help is available.\n\nExecute \'apt update\' to update the packages list before installing packages.</string>
<string name="welcome_dialog_body">Long press and select <i>More…</i> to show a menu where <i>Help</i> is available.\n\nExecute <b>apt update</b> to update the packages list before installing packages.</string>
<string name="welcome_dialog_dont_show_again_button">Do not show again</string>
<string name="bootstrap_installer_body">Installing…</string>
@@ -55,4 +55,8 @@
<string name="empty_folder">Empty folder.</string>
<string name="file_received_title">Save file in ~/downloads/</string>
<string name="file_received_edit_button">Edit</string>
<string name="file_received_open_folder_button">Open folder</string>
</resources>

View File

@@ -20,4 +20,13 @@ public class ControlSequenceIntroducerTest extends TerminalTestCase {
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 ", " ");
}
}

View File

@@ -163,7 +163,12 @@ public class CursorAndScreenTest extends TerminalTestCase {
}
}
public void testHorizontalTabColorsBackground() {
/**
* 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++) {
@@ -172,4 +177,54 @@ public class CursorAndScreenTest extends TerminalTestCase {
}
}
/**
* 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 ", " ", " ");
}
}

View File

@@ -28,6 +28,11 @@ public class DecSetTest extends TerminalTestCase {
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. */

View File

@@ -92,6 +92,7 @@ public class TerminalTest extends TerminalTestCase {
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());
@@ -257,4 +258,9 @@ public class TerminalTest extends TerminalTestCase {
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", " ");
}
}

View File

@@ -12,6 +12,47 @@ public class UnicodeInputTest extends TerminalTestCase {
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 {

View File

@@ -39,6 +39,7 @@ public class WcWidthTest extends TestCase {
public void testCombining() {
assertWidthIs(0, 0x0302);
assertWidthIs(0, 0x0308);
assertWidthIs(0, 0x2060);
}
public void testWatch() {

Binary file not shown.

View File

@@ -1,6 +1,6 @@
#Wed Dec 23 01:44:47 CET 2015
#Tue Mar 15 00:24:33 CET 2016
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-2.12-bin.zip