Compare commits

..

40 Commits
v0.23 ... v0.29

Author SHA1 Message Date
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
Fredrik Fornwall
19eb371d23 Do not force soft keyboard visible when hw exists 2016-01-13 13:17:53 +01:00
Fredrik Fornwall
6caaae4fd6 Do not start text selection directly on LMB 2016-01-13 13:17:36 +01:00
Fredrik Fornwall
ed544102bc Show icons for copy and paste menu items 2016-01-13 12:28:31 +01:00
Fredrik Fornwall
8f1ab1bc17 Use action mode overlay on pre-6.0 devices
This avoids the terminal content from being pushed down when starting
text selection. The drawback is that one cannot select text at the
top rows without scrolling - something to fix for the future.
2016-01-13 12:27:15 +01:00
Fredrik Fornwall
50337cbf9d Fix gesture handling while selecting text
Also remove stray debug logging.
2016-01-13 10:52:23 +01:00
Fredrik Fornwall
fa9ea2db5c Do not auto scroll when selecting text 2016-01-13 04:20:36 +01:00
Fredrik Fornwall
7a659ebd21 Improve session name dialog
- Show keyboard directly.
- Let return create the session.
2016-01-13 03:44:03 +01:00
Fredrik Fornwall
54bc1ed791 Do not recognize gestures while selecting text 2016-01-13 03:42:46 +01:00
Fredrik Fornwall
fe4365c94b Simplify long press on new session button 2016-01-13 03:18:22 +01:00
Fredrik Fornwall
0c13ea1bd4 Bump version to 0.26 2016-01-13 03:02:09 +01:00
Fredrik Fornwall
862b461a07 Improve text selection functionality
- Make text selection easier and quicker by selecting text directly on long press, and using standard grip bars for changing the selection.
- Disable the drawer while selecting text.
- Fix problem with selecting snippets of text with wide unicode characters at start and end.
- Remove the "tap-screen" configuration option for a more common show keyboard behaviour when tapping the terminal.
- Do no longer map the back key to escape by default - but it's still possible to do by configuration.
- Add new hardware keyboard shortcut Ctrl+Shift+K for toggling soft keyboard visibility.
2016-01-13 03:01:29 +01:00
Fredrik Fornwall
845976be0f Update README.md 2016-01-13 01:29:57 +01:00
Fredrik Fornwall
60bdaa3bf6 Add test for space handling 2016-01-05 03:14:18 +01:00
Fredrik Fornwall
eeb873f4e4 Do not save instance state in DrawerLayout
This was not needed, and due to missing CREATOR field caused a crash
after returning to the activity after it was evicted
2016-01-05 01:00:25 +01:00
Fredrik Fornwall
207ddf9fdc Change member to local variable 2016-01-05 00:48:16 +01:00
Fredrik Fornwall
5ca82ea095 Bump version to 0.25 2015-12-30 00:49:35 +01:00
Fredrik Fornwall
913c474d32 Input normal ^ even on other unicode char input
Some bluetooth keyboards [1] input U+02C6, the unicode character
MODIFIER LETTER CIRCUMFLEX ACCENT instead of the more common ^
(U+005E CIRCUMFLEX ACCENT). Remap it to the common caret since
that is what terminal programs expect.

[1] https://plus.google.com/100972300636796512022/posts/f7PKpXWesgG
2015-12-30 00:46:08 +01:00
Fredrik Fornwall
657c270d97 Fix error message 2015-12-28 20:49:46 +01:00
Fredrik Fornwall
7763931035 Bump app version to 0.24 2015-12-28 20:48:13 +01:00
Fredrik Fornwall
50005bc794 Fix problem with font and color loading at startup
Using View#post() does not work in onCreate().
2015-12-28 20:47:34 +01:00
Fredrik Fornwall
96f5ed985a Remove unused files 2015-12-28 02:30:28 +01:00
36 changed files with 708 additions and 529 deletions

1
.gitignore vendored
View File

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

View File

@@ -1,6 +1,7 @@
Termux app
==========
[![Travis build status](https://travis-ci.org/termux/termux-app.svg?branch=master)](https://travis-ci.org/termux/termux-app)
[![Join the chat at https://gitter.im/termux/termux](https://badges.gitter.im/termux/termux.svg)](https://gitter.im/termux/termux)
Termux is an Android terminal app and Linux environment.
@@ -19,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

@@ -9,17 +9,17 @@ android {
}
sourceSets {
main {
jni.srcDirs = []
}
main {
jni.srcDirs = []
}
}
defaultConfig {
applicationId "com.termux"
minSdkVersion 21
targetSdkVersion 23
versionCode 23
versionName "0.23"
versionCode 29
versionName "0.29"
}
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

View File

@@ -3,10 +3,15 @@ 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 {
@@ -14,10 +19,25 @@ final class DialogUtils {
void onTextSet(String text);
}
static void textInput(Activity activity, int titleText, int positiveButtonText, String initialText, final TextSetListener onPositive) {
static void textInput(Activity activity, int titleText, int positiveButtonText, String initialText, final TextSetListener onPositive,
int neutralButtonText, final TextSetListener onNeutral) {
final EditText input = new EditText(activity);
input.setSingleLine();
if (initialText != null) input.setText(initialText);
if (initialText != null) {
input.setText(initialText);
Selection.setSelection(input.getText(), initialText.length());
}
final AlertDialog[] dialogHolder = new AlertDialog[1];
input.setImeActionLabel(activity.getResources().getString(positiveButtonText), KeyEvent.KEYCODE_ENTER);
input.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
onPositive.onTextSet(input.getText().toString());
dialogHolder[0].dismiss();
return true;
}
});
float dipInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, activity.getResources().getDisplayMetrics());
// https://www.google.com/design/spec/components/dialogs.html#dialogs-specs
@@ -27,17 +47,34 @@ final class DialogUtils {
LinearLayout layout = new LinearLayout(activity);
layout.setOrientation(LinearLayout.VERTICAL);
layout.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
// layout.setGravity(Gravity.CLIP_VERTICAL);
layout.setPadding(paddingTopAndSides, paddingTopAndSides, paddingTopAndSides, paddingBottom);
layout.addView(input);
new AlertDialog.Builder(activity).setTitle(titleText).setView(layout).setPositiveButton(positiveButtonText, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface d, int whichButton) {
onPositive.onTextSet(input.getText().toString());
}
}).setNegativeButton(android.R.string.cancel, null).show();
input.requestFocus();
AlertDialog.Builder builder = new AlertDialog.Builder(activity)
.setTitle(titleText).setView(layout)
.setPositiveButton(positiveButtonText, new DialogInterface.OnClickListener() {
@Override
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());
}
});
}
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].show();
}
}

View File

@@ -82,7 +82,8 @@ import java.util.regex.Pattern;
*/
public final class TermuxActivity extends Activity implements ServiceConnection {
private static final int CONTEXTMENU_SELECT_ID = 0;
private static final int CONTEXTMENU_SELECT_URL_ID = 0;
private static final int CONTEXTMENU_SHARE_TRANSCRIPT_ID = 1;
private static final int CONTEXTMENU_PASTE_ID = 3;
private static final int CONTEXTMENU_KILL_PROCESS_ID = 4;
private static final int CONTEXTMENU_RESET_TERMINAL_ID = 5;
@@ -122,7 +123,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
*/
boolean mIsVisible;
private SoundPool mBellSoundPool = new SoundPool.Builder().setMaxStreams(1).setAudioAttributes(
private final SoundPool mBellSoundPool = new SoundPool.Builder().setMaxStreams(1).setAudioAttributes(
new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build()).build();
private int mBellSoundId;
@@ -136,9 +137,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
if (ensureStoragePermissionGranted()) TermuxInstaller.setupStorageSymlinks(TermuxActivity.this);
return;
}
if (whatToReload == null || "colors".equals(whatToReload)) mTerminalView.checkForColors();
if (whatToReload == null || "font".equals(whatToReload)) mTerminalView.checkForTypeface();
if (whatToReload == null || "settings".equals(whatToReload)) mSettings.reloadFromProperties(TermuxActivity.this);
mTerminalView.checkForFontAndColors();
mSettings.reloadFromProperties(TermuxActivity.this);
}
}
};
@@ -219,6 +219,9 @@ public final class TermuxActivity extends Activity implements ServiceConnection
getDrawer().closeDrawers();
} else if (unicodeChar == 'f'/* full screen */) {
toggleImmersive();
} else if (unicodeChar == 'k'/* keyboard */) {
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
} else if (unicodeChar == 'm'/* menu */) {
mTerminalView.showContextMenu();
} else if (unicodeChar == 'r'/* rename */) {
@@ -256,66 +259,51 @@ public final class TermuxActivity extends Activity implements ServiceConnection
return scale;
}
@Override
public void onLongPress(MotionEvent event) {
mTerminalView.showContextMenu();
}
@Override
public void onSingleTapUp(MotionEvent e) {
switch (mSettings.mTapBehaviour) {
case TermuxPreferences.TAP_TOGGLE_KEYBOARD:
// Toggle keyboard visibility if tapping with a finger:
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, 0);
break;
case TermuxPreferences.TAP_SHOW_MENU:
mTerminalView.showContextMenu();
break;
}
InputMethodManager mgr = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
mgr.showSoftInput(mTerminalView, InputMethodManager.SHOW_IMPLICIT);
}
@Override
public boolean shouldBackButtonBeMappedToEscape() {
return mSettings.mBackIsEscape;
}
@Override
public void copyModeChanged(boolean copyMode) {
// Disable drawer while copying.
getDrawer().setDrawerLockMode(copyMode ? DrawerLayout.LOCK_MODE_LOCKED_CLOSED : DrawerLayout.LOCK_MODE_UNLOCKED);
}
});
findViewById(R.id.new_session_button).setOnClickListener(new OnClickListener() {
View newSessionButton = findViewById(R.id.new_session_button);
newSessionButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
addNewSession(false, null);
}
});
findViewById(R.id.new_session_button).setOnLongClickListener(new OnLongClickListener() {
newSessionButton.setOnLongClickListener(new OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
Resources res = getResources();
new AlertDialog.Builder(TermuxActivity.this).setTitle(R.string.new_session)
.setItems(new String[] { res.getString(R.string.new_session_normal_unnamed), res.getString(R.string.new_session_normal_named),
res.getString(R.string.new_session_failsafe) }, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case 0:
addNewSession(false, null);
break;
case 1:
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);
}
});
break;
case 2:
addNewSession(true, null);
break;
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);
}
}
}).show();
);
return true;
}
});
@@ -336,8 +324,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
startService(serviceIntent);
if (!bindService(serviceIntent, this, 0)) throw new RuntimeException("bindService() failed");
mTerminalView.checkForTypeface();
mTerminalView.checkForColors();
mTerminalView.checkForFontAndColors();
mBellSoundId = mBellSoundPool.load(this, R.raw.bell, 1);
}
@@ -389,7 +376,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
@Override
public void onClipboardText(TerminalSession session, String text) {
if (!mIsVisible) return;
showToast("Clipboard set:\n\"" + text + "\"", true);
showToast("Clipboard:\n\"" + text + "\"", false);
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
clipboard.setPrimaryClip(new ClipData(null, new String[] { "text/plain" }, new ClipData.Item(text)));
}
@@ -475,6 +462,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
TermuxInstaller.setupIfNeeded(TermuxActivity.this, new Runnable() {
@Override
public void run() {
if (mTermService == null) return; // Activity might have been destroyed.
try {
if (TermuxPreferences.isShowWelcomeDialog(TermuxActivity.this)) {
new AlertDialog.Builder(TermuxActivity.this).setTitle(R.string.welcome_dialog_title).setMessage(R.string.welcome_dialog_body)
@@ -510,7 +498,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
public void onTextSet(String text) {
sessionToRename.mSessionName = text;
}
});
}, -1, null);
}
@Override
@@ -537,6 +525,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
@@ -623,9 +615,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
TerminalSession currentSession = getCurrentTermSession();
if (currentSession == null) return;
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
menu.add(Menu.NONE, CONTEXTMENU_PASTE_ID, Menu.NONE, R.string.paste_text).setEnabled(clipboard.hasPrimaryClip());
menu.add(Menu.NONE, CONTEXTMENU_SELECT_ID, Menu.NONE, R.string.select);
menu.add(Menu.NONE, CONTEXTMENU_SELECT_URL_ID, Menu.NONE, R.string.select_url);
menu.add(Menu.NONE, CONTEXTMENU_SHARE_TRANSCRIPT_ID, Menu.NONE, R.string.select_all_and_share);
menu.add(Menu.NONE, CONTEXTMENU_RESET_TERMINAL_ID, Menu.NONE, R.string.reset_terminal);
menu.add(Menu.NONE, CONTEXTMENU_KILL_PROCESS_ID, Menu.NONE, R.string.kill_process).setEnabled(currentSession.isRunning());
menu.add(Menu.NONE, CONTEXTMENU_TOGGLE_FULLSCREEN_ID, Menu.NONE, R.string.toggle_fullscreen).setCheckable(true).setChecked(mSettings.isFullScreen());
@@ -701,89 +692,74 @@ public final class TermuxActivity extends Activity implements ServiceConnection
@Override
public boolean onContextItemSelected(MenuItem item) {
TerminalSession session = getCurrentTermSession();
switch (item.getItemId()) {
case CONTEXTMENU_SELECT_ID:
CharSequence[] items = new CharSequence[] { getString(R.string.select_text), getString(R.string.select_url),
getString(R.string.select_all_and_share) };
new AlertDialog.Builder(this).setItems(items, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case 0:
mTerminalView.toggleSelectingText();
break;
case 1:
showUrlSelection();
break;
case 2:
TerminalSession session = getCurrentTermSession();
if (session != null) {
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TEXT, session.getEmulator().getScreen().getTranscriptText().trim());
intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.share_transcript_title));
startActivity(Intent.createChooser(intent, getString(R.string.share_transcript_chooser_title)));
}
break;
case CONTEXTMENU_SELECT_URL_ID:
showUrlSelection();
return true;
case CONTEXTMENU_SHARE_TRANSCRIPT_ID:
if (session != null) {
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TEXT, session.getEmulator().getScreen().getTranscriptText().trim());
intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.share_transcript_title));
startActivity(Intent.createChooser(intent, getString(R.string.share_transcript_chooser_title)));
}
return true;
case CONTEXTMENU_PASTE_ID:
doPaste();
return true;
case CONTEXTMENU_KILL_PROCESS_ID:
final AlertDialog.Builder b = new AlertDialog.Builder(this);
b.setIcon(android.R.drawable.ic_dialog_alert);
b.setMessage(R.string.confirm_kill_process);
b.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
dialog.dismiss();
getCurrentTermSession().finishIfRunning();
}
dialog.dismiss();
});
b.setNegativeButton(android.R.string.no, null);
b.show();
return true;
case CONTEXTMENU_RESET_TERMINAL_ID: {
if (session != null) {
session.reset();
showToast(getResources().getString(R.string.reset_toast_notification), true);
}
}).show();
return true;
case CONTEXTMENU_PASTE_ID:
doPaste();
return true;
case CONTEXTMENU_KILL_PROCESS_ID:
final AlertDialog.Builder b = new AlertDialog.Builder(this);
b.setIcon(android.R.drawable.ic_dialog_alert);
b.setMessage(R.string.confirm_kill_process);
b.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
dialog.dismiss();
getCurrentTermSession().finishIfRunning();
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();
}
});
b.setNegativeButton(android.R.string.no, null);
b.show();
return true;
case CONTEXTMENU_RESET_TERMINAL_ID: {
TerminalSession session = getCurrentTermSession();
if (session != null) {
session.reset();
showToast(getResources().getString(R.string.reset_toast_notification), true);
}
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();
}
}
return true;
case CONTEXTMENU_TOGGLE_FULLSCREEN_ID:
toggleImmersive();
return true;
case CONTEXTMENU_HELP_ID:
startActivity(new Intent(this, TermuxHelpActivity.class));
return true;
default:
return super.onContextItemSelected(item);
case CONTEXTMENU_TOGGLE_FULLSCREEN_ID:
toggleImmersive();
return true;
case CONTEXTMENU_HELP_ID:
startActivity(new Intent(this, TermuxHelpActivity.class));
return true;
default:
return super.onContextItemSelected(item);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
if (requestCode == REQUESTCODE_PERMISSION_STORAGE && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
TermuxInstaller.setupStorageSymlinks(this);
}

View File

@@ -16,7 +16,6 @@ import android.widget.RelativeLayout;
public final class TermuxHelpActivity extends Activity {
private WebView mWebView;
private ProgressBar mProgressBar;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -25,10 +24,10 @@ public final class TermuxHelpActivity extends Activity {
final RelativeLayout progressLayout = new RelativeLayout(this);
RelativeLayout.LayoutParams lParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
lParams.addRule(RelativeLayout.CENTER_IN_PARENT);
mProgressBar = new ProgressBar(this);
mProgressBar.setIndeterminate(true);
mProgressBar.setLayoutParams(lParams);
progressLayout.addView(mProgressBar);
ProgressBar progressBar = new ProgressBar(this);
progressBar.setIndeterminate(true);
progressBar.setLayoutParams(lParams);
progressLayout.addView(progressBar);
mWebView = new WebView(this);
WebSettings settings = mWebView.getSettings();

View File

@@ -182,7 +182,7 @@ final class TermuxInstaller {
if (arch.startsWith("arm") || arch.equals("aarch64")) {
// Handle different arm variants such as armv7l:
arch = "arm";
} else if (arch.equals("x86_64")) {
} 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");

View File

@@ -1,7 +1,5 @@
package com.termux.app;
import com.termux.terminal.TerminalSession;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
@@ -10,6 +8,8 @@ import android.util.Log;
import android.util.TypedValue;
import android.widget.Toast;
import com.termux.terminal.TerminalSession;
import java.io.File;
import java.io.FileInputStream;
import java.lang.annotation.Retention;
@@ -26,14 +26,6 @@ final class TermuxPreferences {
static final int BELL_BEEP = 2;
static final int BELL_IGNORE = 3;
@IntDef({TAP_TOGGLE_KEYBOARD, TAP_SHOW_MENU, TAP_IGNORE})
@Retention(RetentionPolicy.SOURCE)
public @interface TapTerminalBehaviour {}
static final int TAP_TOGGLE_KEYBOARD = 1;
static final int TAP_SHOW_MENU = 2;
static final int TAP_IGNORE = 3;
private final int MIN_FONTSIZE;
private static final int MAX_FONTSIZE = 256;
@@ -48,9 +40,6 @@ final class TermuxPreferences {
@AsciiBellBehaviour
int mBellBehaviour = BELL_VIBRATE;
@TapTerminalBehaviour
int mTapBehaviour = TAP_TOGGLE_KEYBOARD;
boolean mBackIsEscape = true;
TermuxPreferences(Context context) {
@@ -124,42 +113,26 @@ final class TermuxPreferences {
public void reloadFromProperties(Context context) {
try {
File propsFile = new File(TermuxService.HOME_PATH + "/.config/termux/termux.properties");
Properties props = new Properties();
if (propsFile.isFile() && propsFile.canRead()) {
Properties props = new Properties();
try (FileInputStream in = new FileInputStream(propsFile)) {
props.load(in);
}
switch (props.getProperty("bell-character", "vibrate")) {
case "beep":
mBellBehaviour = BELL_BEEP;
break;
case "ignore":
mBellBehaviour = BELL_IGNORE;
break;
default: // "vibrate".
mBellBehaviour = BELL_VIBRATE;
break;
}
switch (props.getProperty("tap-screen", "toggle-keyboard")) {
case "show-menu":
mTapBehaviour = TAP_SHOW_MENU;
break;
case "ignore":
mTapBehaviour = TAP_IGNORE;
break;
default: // "toggle-keyboard".
mTapBehaviour = TAP_TOGGLE_KEYBOARD;
break;
}
mBackIsEscape = !"back".equals(props.getProperty("back-key", "escape"));
} else {
mBellBehaviour = BELL_VIBRATE;
mTapBehaviour = TAP_TOGGLE_KEYBOARD;
mBackIsEscape = true;
}
switch (props.getProperty("bell-character", "vibrate")) {
case "beep":
mBellBehaviour = BELL_BEEP;
break;
case "ignore":
mBellBehaviour = BELL_IGNORE;
break;
default: // "vibrate".
mBellBehaviour = BELL_VIBRATE;
break;
}
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

@@ -16,8 +16,6 @@ package com.termux.drawer;
* limitations under the License.
*/
import java.util.List;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
@@ -27,8 +25,6 @@ import android.graphics.PixelFormat;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.SystemClock;
import android.util.AttributeSet;
import android.view.Gravity;
@@ -39,6 +35,8 @@ import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import java.util.List;
/**
* DrawerLayout acts as a top-level container for window content that allows for interactive "drawer" views to be pulled
* out from the edge of the window.
@@ -1422,38 +1420,6 @@ public class DrawerLayout extends ViewGroup {
return super.onKeyUp(keyCode, event);
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
final SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
if (ss.openDrawerGravity != Gravity.NO_GRAVITY) {
final View toOpen = findDrawerWithGravity(ss.openDrawerGravity);
if (toOpen != null) {
openDrawer(toOpen);
}
}
setDrawerLockMode(ss.lockModeLeft, Gravity.LEFT);
setDrawerLockMode(ss.lockModeRight, Gravity.RIGHT);
}
@Override
protected Parcelable onSaveInstanceState() {
final Parcelable superState = super.onSaveInstanceState();
final SavedState ss = new SavedState(superState);
final View openDrawer = findOpenDrawer();
if (openDrawer != null) {
ss.openDrawerGravity = ((LayoutParams) openDrawer.getLayoutParams()).gravity;
}
ss.lockModeLeft = mLockModeLeft;
ss.lockModeRight = mLockModeRight;
return ss;
}
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
super.addView(child, index, params);
@@ -1486,30 +1452,6 @@ public class DrawerLayout extends ViewGroup {
&& child.getImportantForAccessibility() != View.IMPORTANT_FOR_ACCESSIBILITY_NO;
}
/**
* State persisted across instances
*/
protected static class SavedState extends BaseSavedState {
int openDrawerGravity = Gravity.NO_GRAVITY;
int lockModeLeft = LOCK_MODE_UNLOCKED;
int lockModeRight = LOCK_MODE_UNLOCKED;
public SavedState(Parcel in) {
super(in);
openDrawerGravity = in.readInt();
}
public SavedState(Parcelable superState) {
super(superState);
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeInt(openDrawerGravity);
}
}
private class ViewDragCallback extends ViewDragHelper.Callback {
private final int mAbsGravity;
private ViewDragHelper mDragger;

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

@@ -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

@@ -61,6 +61,10 @@ public final class TerminalBuffer {
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;
@@ -71,7 +75,7 @@ public final class TerminalBuffer {
} else {
for (i = x1Index; i < x2Index; ++i) {
char c = line[i];
if (c != ' ' && !Character.isLowSurrogate(c)) lastPrintingCharIndex = i;
if (c != ' ') lastPrintingCharIndex = i;
}
}
if (lastPrintingCharIndex != -1) builder.append(line, x1Index, lastPrintingCharIndex - x1Index + 1);

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);
}
}
@@ -907,8 +911,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 +1234,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 +1331,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 +1369,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 +1386,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 +1403,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 +1418,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 +1433,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 +1464,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

@@ -184,6 +184,7 @@ public final class TerminalRow {
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) {

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[])}.
*/
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

@@ -14,11 +14,11 @@ public interface TerminalKeyListener {
/** Callback function on scale events according to {@link ScaleGestureDetector#getScaleFactor()}. */
float onScale(float scale);
void onLongPress(MotionEvent e);
/** On a single tap on the terminal if terminal mouse reporting not enabled. */
void onSingleTapUp(MotionEvent e);
boolean shouldBackButtonBeMappedToEscape();
void copyModeChanged(boolean copyMode);
}

View File

@@ -1,28 +1,28 @@
package com.termux.view;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.Properties;
import com.termux.terminal.EmulatorDebug;
import com.termux.terminal.KeyHandler;
import com.termux.terminal.TerminalColors;
import com.termux.terminal.TerminalEmulator;
import com.termux.terminal.TerminalSession;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.res.TypedArray;
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;
import android.util.AttributeSet;
import android.util.Log;
import android.view.ActionMode;
import android.view.HapticFeedbackConstants;
import android.view.InputDevice;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.inputmethod.BaseInputConnection;
@@ -30,6 +30,19 @@ import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.widget.Scroller;
import com.termux.R;
import com.termux.terminal.EmulatorDebug;
import com.termux.terminal.KeyHandler;
import com.termux.terminal.TerminalBuffer;
import com.termux.terminal.TerminalColors;
import com.termux.terminal.TerminalEmulator;
import com.termux.terminal.TerminalSession;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.Properties;
/** View displaying and interacting with a {@link TerminalSession}. */
public final class TerminalView extends View {
@@ -51,9 +64,11 @@ public final class TerminalView extends View {
/** Keeping track of the special keys acting as Ctrl and Fn for the soft keyboard and other hardware keys. */
boolean mVirtualControlKeyDown, mVirtualFnKeyDown;
boolean mIsSelectingText = false;
int mSelXAnchor = -1, mSelYAnchor = -1;
boolean mIsSelectingText = false, mIsDraggingLeftSelection, mInitialTextSelection;
int mSelX1 = -1, mSelX2 = -1, mSelY1 = -1, mSelY2 = -1;
float mSelectionDownX, mSelectionDownY;
private ActionMode mActionMode;
private BitmapDrawable mLeftSelectionHandle, mRightSelectionHandle;
float mScaleFactor = 1.f;
final GestureAndScaleRecognizer mGestureRecognizer;
@@ -78,7 +93,7 @@ public final class TerminalView extends View {
@Override
public boolean onUp(MotionEvent e) {
mScrollRemainder = 0.0f;
if (mEmulator != null && mEmulator.isMouseTrackingActive()) {
if (mEmulator != null && mEmulator.isMouseTrackingActive() && !mIsSelectingText) {
// Quick event processing when mouse tracking is active - do not wait for check of double tapping
// for zooming.
sendMouseEventCode(e, TerminalEmulator.MOUSE_LEFT_BUTTON, true);
@@ -91,6 +106,7 @@ public final class TerminalView extends View {
@Override
public boolean onSingleTapUp(MotionEvent e) {
if (mEmulator == null) return true;
if (mIsSelectingText) { toggleSelectingText(null); return true; }
requestFocus();
if (!mEmulator.isMouseTrackingActive()) {
if (!e.isFromSource(InputDevice.SOURCE_MOUSE)) {
@@ -103,7 +119,7 @@ public final class TerminalView extends View {
@Override
public boolean onScroll(MotionEvent e2, float distanceX, float distanceY) {
if (mEmulator == null) return true;
if (mEmulator == null || mIsSelectingText) return true;
if (mEmulator.isMouseTrackingActive() && e2.isFromSource(InputDevice.SOURCE_MOUSE)) {
// If moving with mouse pointer while pressing button, report that instead of scroll.
// This means that we never report moving with button press-events for touch input,
@@ -121,6 +137,7 @@ public final class TerminalView extends View {
@Override
public boolean onScale(float focusX, float focusY, float scale) {
if (mEmulator == null || mIsSelectingText) return true;
mScaleFactor *= scale;
mScaleFactor = mOnKeyListener.onScale(mScaleFactor);
return true;
@@ -128,7 +145,7 @@ public final class TerminalView extends View {
@Override
public boolean onFling(final MotionEvent e2, float velocityX, float velocityY) {
if (mEmulator == null) return true;
if (mEmulator == null || mIsSelectingText) return true;
// Do not start scrolling until last fling has been taken care of:
if (!mScroller.isFinished()) return true;
@@ -175,9 +192,9 @@ public final class TerminalView extends View {
@Override
public void onLongPress(MotionEvent e) {
if (mEmulator != null && !mGestureRecognizer.isInProgress()) {
if (!mGestureRecognizer.isInProgress() && !mIsSelectingText) {
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
mOnKeyListener.onLongPress(e);
toggleSelectingText(e);
}
}
});
@@ -228,7 +245,7 @@ public final class TerminalView extends View {
//
// So a bit messy. If this gets too messy it's perhaps best resolved by reverting back to just
// "TYPE_NULL" and let the Pinyin Input english keyboard be in word mode.
outAttrs.inputType = InputType.TYPE_NULL | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD;
outAttrs.inputType = InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD;
// Let part of the application show behind when in landscape:
outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_FULLSCREEN;
@@ -335,20 +352,36 @@ public final class TerminalView extends View {
public void onScreenUpdated() {
if (mEmulator == null) return;
boolean skipScrolling = false;
if (mIsSelectingText) {
// Do not scroll when selecting text.
int rowsInHistory = mEmulator.getScreen().getActiveTranscriptRows();
int rowShift = mEmulator.getScrollCounter();
mSelY1 -= rowShift;
mSelY2 -= rowShift;
mSelYAnchor -= rowShift;
if (-mTopRow + rowShift > rowsInHistory) {
// .. unless we're hitting the end of history transcript, in which
// case we abort text selection and scroll to end.
toggleSelectingText(null);
} else {
skipScrolling = true;
mTopRow -= rowShift;
mSelY1 -= rowShift;
mSelY2 -= rowShift;
}
}
mEmulator.clearScrollCounter();
if (mTopRow != 0) {
if (!skipScrolling && mTopRow != 0) {
// Scroll down if not already there.
if (mTopRow < -3) {
// Awaken scroll bars only if scrolling a noticeable amount
// - we do not want visible scroll bars during normal typing
// of one row at a time.
awakenScrollBars();
}
mTopRow = 0;
scrollTo(0, 0);
}
mEmulator.clearScrollCounter();
invalidate();
}
@@ -422,82 +455,105 @@ public final class TerminalView extends View {
@SuppressLint("ClickableViewAccessibility")
@Override
@TargetApi(23)
public boolean onTouchEvent(MotionEvent ev) {
if (mEmulator == null) return true;
final boolean eventFromMouse = ev.isFromSource(InputDevice.SOURCE_MOUSE);
final int action = ev.getAction();
if (eventFromMouse) {
if ((ev.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) {
if (action == MotionEvent.ACTION_DOWN) showContextMenu();
return true;
} else if (mEmulator.isMouseTrackingActive() && (ev.getAction() == MotionEvent.ACTION_DOWN || ev.getAction() == MotionEvent.ACTION_UP)) {
sendMouseEventCode(ev, TerminalEmulator.MOUSE_LEFT_BUTTON, ev.getAction() == MotionEvent.ACTION_DOWN);
return true;
} else if (!mEmulator.isMouseTrackingActive() && action == MotionEvent.ACTION_DOWN) {
// Start text selection with mouse. Note that the check against MotionEvent.ACTION_DOWN is
// important, since we otherwise would pick up secondary mouse button up actions.
mIsSelectingText = true;
}
} else if (!mIsSelectingText) {
mGestureRecognizer.onTouchEvent(ev);
return true;
}
if (mIsSelectingText) {
int cy = (int) (ev.getY() / mRenderer.mFontLineSpacing) + mTopRow;
int cx = (int) (ev.getX() / mRenderer.mFontWidth);
// Offset for finger:
final int SELECT_TEXT_OFFSET_Y = eventFromMouse ? 0 : -40;
int cy = (int) ((ev.getY() + SELECT_TEXT_OFFSET_Y) / mRenderer.mFontLineSpacing) + mTopRow;
switch (action) {
case MotionEvent.ACTION_UP:
mInitialTextSelection = false;
break;
case MotionEvent.ACTION_DOWN:
mSelXAnchor = cx;
mSelYAnchor = cy;
mSelX1 = cx;
mSelY1 = cy;
mSelX2 = mSelX1;
mSelY2 = mSelY1;
invalidate();
int distanceFromSel1 = Math.abs(cx-mSelX1) + Math.abs(cy-mSelY1);
int distanceFromSel2 = Math.abs(cx-mSelX2) + Math.abs(cy-mSelY2);
mIsDraggingLeftSelection = distanceFromSel1 <= distanceFromSel2;
mSelectionDownX = ev.getX();
mSelectionDownY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_UP:
boolean touchBeforeAnchor = (cy < mSelYAnchor || (cy == mSelYAnchor && cx < mSelXAnchor));
int minx = touchBeforeAnchor ? cx : mSelXAnchor;
int maxx = !touchBeforeAnchor ? cx : mSelXAnchor;
int miny = touchBeforeAnchor ? cy : mSelYAnchor;
int maxy = !touchBeforeAnchor ? cy : mSelYAnchor;
mSelX1 = minx;
mSelY1 = miny;
mSelX2 = maxx;
mSelY2 = maxy;
if (action == MotionEvent.ACTION_UP) {
String selectedText = mEmulator.getSelectedText(mSelX1, mSelY1, mSelX2, mSelY2).trim();
mTermSession.clipboardText(selectedText);
toggleSelectingText();
if (mInitialTextSelection) break;
float deltaX = ev.getX() - mSelectionDownX;
float deltaY = ev.getY() - mSelectionDownY;
int deltaCols = (int) Math.ceil(deltaX / mRenderer.mFontWidth);
int deltaRows = (int) Math.ceil(deltaY / mRenderer.mFontLineSpacing);
mSelectionDownX += deltaCols * mRenderer.mFontWidth;
mSelectionDownY += deltaRows * mRenderer.mFontLineSpacing;
if (mIsDraggingLeftSelection) {
mSelX1 += deltaCols;
mSelY1 += deltaRows;
} else {
mSelX2 += deltaCols;
mSelY2 += deltaRows;
}
mSelX1 = Math.min(mEmulator.mColumns, Math.max(0, mSelX1));
mSelX2 = Math.min(mEmulator.mColumns, Math.max(0, mSelX2));
if (mSelY1 == mSelY2 && mSelX1 > mSelX2 || mSelY1 > mSelY2) {
// Switch handles.
mIsDraggingLeftSelection = !mIsDraggingLeftSelection;
int tmpX1 = mSelX1, tmpY1 = mSelY1;
mSelX1 = mSelX2; mSelY1 = mSelY2;
mSelX2 = tmpX1; mSelY2 = tmpY1;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) mActionMode.invalidateContentRect();
invalidate();
break;
default:
toggleSelectingText();
invalidate();
break;
}
mGestureRecognizer.onTouchEvent(ev);
return true;
} else if (ev.isFromSource(InputDevice.SOURCE_MOUSE)) {
if (ev.isButtonPressed(MotionEvent.BUTTON_SECONDARY)) {
if (action == MotionEvent.ACTION_DOWN) showContextMenu();
return true;
} else if (ev.isButtonPressed(MotionEvent.BUTTON_TERTIARY)) {
ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clipData = clipboard.getPrimaryClip();
if (clipData != null) {
CharSequence paste = clipData.getItemAt(0).coerceToText(getContext());
if (!TextUtils.isEmpty(paste)) mEmulator.paste(paste.toString());
}
} else if (mEmulator.isMouseTrackingActive()) { // BUTTON_PRIMARY.
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_UP:
sendMouseEventCode(ev, TerminalEmulator.MOUSE_LEFT_BUTTON, ev.getAction() == MotionEvent.ACTION_DOWN);
break;
case MotionEvent.ACTION_MOVE:
sendMouseEventCode(ev, TerminalEmulator.MOUSE_LEFT_BUTTON_MOVED, true);
break;
}
return true;
}
}
return false;
mGestureRecognizer.onTouchEvent(ev);
return true;
}
@Override
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "onKeyPreIme(keyCode=" + keyCode + ", event=" + event + ")");
if (keyCode == KeyEvent.KEYCODE_BACK && mOnKeyListener.shouldBackButtonBeMappedToEscape()) {
// Intercept back button to treat it as escape:
switch (event.getAction()) {
case KeyEvent.ACTION_DOWN:
return onKeyDown(keyCode, event);
case KeyEvent.ACTION_UP:
return onKeyUp(keyCode, event);
if (keyCode == KeyEvent.KEYCODE_BACK) {
if (mIsSelectingText) {
toggleSelectingText(null);
return true;
} else if (mOnKeyListener.shouldBackButtonBeMappedToEscape()) {
// Intercept back button to treat it as escape:
switch (event.getAction()) {
case KeyEvent.ACTION_DOWN:
return onKeyDown(keyCode, event);
case KeyEvent.ACTION_UP:
return onKeyUp(keyCode, event);
}
}
}
return super.onKeyPreIme(keyCode, event);
@@ -637,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);
}
}
@@ -644,13 +704,20 @@ public final class TerminalView extends View {
if (resultingKeyCode > -1) {
handleKeyCode(resultingKeyCode, 0);
} else {
// The below two workarounds are needed on at least Logitech Keyboard k810 on Samsung Galaxy Tab Pro
// (Android 4.4) with the stock Samsung Keyboard. They should be harmless when not used since the need
// to input the original characters instead of the new ones using the keyboard should be low.
// Rewrite U+02DC 'SMALL TILDE' to U+007E 'TILDE' for ~ to work in shells:
if (codePoint == 0x02DC) codePoint = 0x07E;
// Rewrite U+02CB 'MODIFIER LETTER GRAVE ACCENT' to U+0060 'GRAVE ACCENT' for ` (backticks) to work:
if (codePoint == 0x02CB) codePoint = 0x60;
// Work around bluetooth keyboards sending funny unicode characters instead
// of the more normal ones from ASCII that terminal programs expect - the
// desire to input the original characters should be low.
switch (codePoint) {
case 0x02DC: // SMALL TILDE.
codePoint = 0x007E; // TILDE (~).
break;
case 0x02CB: // MODIFIER LETTER GRAVE ACCENT.
codePoint = 0x0060; // GRAVE ACCENT (`).
break;
case 0x02C6: // MODIFIER LETTER CIRCUMFLEX ACCENT.
codePoint = 0x005E; // CIRCUMFLEX ACCENT (^).
break;
}
// If left alt, send escape before the code point to make e.g. Alt+B and Alt+F work in readline:
mTermSession.writeCodePoint(leftAltDownFromEvent, codePoint);
@@ -710,67 +777,28 @@ public final class TerminalView extends View {
return false;
}
public void checkForTypeface() {
new Thread() {
@Override
public void run() {
try {
File fontFile = new File("/data/data/com.termux/files/home/.termux/font.ttf");
final Typeface newTypeface = fontFile.exists() ? Typeface.createFromFile(fontFile) : Typeface.MONOSPACE;
if (newTypeface != mRenderer.mTypeface) {
post(new Runnable() {
@Override
public void run() {
try {
mRenderer = new TerminalRenderer(mRenderer.mTextSize, newTypeface);
updateSize();
invalidate();
} catch (Exception e) {
Log.e(EmulatorDebug.LOG_TAG, "Error loading font", e);
}
}
});
}
} catch (Exception e) {
Log.e(EmulatorDebug.LOG_TAG, "Error loading font", e);
}
}
}.start();
}
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");
public void checkForColors() {
new Thread() {
@Override
public void run() {
try {
File colorsFile = new File("/data/data/com.termux/files/home/.termux/colors.properties");
final Properties props = colorsFile.isFile() ? new Properties() : null;
if (props != null) {
try (InputStream in = new FileInputStream(colorsFile)) {
props.load(in);
}
}
post(new Runnable() {
@Override
public void run() {
try {
if (props == null) {
TerminalColors.COLOR_SCHEME.reset();
} else {
TerminalColors.COLOR_SCHEME.updateWith(props);
}
if (mEmulator != null) mEmulator.mColors.reset();
invalidate();
} catch (Exception e) {
Log.e(EmulatorDebug.LOG_TAG, "Setting colors failed: " + e.getMessage());
}
}
});
} catch (Exception e) {
Log.e(EmulatorDebug.LOG_TAG, "Failed colors handling", e);
final Properties props = new Properties();
if (colorsFile.isFile()) {
try (InputStream in = new FileInputStream(colorsFile)) {
props.load(in);
}
}
}.start();
TerminalColors.COLOR_SCHEME.updateWith(props);
if (mEmulator != null) mEmulator.mColors.reset();
final Typeface newTypeface = fontFile.exists() ? Typeface.createFromFile(fontFile) : Typeface.MONOSPACE;
mRenderer = new TerminalRenderer(mRenderer.mTextSize, newTypeface);
updateSize();
invalidate();
} catch (Exception e) {
Log.e(EmulatorDebug.LOG_TAG, "Error in checkForFontAndColors()", e);
}
}
/**
@@ -808,13 +836,151 @@ public final class TerminalView extends View {
canvas.drawColor(0XFF000000);
} else {
mRenderer.render(mEmulator, canvas, mTopRow, mSelY1, mSelY2, mSelX1, mSelX2);
if (mIsSelectingText) {
final int gripHandleWidth = mLeftSelectionHandle.getIntrinsicWidth();
final int gripHandleMargin = gripHandleWidth / 4; // See the png.
int right = Math.round((mSelX1) * mRenderer.mFontWidth) + gripHandleMargin;
int top = (mSelY1+1 - mTopRow)*mRenderer.mFontLineSpacing + mRenderer.mFontLineSpacingAndAscent;
mLeftSelectionHandle.setBounds(right - gripHandleWidth, top, right, top + mLeftSelectionHandle.getIntrinsicHeight());
mLeftSelectionHandle.draw(canvas);
int left = Math.round((mSelX2+1)*mRenderer.mFontWidth) - gripHandleMargin;
top = (mSelY2+1 - mTopRow) *mRenderer.mFontLineSpacing + mRenderer.mFontLineSpacingAndAscent;
mRightSelectionHandle.setBounds(left, top, left + gripHandleWidth, top + mRightSelectionHandle.getIntrinsicHeight());
mRightSelectionHandle.draw(canvas);
}
}
}
/** Toggle text selection mode in the view. */
public void toggleSelectingText() {
@TargetApi(23)
public void toggleSelectingText(MotionEvent ev) {
mIsSelectingText = !mIsSelectingText;
if (!mIsSelectingText) mSelX1 = mSelY1 = mSelX2 = mSelY2 = -1;
mOnKeyListener.copyModeChanged(mIsSelectingText);
if (mIsSelectingText) {
if (mLeftSelectionHandle == null) {
mLeftSelectionHandle = (BitmapDrawable) getContext().getDrawable(R.drawable.text_select_handle_left_material);
mRightSelectionHandle = (BitmapDrawable) getContext().getDrawable(R.drawable.text_select_handle_right_material);
}
int cx = (int) (ev.getX() / mRenderer.mFontWidth);
final boolean eventFromMouse = ev.isFromSource(InputDevice.SOURCE_MOUSE);
// Offset for finger:
final int SELECT_TEXT_OFFSET_Y = eventFromMouse ? 0 : -40;
int cy = (int) ((ev.getY() + SELECT_TEXT_OFFSET_Y) / mRenderer.mFontLineSpacing) + mTopRow;
mSelX1 = mSelX2 = cx;
mSelY1 = mSelY2 = cy;
TerminalBuffer screen = mEmulator.getScreen();
if (!" ".equals(screen.getSelectedText(mSelX1, mSelY1, mSelX1, mSelY1))) {
// Selecting something other than whitespace. Expand to word.
while (mSelX1 > 0 && !"".equals(screen.getSelectedText(mSelX1-1, mSelY1, mSelX1-1, mSelY1))) {
mSelX1--;
}
while (mSelX2 < mEmulator.mColumns-1 && !"".equals(screen.getSelectedText(mSelX2+1, mSelY1, mSelX2+1, mSelY1))) {
mSelX2++;
}
}
mInitialTextSelection = true;
mIsDraggingLeftSelection = true;
mSelectionDownX = ev.getX();
mSelectionDownY = ev.getY();
final ActionMode.Callback callback = new ActionMode.Callback() {
@Override
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;
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);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
switch (item.getItemId()) {
case 1:
String selectedText = mEmulator.getSelectedText(mSelX1, mSelY1, mSelX2, mSelY2).trim();
mTermSession.clipboardText(selectedText);
break;
case 2:
ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clipData = clipboard.getPrimaryClip();
if (clipData != null) {
CharSequence paste = clipData.getItemAt(0).coerceToText(getContext());
if (!TextUtils.isEmpty(paste)) mEmulator.paste(paste.toString());
}
break;
case 3:
showContextMenu();
break;
}
toggleSelectingText(null);
return true;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
}
};
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mActionMode = startActionMode(new ActionMode.Callback2() {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
return callback.onCreateActionMode(mode, menu);
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
return callback.onActionItemClicked(mode, item);
}
@Override
public void onDestroyActionMode(ActionMode mode) {
// Ignore.
}
@Override
public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
int x1 = Math.round(mSelX1 * mRenderer.mFontWidth);
int x2 = Math.round(mSelX2 * mRenderer.mFontWidth);
int y1 = Math.round((mSelY1 - mTopRow) * mRenderer.mFontLineSpacing);
int y2 = Math.round((mSelY2 + 1 - mTopRow) * mRenderer.mFontLineSpacing);
outRect.set(Math.min(x1, x2), y1, Math.max(x1, x2), y2);
}
}, ActionMode.TYPE_FLOATING);
} else {
mActionMode = startActionMode(callback);
}
invalidate();
} else {
mActionMode.finish();
mSelX1 = mSelY1 = mSelX2 = mSelY2 = -1;
invalidate();
}
}
public TerminalSession getCurrentSession() {

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);

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
android:src="@drawable/text_select_handle_left_mtrl_alpha"
android:tint="#2196F3" />

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
android:src="@drawable/text_select_handle_right_mtrl_alpha"
android:tint="#2196F3" />

View File

@@ -3,8 +3,6 @@
<string name="application_name">Termux</string>
<string name="shared_user_label">Termux user</string>
<string name="new_session">New session</string>
<string name="new_session_normal_unnamed">Normal - unnamed</string>
<string name="new_session_normal_named">Normal - named</string>
<string name="new_session_failsafe">Failsafe</string>
<string name="toggle_soft_keyboard">Keyboard</string>
<string name="reset_terminal">Reset</string>
@@ -14,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>
@@ -29,18 +27,18 @@
<string name="reset_toast_notification">Terminal reset.</string>
<string name="select">Select…</string>
<string name="select_text">Select text</string>
<string name="select_url">Select URL</string>
<string name="select_url_dialog_title">Click URL to copy or long press to open</string>
<string name="select_all_and_share">Select all text and share</string>
<string name="select_all_and_share">Share transcript</string>
<string name="select_url_no_found">No URL found in the terminal.</string>
<string name="select_url_copied_to_clipboard">URL copied to clipboard</string>
<string name="share_transcript_chooser_title">Send text to:</string>
<string name="paste_text">Paste</string>
<string name="kill_process">Hangup</string>
<string name="copy_text">Copy</string>
<string name="text_selection_more">More…</string>
<string name="kill_process">Hangup</string>
<string name="confirm_kill_process">Close this process?</string>
<string name="session_rename_title">Set session name</string>

View File

@@ -6,10 +6,13 @@
<style name="Theme.Termux" parent="@android:style/Theme.Material.Light.NoActionBar">
<item name="android:statusBarColor">#000000</item>
<item name="android:windowBackground">@android:color/black</item>
<!-- Seen in buttons on left drawer: -->
<item name="android:colorAccent">#212121</item>
<item name="android:alertDialogTheme">@style/TermuxAlertDialogStyle</item>
<item name="android:alertDialogTheme">@style/TermuxAlertDialogStyle</item>
<!-- Avoid action mode toolbar pushing down terminal content when
selecting text on pre-6.0 (non-floating toolbar). -->
<item name="android:windowActionModeOverlay">true</item>
</style>
<style name="TermuxAlertDialogStyle" parent="@android:style/Theme.Material.Light.Dialog.Alert">
@@ -17,4 +20,4 @@
<item name="android:colorAccent">#212121</item>
</style>
</resources>
</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

@@ -172,4 +172,45 @@ 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 -----",
" -");
}
}

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

@@ -111,6 +111,10 @@ public class KeyHandlerTest extends TestCase {
// 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));

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());

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() {

90
gradlew.bat vendored
View File

@@ -1,90 +0,0 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windowz variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

Binary file not shown.