Reformat code project-wide (getting rid of tabs)

This commit is contained in:
Fredrik Fornwall
2016-06-28 01:03:03 +02:00
parent d72fd579ee
commit 2db6923bc4
28 changed files with 6000 additions and 5748 deletions

231
.idea/codeStyleSettings.xml generated Normal file
View File

@@ -0,0 +1,231 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectCodeStyleSettingsManager">
<option name="PER_PROJECT_SETTINGS">
<value>
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
<option name="PACKAGES_TO_USE_IMPORT_ON_DEMAND">
<value />
</option>
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="android" withSubpackages="true" static="false" />
<emptyLine />
<package name="com" withSubpackages="true" static="false" />
<emptyLine />
<package name="junit" withSubpackages="true" static="false" />
<emptyLine />
<package name="net" withSubpackages="true" static="false" />
<emptyLine />
<package name="org" withSubpackages="true" static="false" />
<emptyLine />
<package name="java" withSubpackages="true" static="false" />
<emptyLine />
<package name="javax" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="true" />
<emptyLine />
</value>
</option>
<option name="RIGHT_MARGIN" value="100" />
<AndroidXmlCodeStyleSettings>
<option name="USE_CUSTOM_SETTINGS" value="true" />
</AndroidXmlCodeStyleSettings>
<Objective-C-extensions>
<option name="GENERATE_INSTANCE_VARIABLES_FOR_PROPERTIES" value="ASK" />
<option name="RELEASE_STYLE" value="IVAR" />
<option name="TYPE_QUALIFIERS_PLACEMENT" value="BEFORE" />
<file>
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Import" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Macro" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Typedef" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Enum" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Constant" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Global" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Struct" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="FunctionPredecl" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Function" />
</file>
<class>
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Property" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Synthesize" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InitMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="StaticMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InstanceMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="DeallocMethod" />
</class>
<extensions>
<pair source="cpp" header="h" />
<pair source="c" header="h" />
</extensions>
</Objective-C-extensions>
<XML>
<option name="XML_KEEP_LINE_BREAKS" value="false" />
<option name="XML_ALIGN_ATTRIBUTES" value="false" />
<option name="XML_SPACE_INSIDE_EMPTY_TAG" value="true" />
</XML>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_width</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_height</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_.*</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:width</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:height</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
</value>
</option>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default (1)" />
</component>
</project>

View File

@@ -21,7 +21,7 @@ public final class BackgroundJob {
public BackgroundJob(File cwd, File fileToExecute, String[] args) throws IOException { public BackgroundJob(File cwd, File fileToExecute, String[] args) throws IOException {
String[] env = buildEnvironment(false, cwd.getAbsolutePath()); String[] env = buildEnvironment(false, cwd.getAbsolutePath());
String[] progArray = new String[args.length+1]; String[] progArray = new String[args.length + 1];
mProcess = Runtime.getRuntime().exec(progArray, env, cwd); mProcess = Runtime.getRuntime().exec(progArray, env, cwd);
@@ -81,32 +81,32 @@ public final class BackgroundJob {
} }
public String[] buildEnvironment(boolean failSafe, String cwd) { public String[] buildEnvironment(boolean failSafe, String cwd) {
new File(TermuxService.HOME_PATH).mkdirs(); new File(TermuxService.HOME_PATH).mkdirs();
if (cwd == null) cwd = TermuxService.HOME_PATH; if (cwd == null) cwd = TermuxService.HOME_PATH;
final String termEnv = "TERM=xterm-256color"; final String termEnv = "TERM=xterm-256color";
final String homeEnv = "HOME=" + TermuxService.HOME_PATH; final String homeEnv = "HOME=" + TermuxService.HOME_PATH;
final String prefixEnv = "PREFIX=" + TermuxService.PREFIX_PATH; final String prefixEnv = "PREFIX=" + TermuxService.PREFIX_PATH;
final String androidRootEnv = "ANDROID_ROOT=" + System.getenv("ANDROID_ROOT"); final String androidRootEnv = "ANDROID_ROOT=" + System.getenv("ANDROID_ROOT");
final String androidDataEnv = "ANDROID_DATA=" + System.getenv("ANDROID_DATA"); final String androidDataEnv = "ANDROID_DATA=" + System.getenv("ANDROID_DATA");
// EXTERNAL_STORAGE is needed for /system/bin/am to work on at least // EXTERNAL_STORAGE is needed for /system/bin/am to work on at least
// Samsung S7 - see https://plus.google.com/110070148244138185604/posts/gp8Lk3aCGp3. // Samsung S7 - see https://plus.google.com/110070148244138185604/posts/gp8Lk3aCGp3.
final String externalStorageEnv = "EXTERNAL_STORAGE=" + System.getenv("EXTERNAL_STORAGE"); final String externalStorageEnv = "EXTERNAL_STORAGE=" + System.getenv("EXTERNAL_STORAGE");
String[] env; String[] env;
if (failSafe) { if (failSafe) {
// Keep the default path so that system binaries can be used in the failsafe session. // Keep the default path so that system binaries can be used in the failsafe session.
final String pathEnv = "PATH=" + System.getenv("PATH"); final String pathEnv = "PATH=" + System.getenv("PATH");
return new String[] { termEnv, homeEnv, prefixEnv, androidRootEnv, androidDataEnv, pathEnv, externalStorageEnv }; return new String[]{termEnv, homeEnv, prefixEnv, androidRootEnv, androidDataEnv, pathEnv, externalStorageEnv};
} else { } else {
final String ps1Env = "PS1=$ "; final String ps1Env = "PS1=$ ";
final String ldEnv = "LD_LIBRARY_PATH=" + TermuxService.PREFIX_PATH + "/lib"; final String ldEnv = "LD_LIBRARY_PATH=" + TermuxService.PREFIX_PATH + "/lib";
final String langEnv = "LANG=en_US.UTF-8"; final String langEnv = "LANG=en_US.UTF-8";
final String pathEnv = "PATH=" + TermuxService.PREFIX_PATH + "/bin:" + TermuxService.PREFIX_PATH + "/bin/applets"; final String pathEnv = "PATH=" + TermuxService.PREFIX_PATH + "/bin:" + TermuxService.PREFIX_PATH + "/bin/applets";
final String pwdEnv = "PWD=" + cwd; final String pwdEnv = "PWD=" + cwd;
return new String[] { termEnv, homeEnv, prefixEnv, ps1Env, ldEnv, langEnv, pathEnv, pwdEnv, androidRootEnv, androidDataEnv, externalStorageEnv }; return new String[]{termEnv, homeEnv, prefixEnv, ps1Env, ldEnv, langEnv, pathEnv, pwdEnv, androidRootEnv, androidDataEnv, externalStorageEnv};
}
} }
}
} }

View File

@@ -13,52 +13,52 @@ import android.widget.TextView;
public final class DialogUtils { public final class DialogUtils {
public interface TextSetListener { public interface TextSetListener {
void onTextSet(String text); void onTextSet(String text);
} }
public static void textInput(Activity activity, int titleText, String initialText, public static void textInput(Activity activity, int titleText, String initialText,
int positiveButtonText, final TextSetListener onPositive, int positiveButtonText, final TextSetListener onPositive,
int neutralButtonText, final TextSetListener onNeutral, int neutralButtonText, final TextSetListener onNeutral,
int negativeButtonText, final TextSetListener onNegative, int negativeButtonText, final TextSetListener onNegative,
final DialogInterface.OnDismissListener onDismiss) { final DialogInterface.OnDismissListener onDismiss) {
final EditText input = new EditText(activity); final EditText input = new EditText(activity);
input.setSingleLine(); input.setSingleLine();
if (initialText != null) { if (initialText != null) {
input.setText(initialText); input.setText(initialText);
Selection.setSelection(input.getText(), initialText.length()); Selection.setSelection(input.getText(), initialText.length());
} }
final AlertDialog[] dialogHolder = new AlertDialog[1]; final AlertDialog[] dialogHolder = new AlertDialog[1];
input.setImeActionLabel(activity.getResources().getString(positiveButtonText), KeyEvent.KEYCODE_ENTER); input.setImeActionLabel(activity.getResources().getString(positiveButtonText), KeyEvent.KEYCODE_ENTER);
input.setOnEditorActionListener(new TextView.OnEditorActionListener() { input.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override @Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
onPositive.onTextSet(input.getText().toString()); onPositive.onTextSet(input.getText().toString());
dialogHolder[0].dismiss(); dialogHolder[0].dismiss();
return true; return true;
} }
}); });
float dipInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, activity.getResources().getDisplayMetrics()); float dipInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, activity.getResources().getDisplayMetrics());
// https://www.google.com/design/spec/components/dialogs.html#dialogs-specs // https://www.google.com/design/spec/components/dialogs.html#dialogs-specs
int paddingTopAndSides = Math.round(16 * dipInPixels); int paddingTopAndSides = Math.round(16 * dipInPixels);
int paddingBottom = Math.round(24 * dipInPixels); int paddingBottom = Math.round(24 * dipInPixels);
LinearLayout layout = new LinearLayout(activity); LinearLayout layout = new LinearLayout(activity);
layout.setOrientation(LinearLayout.VERTICAL); layout.setOrientation(LinearLayout.VERTICAL);
layout.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); layout.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
layout.setPadding(paddingTopAndSides, paddingTopAndSides, paddingTopAndSides, paddingBottom); layout.setPadding(paddingTopAndSides, paddingTopAndSides, paddingTopAndSides, paddingBottom);
layout.addView(input); layout.addView(input);
AlertDialog.Builder builder = new AlertDialog.Builder(activity) AlertDialog.Builder builder = new AlertDialog.Builder(activity)
.setTitle(titleText).setView(layout) .setTitle(titleText).setView(layout)
.setPositiveButton(positiveButtonText, new DialogInterface.OnClickListener() { .setPositiveButton(positiveButtonText, new DialogInterface.OnClickListener() {
@Override @Override
public void onClick(DialogInterface d, int whichButton) { public void onClick(DialogInterface d, int whichButton) {
onPositive.onTextSet(input.getText().toString()); onPositive.onTextSet(input.getText().toString());
} }
}); });
if (onNeutral != null) { if (onNeutral != null) {
builder.setNeutralButton(neutralButtonText, new DialogInterface.OnClickListener() { builder.setNeutralButton(neutralButtonText, new DialogInterface.OnClickListener() {
@@ -82,9 +82,9 @@ public final class DialogUtils {
if (onDismiss != null) builder.setOnDismissListener(onDismiss); if (onDismiss != null) builder.setOnDismissListener(onDismiss);
dialogHolder[0] = builder.create(); dialogHolder[0] = builder.create();
dialogHolder[0].setCanceledOnTouchOutside(false); dialogHolder[0].setCanceledOnTouchOutside(false);
dialogHolder[0].show(); dialogHolder[0].show();
} }
} }

View File

@@ -31,14 +31,29 @@ public final class ExtraKeysView extends GridLayout {
int keyCode = 0; int keyCode = 0;
String chars = null; String chars = null;
switch (keyName) { switch (keyName) {
case "ESC": keyCode = KeyEvent.KEYCODE_ESCAPE; break; case "ESC":
case "TAB": keyCode = KeyEvent.KEYCODE_TAB; break; keyCode = KeyEvent.KEYCODE_ESCAPE;
case "": keyCode = KeyEvent.KEYCODE_DPAD_UP; break; break;
case "": keyCode = KeyEvent.KEYCODE_DPAD_LEFT; break; case "TAB":
case "": keyCode = KeyEvent.KEYCODE_DPAD_RIGHT; break; keyCode = KeyEvent.KEYCODE_TAB;
case "": keyCode = KeyEvent.KEYCODE_DPAD_DOWN; break; break;
case "": chars = "-"; break; case "":
default: chars = keyName; keyCode = KeyEvent.KEYCODE_DPAD_UP;
break;
case "":
keyCode = KeyEvent.KEYCODE_DPAD_LEFT;
break;
case "":
keyCode = KeyEvent.KEYCODE_DPAD_RIGHT;
break;
case "":
keyCode = KeyEvent.KEYCODE_DPAD_DOWN;
break;
case "":
chars = "-";
break;
default:
chars = keyName;
} }
if (keyCode > 0) { if (keyCode > 0) {

File diff suppressed because it is too large Load Diff

View File

@@ -15,61 +15,61 @@ import android.widget.RelativeLayout;
/** Basic embedded browser for viewing help pages. */ /** Basic embedded browser for viewing help pages. */
public final class TermuxHelpActivity extends Activity { public final class TermuxHelpActivity extends Activity {
private WebView mWebView; private WebView mWebView;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
final RelativeLayout progressLayout = new RelativeLayout(this); final RelativeLayout progressLayout = new RelativeLayout(this);
RelativeLayout.LayoutParams lParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); RelativeLayout.LayoutParams lParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
lParams.addRule(RelativeLayout.CENTER_IN_PARENT); lParams.addRule(RelativeLayout.CENTER_IN_PARENT);
ProgressBar progressBar = new ProgressBar(this); ProgressBar progressBar = new ProgressBar(this);
progressBar.setIndeterminate(true); progressBar.setIndeterminate(true);
progressBar.setLayoutParams(lParams); progressBar.setLayoutParams(lParams);
progressLayout.addView(progressBar); progressLayout.addView(progressBar);
mWebView = new WebView(this); mWebView = new WebView(this);
WebSettings settings = mWebView.getSettings(); WebSettings settings = mWebView.getSettings();
settings.setCacheMode(WebSettings.LOAD_NO_CACHE); settings.setCacheMode(WebSettings.LOAD_NO_CACHE);
settings.setAppCacheEnabled(false); settings.setAppCacheEnabled(false);
setContentView(progressLayout); setContentView(progressLayout);
mWebView.clearCache(true); mWebView.clearCache(true);
mWebView.setWebViewClient(new WebViewClient() { mWebView.setWebViewClient(new WebViewClient() {
@Override @Override
public boolean shouldOverrideUrlLoading(WebView view, String url) { public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url.startsWith("https://termux.com")) { if (url.startsWith("https://termux.com")) {
// Inline help. // Inline help.
setContentView(progressLayout); setContentView(progressLayout);
return false; return false;
} }
try { try {
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
} catch (ActivityNotFoundException e) { } catch (ActivityNotFoundException e) {
// Android TV does not have a system browser. // Android TV does not have a system browser.
setContentView(progressLayout); setContentView(progressLayout);
return false; return false;
} }
return true; return true;
} }
@Override @Override
public void onPageFinished(WebView view, String url) { public void onPageFinished(WebView view, String url) {
setContentView(mWebView); setContentView(mWebView);
} }
}); });
mWebView.loadUrl("https://termux.com/help.html"); mWebView.loadUrl("https://termux.com/help.html");
} }
@Override @Override
public void onBackPressed() { public void onBackPressed() {
if (mWebView.canGoBack()) { if (mWebView.canGoBack()) {
mWebView.goBack(); mWebView.goBack();
} else { } else {
super.onBackPressed(); super.onBackPressed();
} }
} }
} }

View File

@@ -31,152 +31,155 @@ import java.util.zip.ZipInputStream;
/** /**
* Install the Termux bootstrap packages if necessary by following the below steps: * Install the Termux bootstrap packages if necessary by following the below steps:
* * <p/>
* (1) If $PREFIX already exist, assume that it is correct and be done. Note that this relies on that we do not create a * (1) If $PREFIX already exist, assume that it is correct and be done. Note that this relies on that we do not create a
* broken $PREFIX folder below. * broken $PREFIX folder below.
* * <p/>
* (2) A progress dialog is shown with "Installing..." message and a spinner. * (2) A progress dialog is shown with "Installing..." message and a spinner.
* * <p/>
* (3) A staging folder, $STAGING_PREFIX, is {@link #deleteFolder(File)} if left over from broken installation below. * (3) A staging folder, $STAGING_PREFIX, is {@link #deleteFolder(File)} if left over from broken installation below.
* * <p/>
* (4) The architecture is determined and an appropriate bootstrap zip url is determined in {@link #determineZipUrl()}. * (4) The architecture is determined and an appropriate bootstrap zip url is determined in {@link #determineZipUrl()}.
* * <p/>
* (5) The zip, containing entries relative to the $PREFIX, is is downloaded and extracted by a zip input stream * (5) The zip, containing entries relative to the $PREFIX, is is downloaded and extracted by a zip input stream
* continously encountering zip file entries: * continously encountering zip file entries:
* * <p/>
* (5.1) If the zip entry encountered is SYMLINKS.txt, go through it and remember all symlinks to setup. * (5.1) If the zip entry encountered is SYMLINKS.txt, go through it and remember all symlinks to setup.
* * <p/>
* (5.2) For every other zip entry, extract it into $STAGING_PREFIX and set execute permissions if necessary. * (5.2) For every other zip entry, extract it into $STAGING_PREFIX and set execute permissions if necessary.
*/ */
final class TermuxInstaller { final class TermuxInstaller {
/** Performs setup if necessary. */ /** Performs setup if necessary. */
static void setupIfNeeded(final Activity activity, final Runnable whenDone) { static void setupIfNeeded(final Activity activity, final Runnable whenDone) {
// Termux can only be run as the primary user (device owner) since only that // Termux can only be run as the primary user (device owner) since only that
// account has the expected file system paths. Verify that: // account has the expected file system paths. Verify that:
android.os.UserManager um = (android.os.UserManager) activity.getSystemService(Context.USER_SERVICE); android.os.UserManager um = (android.os.UserManager) activity.getSystemService(Context.USER_SERVICE);
boolean isPrimaryUser = um.getSerialNumberForUser(android.os.Process.myUserHandle()) == 0; boolean isPrimaryUser = um.getSerialNumberForUser(android.os.Process.myUserHandle()) == 0;
if (!isPrimaryUser) { if (!isPrimaryUser) {
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_not_primary_user_message) new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_not_primary_user_message)
.setOnDismissListener(new OnDismissListener() { .setOnDismissListener(new OnDismissListener() {
@Override @Override
public void onDismiss(DialogInterface dialog) { public void onDismiss(DialogInterface dialog) {
System.exit(0); System.exit(0);
} }
}).setPositiveButton(android.R.string.ok, null).show(); }).setPositiveButton(android.R.string.ok, null).show();
return; return;
} }
final File PREFIX_FILE = new File(TermuxService.PREFIX_PATH); final File PREFIX_FILE = new File(TermuxService.PREFIX_PATH);
if (PREFIX_FILE.isDirectory()) { if (PREFIX_FILE.isDirectory()) {
whenDone.run(); whenDone.run();
return; return;
} }
final ProgressDialog progress = ProgressDialog.show(activity, null, activity.getString(R.string.bootstrap_installer_body), true, false); final ProgressDialog progress = ProgressDialog.show(activity, null, activity.getString(R.string.bootstrap_installer_body), true, false);
new Thread() { new Thread() {
@Override @Override
public void run() { public void run() {
try { try {
final String STAGING_PREFIX_PATH = TermuxService.FILES_PATH + "/usr-staging"; final String STAGING_PREFIX_PATH = TermuxService.FILES_PATH + "/usr-staging";
final File STAGING_PREFIX_FILE = new File(STAGING_PREFIX_PATH); final File STAGING_PREFIX_FILE = new File(STAGING_PREFIX_PATH);
if (STAGING_PREFIX_FILE.exists()) { if (STAGING_PREFIX_FILE.exists()) {
deleteFolder(STAGING_PREFIX_FILE); deleteFolder(STAGING_PREFIX_FILE);
} }
final byte[] buffer = new byte[8096]; final byte[] buffer = new byte[8096];
final List<Pair<String, String>> symlinks = new ArrayList<>(50); final List<Pair<String, String>> symlinks = new ArrayList<>(50);
final URL zipUrl = determineZipUrl(); final URL zipUrl = determineZipUrl();
try (ZipInputStream zipInput = new ZipInputStream(zipUrl.openStream())) { try (ZipInputStream zipInput = new ZipInputStream(zipUrl.openStream())) {
ZipEntry zipEntry; ZipEntry zipEntry;
while ((zipEntry = zipInput.getNextEntry()) != null) { while ((zipEntry = zipInput.getNextEntry()) != null) {
if (zipEntry.getName().equals("SYMLINKS.txt")) { if (zipEntry.getName().equals("SYMLINKS.txt")) {
BufferedReader symlinksReader = new BufferedReader(new InputStreamReader(zipInput)); BufferedReader symlinksReader = new BufferedReader(new InputStreamReader(zipInput));
String line; String line;
while ((line = symlinksReader.readLine()) != null) { while ((line = symlinksReader.readLine()) != null) {
String[] parts = line.split(""); String[] parts = line.split("");
if (parts.length != 2) throw new RuntimeException("Malformed symlink line: " + line); if (parts.length != 2)
String oldPath = parts[0]; throw new RuntimeException("Malformed symlink line: " + line);
String newPath = STAGING_PREFIX_PATH + "/" + parts[1]; String oldPath = parts[0];
symlinks.add(Pair.create(oldPath, newPath)); String newPath = STAGING_PREFIX_PATH + "/" + parts[1];
} symlinks.add(Pair.create(oldPath, newPath));
} else { }
String zipEntryName = zipEntry.getName(); } else {
File targetFile = new File(STAGING_PREFIX_PATH, zipEntryName); String zipEntryName = zipEntry.getName();
if (zipEntry.isDirectory()) { File targetFile = new File(STAGING_PREFIX_PATH, zipEntryName);
if (!targetFile.mkdirs()) throw new RuntimeException("Failed to create directory: " + targetFile.getAbsolutePath()); if (zipEntry.isDirectory()) {
} else { if (!targetFile.mkdirs())
try (FileOutputStream outStream = new FileOutputStream(targetFile)) { throw new RuntimeException("Failed to create directory: " + targetFile.getAbsolutePath());
int readBytes; } else {
while ((readBytes = zipInput.read(buffer)) != -1) try (FileOutputStream outStream = new FileOutputStream(targetFile)) {
outStream.write(buffer, 0, readBytes); int readBytes;
} while ((readBytes = zipInput.read(buffer)) != -1)
if (zipEntryName.startsWith("bin/") || zipEntryName.startsWith("libexec") || zipEntryName.startsWith("lib/apt/methods")) { outStream.write(buffer, 0, readBytes);
//noinspection OctalInteger }
Os.chmod(targetFile.getAbsolutePath(), 0700); if (zipEntryName.startsWith("bin/") || zipEntryName.startsWith("libexec") || zipEntryName.startsWith("lib/apt/methods")) {
} //noinspection OctalInteger
} Os.chmod(targetFile.getAbsolutePath(), 0700);
} }
} }
} }
}
}
if (symlinks.isEmpty()) throw new RuntimeException("No SYMLINKS.txt encountered"); if (symlinks.isEmpty())
for (Pair<String, String> symlink : symlinks) { throw new RuntimeException("No SYMLINKS.txt encountered");
Os.symlink(symlink.first, symlink.second); for (Pair<String, String> symlink : symlinks) {
} Os.symlink(symlink.first, symlink.second);
}
if (!STAGING_PREFIX_FILE.renameTo(PREFIX_FILE)) { if (!STAGING_PREFIX_FILE.renameTo(PREFIX_FILE)) {
throw new RuntimeException("Unable to rename staging folder"); throw new RuntimeException("Unable to rename staging folder");
} }
activity.runOnUiThread(new Runnable() { activity.runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
whenDone.run(); whenDone.run();
} }
}); });
} catch (final Exception e) { } catch (final Exception e) {
Log.e(EmulatorDebug.LOG_TAG, "Bootstrap error", e); Log.e(EmulatorDebug.LOG_TAG, "Bootstrap error", e);
activity.runOnUiThread(new Runnable() { activity.runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
try { try {
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_body) new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_body)
.setNegativeButton(R.string.bootstrap_error_abort, new OnClickListener() { .setNegativeButton(R.string.bootstrap_error_abort, new OnClickListener() {
@Override @Override
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
dialog.dismiss(); dialog.dismiss();
activity.finish(); activity.finish();
} }
}).setPositiveButton(R.string.bootstrap_error_try_again, new OnClickListener() { }).setPositiveButton(R.string.bootstrap_error_try_again, new OnClickListener() {
@Override @Override
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
dialog.dismiss(); dialog.dismiss();
TermuxInstaller.setupIfNeeded(activity, whenDone); TermuxInstaller.setupIfNeeded(activity, whenDone);
} }
}).show(); }).show();
} catch (WindowManager.BadTokenException e) { } catch (WindowManager.BadTokenException e) {
// Activity already dismissed - ignore. // Activity already dismissed - ignore.
} }
} }
}); });
} finally { } finally {
activity.runOnUiThread(new Runnable() { activity.runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
try { try {
progress.dismiss(); progress.dismiss();
} catch (RuntimeException e) { } catch (RuntimeException e) {
// Activity already dismissed - ignore. // Activity already dismissed - ignore.
} }
} }
}); });
} }
} }
}.start(); }.start();
} }
/** Get bootstrap zip url for this systems cpu architecture. */ /** Get bootstrap zip url for this systems cpu architecture. */
static URL determineZipUrl() throws MalformedURLException { static URL determineZipUrl() throws MalformedURLException {
@@ -201,64 +204,64 @@ final class TermuxInstaller {
return new URL("https://termux.net/bootstrap/bootstrap-" + termuxArch + ".zip"); return new URL("https://termux.net/bootstrap/bootstrap-" + termuxArch + ".zip");
} }
/** Delete a folder and all its content or throw. */ /** Delete a folder and all its content or throw. */
static void deleteFolder(File fileOrDirectory) { static void deleteFolder(File fileOrDirectory) {
File[] children = fileOrDirectory.listFiles(); File[] children = fileOrDirectory.listFiles();
if (children != null) { if (children != null) {
for (File child : children) { for (File child : children) {
deleteFolder(child); deleteFolder(child);
} }
} }
if (!fileOrDirectory.delete()) { if (!fileOrDirectory.delete()) {
throw new RuntimeException("Unable to delete " + (fileOrDirectory.isDirectory() ? "directory " : "file ") + fileOrDirectory.getAbsolutePath()); throw new RuntimeException("Unable to delete " + (fileOrDirectory.isDirectory() ? "directory " : "file ") + fileOrDirectory.getAbsolutePath());
} }
} }
public static void setupStorageSymlinks(final Context context) { public static void setupStorageSymlinks(final Context context) {
final String LOG_TAG = "termux-storage"; final String LOG_TAG = "termux-storage";
new Thread() { new Thread() {
public void run() { public void run() {
try { try {
File storageDir = new File(TermuxService.HOME_PATH, "storage"); File storageDir = new File(TermuxService.HOME_PATH, "storage");
if (storageDir.exists() && !storageDir.delete()) { if (storageDir.exists() && !storageDir.delete()) {
Log.e(LOG_TAG, "Could not delete old $HOME/storage"); Log.e(LOG_TAG, "Could not delete old $HOME/storage");
return; return;
} }
if (!storageDir.mkdirs()) { if (!storageDir.mkdirs()) {
Log.e(LOG_TAG, "Unable to mkdirs() for $HOME/storage"); Log.e(LOG_TAG, "Unable to mkdirs() for $HOME/storage");
return; return;
} }
File sharedDir = Environment.getExternalStorageDirectory(); File sharedDir = Environment.getExternalStorageDirectory();
Os.symlink(sharedDir.getAbsolutePath(), new File(storageDir, "shared").getAbsolutePath()); Os.symlink(sharedDir.getAbsolutePath(), new File(storageDir, "shared").getAbsolutePath());
File downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); File downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
Os.symlink(downloadsDir.getAbsolutePath(), new File(storageDir, "downloads").getAbsolutePath()); Os.symlink(downloadsDir.getAbsolutePath(), new File(storageDir, "downloads").getAbsolutePath());
File dcimDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM); File dcimDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM);
Os.symlink(dcimDir.getAbsolutePath(), new File(storageDir, "dcim").getAbsolutePath()); Os.symlink(dcimDir.getAbsolutePath(), new File(storageDir, "dcim").getAbsolutePath());
File picturesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES); File picturesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
Os.symlink(picturesDir.getAbsolutePath(), new File(storageDir, "pictures").getAbsolutePath()); Os.symlink(picturesDir.getAbsolutePath(), new File(storageDir, "pictures").getAbsolutePath());
File musicDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC); File musicDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC);
Os.symlink(musicDir.getAbsolutePath(), new File(storageDir, "music").getAbsolutePath()); Os.symlink(musicDir.getAbsolutePath(), new File(storageDir, "music").getAbsolutePath());
File moviesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES); File moviesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES);
Os.symlink(moviesDir.getAbsolutePath(), new File(storageDir, "movies").getAbsolutePath()); Os.symlink(moviesDir.getAbsolutePath(), new File(storageDir, "movies").getAbsolutePath());
final File[] dirs = context.getExternalFilesDirs(null); final File[] dirs = context.getExternalFilesDirs(null);
if (dirs != null && dirs.length >= 2) { if (dirs != null && dirs.length >= 2) {
final File externalDir = dirs[1]; final File externalDir = dirs[1];
Os.symlink(externalDir.getAbsolutePath(), new File(storageDir, "external").getAbsolutePath()); Os.symlink(externalDir.getAbsolutePath(), new File(storageDir, "external").getAbsolutePath());
} }
} catch (Exception e) { } catch (Exception e) {
Log.e(LOG_TAG, "Error setting up link", e); Log.e(LOG_TAG, "Error setting up link", e);
} }
} }
}.start(); }.start();
} }
} }

View File

@@ -111,7 +111,8 @@ public final class TermuxKeyListener implements TerminalKeyListener {
mActivity.changeFontSize(false); mActivity.changeFontSize(false);
} else if (unicodeChar >= '1' && unicodeChar <= '9') { } else if (unicodeChar >= '1' && unicodeChar <= '9') {
int num = unicodeChar - '1'; int num = unicodeChar - '1';
if (service.getSessions().size() > num) mActivity.switchToSession(service.getSessions().get(num)); if (service.getSessions().size() > num)
mActivity.switchToSession(service.getSessions().get(num));
} }
return true; return true;
} }
@@ -235,7 +236,7 @@ public final class TermuxKeyListener implements TerminalKeyListener {
session.writeCodePoint(altDown, resultingCodePoint); session.writeCodePoint(altDown, resultingCodePoint);
} }
return true; return true;
} else if (ctrlDown) { } else if (ctrlDown) {
List<TermuxPreferences.KeyboardShortcut> shortcuts = mActivity.mSettings.shortcuts; List<TermuxPreferences.KeyboardShortcut> shortcuts = mActivity.mSettings.shortcuts;
if (!shortcuts.isEmpty()) { if (!shortcuts.isEmpty()) {
for (int i = shortcuts.size() - 1; i >= 0; i--) { for (int i = shortcuts.size() - 1; i >= 0; i--) {

View File

@@ -20,66 +20,67 @@ import java.util.Properties;
final class TermuxPreferences { final class TermuxPreferences {
@IntDef({BELL_VIBRATE, BELL_BEEP, BELL_IGNORE}) @IntDef({BELL_VIBRATE, BELL_BEEP, BELL_IGNORE})
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
public @interface AsciiBellBehaviour {} public @interface AsciiBellBehaviour {
}
static final int BELL_VIBRATE = 1; static final int BELL_VIBRATE = 1;
static final int BELL_BEEP = 2; static final int BELL_BEEP = 2;
static final int BELL_IGNORE = 3; static final int BELL_IGNORE = 3;
private final int MIN_FONTSIZE; private final int MIN_FONTSIZE;
private static final int MAX_FONTSIZE = 256; private static final int MAX_FONTSIZE = 256;
private static final String FULLSCREEN_KEY = "fullscreen"; private static final String FULLSCREEN_KEY = "fullscreen";
private static final String SHOW_EXTRA_KEYS_KEY = "show_extra_keys"; private static final String SHOW_EXTRA_KEYS_KEY = "show_extra_keys";
private static final String FONTSIZE_KEY = "fontsize"; private static final String FONTSIZE_KEY = "fontsize";
private static final String CURRENT_SESSION_KEY = "current_session"; private static final String CURRENT_SESSION_KEY = "current_session";
private static final String SHOW_WELCOME_DIALOG_KEY = "intro_dialog"; private static final String SHOW_WELCOME_DIALOG_KEY = "intro_dialog";
private boolean mFullScreen; private boolean mFullScreen;
private int mFontSize; private int mFontSize;
@AsciiBellBehaviour @AsciiBellBehaviour
int mBellBehaviour = BELL_VIBRATE; int mBellBehaviour = BELL_VIBRATE;
boolean mBackIsEscape; boolean mBackIsEscape;
boolean mShowExtraKeys; boolean mShowExtraKeys;
TermuxPreferences(Context context) { TermuxPreferences(Context context) {
reloadFromProperties(context); reloadFromProperties(context);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
float dipInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, context.getResources().getDisplayMetrics()); float dipInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, context.getResources().getDisplayMetrics());
// This is a bit arbitrary and sub-optimal. We want to give a sensible default for minimum font size // This is a bit arbitrary and sub-optimal. We want to give a sensible default for minimum font size
// to prevent invisible text due to zoom be mistake: // to prevent invisible text due to zoom be mistake:
MIN_FONTSIZE = (int) (4f * dipInPixels); MIN_FONTSIZE = (int) (4f * dipInPixels);
mFullScreen = prefs.getBoolean(FULLSCREEN_KEY, false); mFullScreen = prefs.getBoolean(FULLSCREEN_KEY, false);
mShowExtraKeys = prefs.getBoolean(SHOW_EXTRA_KEYS_KEY, false); mShowExtraKeys = prefs.getBoolean(SHOW_EXTRA_KEYS_KEY, false);
// http://www.google.com/design/spec/style/typography.html#typography-line-height // http://www.google.com/design/spec/style/typography.html#typography-line-height
int defaultFontSize = Math.round(12 * dipInPixels); int defaultFontSize = Math.round(12 * dipInPixels);
// Make it divisible by 2 since that is the minimal adjustment step: // Make it divisible by 2 since that is the minimal adjustment step:
if (defaultFontSize % 2 == 1) defaultFontSize--; if (defaultFontSize % 2 == 1) defaultFontSize--;
try { try {
mFontSize = Integer.parseInt(prefs.getString(FONTSIZE_KEY, Integer.toString(defaultFontSize))); mFontSize = Integer.parseInt(prefs.getString(FONTSIZE_KEY, Integer.toString(defaultFontSize)));
} catch (NumberFormatException | ClassCastException e) { } catch (NumberFormatException | ClassCastException e) {
mFontSize = defaultFontSize; mFontSize = defaultFontSize;
} }
mFontSize = Math.max(MIN_FONTSIZE, Math.min(mFontSize, MAX_FONTSIZE)); mFontSize = Math.max(MIN_FONTSIZE, Math.min(mFontSize, MAX_FONTSIZE));
} }
boolean isFullScreen() { boolean isFullScreen() {
return mFullScreen; return mFullScreen;
} }
void setFullScreen(Context context, boolean newValue) { void setFullScreen(Context context, boolean newValue) {
mFullScreen = newValue; mFullScreen = newValue;
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(FULLSCREEN_KEY, newValue).apply(); PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(FULLSCREEN_KEY, newValue).apply();
} }
boolean isShowExtraKeys() { boolean isShowExtraKeys() {
return mShowExtraKeys; return mShowExtraKeys;
@@ -92,74 +93,75 @@ final class TermuxPreferences {
} }
int getFontSize() { int getFontSize() {
return mFontSize; return mFontSize;
} }
void changeFontSize(Context context, boolean increase) { void changeFontSize(Context context, boolean increase) {
mFontSize += (increase ? 1 : -1) * 2; mFontSize += (increase ? 1 : -1) * 2;
mFontSize = Math.max(MIN_FONTSIZE, Math.min(mFontSize, MAX_FONTSIZE)); mFontSize = Math.max(MIN_FONTSIZE, Math.min(mFontSize, MAX_FONTSIZE));
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
prefs.edit().putString(FONTSIZE_KEY, Integer.toString(mFontSize)).apply(); prefs.edit().putString(FONTSIZE_KEY, Integer.toString(mFontSize)).apply();
} }
static void storeCurrentSession(Context context, TerminalSession session) { static void storeCurrentSession(Context context, TerminalSession session) {
PreferenceManager.getDefaultSharedPreferences(context).edit().putString(TermuxPreferences.CURRENT_SESSION_KEY, session.mHandle).commit(); PreferenceManager.getDefaultSharedPreferences(context).edit().putString(TermuxPreferences.CURRENT_SESSION_KEY, session.mHandle).commit();
} }
static TerminalSession getCurrentSession(TermuxActivity context) { static TerminalSession getCurrentSession(TermuxActivity context) {
String sessionHandle = PreferenceManager.getDefaultSharedPreferences(context).getString(TermuxPreferences.CURRENT_SESSION_KEY, ""); String sessionHandle = PreferenceManager.getDefaultSharedPreferences(context).getString(TermuxPreferences.CURRENT_SESSION_KEY, "");
for (int i = 0, len = context.mTermService.getSessions().size(); i < len; i++) { for (int i = 0, len = context.mTermService.getSessions().size(); i < len; i++) {
TerminalSession session = context.mTermService.getSessions().get(i); TerminalSession session = context.mTermService.getSessions().get(i);
if (session.mHandle.equals(sessionHandle)) return session; if (session.mHandle.equals(sessionHandle)) return session;
} }
return null; return null;
} }
public static boolean isShowWelcomeDialog(Context context) { public static boolean isShowWelcomeDialog(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SHOW_WELCOME_DIALOG_KEY, true); return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SHOW_WELCOME_DIALOG_KEY, true);
} }
public static void disableWelcomeDialog(Context context) { public static void disableWelcomeDialog(Context context) {
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(SHOW_WELCOME_DIALOG_KEY, false).apply(); PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(SHOW_WELCOME_DIALOG_KEY, false).apply();
} }
public void reloadFromProperties(Context context) { public void reloadFromProperties(Context context) {
try { try {
File propsFile = new File(TermuxService.HOME_PATH + "/.termux/termux.properties"); File propsFile = new File(TermuxService.HOME_PATH + "/.termux/termux.properties");
if (!propsFile.exists()) propsFile = new File(TermuxService.HOME_PATH + "/.config/termux/termux.properties"); if (!propsFile.exists())
propsFile = new File(TermuxService.HOME_PATH + "/.config/termux/termux.properties");
Properties props = new Properties(); Properties props = new Properties();
if (propsFile.isFile() && propsFile.canRead()) { if (propsFile.isFile() && propsFile.canRead()) {
try (FileInputStream in = new FileInputStream(propsFile)) { try (FileInputStream in = new FileInputStream(propsFile)) {
props.load(in); props.load(in);
} }
} }
switch (props.getProperty("bell-character", "vibrate")) { switch (props.getProperty("bell-character", "vibrate")) {
case "beep": case "beep":
mBellBehaviour = BELL_BEEP; mBellBehaviour = BELL_BEEP;
break; break;
case "ignore": case "ignore":
mBellBehaviour = BELL_IGNORE; mBellBehaviour = BELL_IGNORE;
break; break;
default: // "vibrate". default: // "vibrate".
mBellBehaviour = BELL_VIBRATE; mBellBehaviour = BELL_VIBRATE;
break; break;
} }
mBackIsEscape = "escape".equals(props.getProperty("back-key", "back")); mBackIsEscape = "escape".equals(props.getProperty("back-key", "back"));
shortcuts.clear(); shortcuts.clear();
parseAction("shortcut.create-session", SHORTCUT_ACTION_CREATE_SESSION, props); parseAction("shortcut.create-session", SHORTCUT_ACTION_CREATE_SESSION, props);
parseAction("shortcut.next-session", SHORTCUT_ACTION_NEXT_SESSION, props); parseAction("shortcut.next-session", SHORTCUT_ACTION_NEXT_SESSION, props);
parseAction("shortcut.previous-session", SHORTCUT_ACTION_PREVIOUS_SESSION, props); parseAction("shortcut.previous-session", SHORTCUT_ACTION_PREVIOUS_SESSION, props);
parseAction("shortcut.rename-session", SHORTCUT_ACTION_RENAME_SESSION, props); parseAction("shortcut.rename-session", SHORTCUT_ACTION_RENAME_SESSION, props);
} catch (Exception e) { } catch (Exception e) {
Toast.makeText(context, "Error loading properties: " + e.getMessage(), Toast.LENGTH_LONG).show(); Toast.makeText(context, "Error loading properties: " + e.getMessage(), Toast.LENGTH_LONG).show();
Log.e("termux", "Error loading props", e); Log.e("termux", "Error loading props", e);
} }
} }
public static final int SHORTCUT_ACTION_CREATE_SESSION = 1; public static final int SHORTCUT_ACTION_CREATE_SESSION = 1;
public static final int SHORTCUT_ACTION_NEXT_SESSION = 2; public static final int SHORTCUT_ACTION_NEXT_SESSION = 2;

View File

@@ -1,15 +1,5 @@
package com.termux.app; package com.termux.app;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import com.termux.R;
import com.termux.terminal.EmulatorDebug;
import com.termux.terminal.TerminalSession;
import com.termux.terminal.TerminalSession.SessionChangedCallback;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Notification; import android.app.Notification;
import android.app.NotificationManager; import android.app.NotificationManager;
@@ -26,331 +16,342 @@ import android.os.PowerManager;
import android.util.Log; import android.util.Log;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import com.termux.R;
import com.termux.terminal.EmulatorDebug;
import com.termux.terminal.TerminalSession;
import com.termux.terminal.TerminalSession.SessionChangedCallback;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/** /**
* A service holding a list of terminal sessions, {@link #mTerminalSessions}, showing a foreground notification while * A service holding a list of terminal sessions, {@link #mTerminalSessions}, showing a foreground notification while
* running so that it is not terminated. The user interacts with the session through {@link TermuxActivity}, but this * running so that it is not terminated. The user interacts with the session through {@link TermuxActivity}, but this
* service may outlive the activity when the user or the system disposes of the activity. In that case the user may * service may outlive the activity when the user or the system disposes of the activity. In that case the user may
* restart {@link TermuxActivity} later to yet again access the sessions. * restart {@link TermuxActivity} later to yet again access the sessions.
* * <p/>
* In order to keep both terminal sessions and spawned processes (who may outlive the terminal sessions) alive as long * In order to keep both terminal sessions and spawned processes (who may outlive the terminal sessions) alive as long
* as wanted by the user this service is a foreground service, {@link Service#startForeground(int, Notification)}. * as wanted by the user this service is a foreground service, {@link Service#startForeground(int, Notification)}.
* * <p/>
* Optionally may hold a wake and a wifi lock, in which case that is shown in the notification - see * Optionally may hold a wake and a wifi lock, in which case that is shown in the notification - see
* {@link #buildNotification()}. * {@link #buildNotification()}.
*/ */
public final class TermuxService extends Service implements SessionChangedCallback { public final class TermuxService extends Service implements SessionChangedCallback {
/** Note that this is a symlink on the Android M preview. */ /** Note that this is a symlink on the Android M preview. */
@SuppressLint("SdCardPath") @SuppressLint("SdCardPath")
public static final String FILES_PATH = "/data/data/com.termux/files"; public static final String FILES_PATH = "/data/data/com.termux/files";
public static final String PREFIX_PATH = FILES_PATH + "/usr"; public static final String PREFIX_PATH = FILES_PATH + "/usr";
public static final String HOME_PATH = FILES_PATH + "/home"; public static final String HOME_PATH = FILES_PATH + "/home";
private static final int NOTIFICATION_ID = 1337; private static final int NOTIFICATION_ID = 1337;
/** Intent action to stop the service. */ /** Intent action to stop the service. */
private static final String ACTION_STOP_SERVICE = "com.termux.service_stop"; private static final String ACTION_STOP_SERVICE = "com.termux.service_stop";
/** Intent action to toggle the wake lock, {@link #mWakeLock}, which this service may hold. */ /** Intent action to toggle the wake lock, {@link #mWakeLock}, which this service may hold. */
private static final String ACTION_LOCK_WAKE = "com.termux.service_toggle_wake_lock"; private static final String ACTION_LOCK_WAKE = "com.termux.service_toggle_wake_lock";
/** Intent action to toggle the wifi lock, {@link #mWifiLock}, which this service may hold. */ /** 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"; private static final String ACTION_LOCK_WIFI = "com.termux.service_toggle_wifi_lock";
/** Intent action to launch a new terminal session. Executed from TermuxWidgetProvider. */ /** Intent action to launch a new terminal session. Executed from TermuxWidgetProvider. */
public 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_ARGUMENTS = "com.termux.execute.arguments";
public static final String EXTRA_CURRENT_WORKING_DIRECTORY = "com.termux.execute.cwd"; 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. */ /** This service is only bound from inside the same process and never uses IPC. */
class LocalBinder extends Binder { class LocalBinder extends Binder {
public final TermuxService service = TermuxService.this; public final TermuxService service = TermuxService.this;
} }
private final IBinder mBinder = new LocalBinder(); private final IBinder mBinder = new LocalBinder();
/** /**
* The terminal sessions which this service manages. * The terminal sessions which this service manages.
* * <p/>
* Note that this list is observed by {@link TermuxActivity#mListViewAdapter}, so any changes must be made on the UI * Note that this list is observed by {@link TermuxActivity#mListViewAdapter}, so any changes must be made on the UI
* thread and followed by a call to {@link ArrayAdapter#notifyDataSetChanged()} }. * thread and followed by a call to {@link ArrayAdapter#notifyDataSetChanged()} }.
*/ */
final List<TerminalSession> mTerminalSessions = new ArrayList<>(); final List<TerminalSession> mTerminalSessions = new ArrayList<>();
/** Note that the service may often outlive the activity, so need to clear this reference. */ /** Note that the service may often outlive the activity, so need to clear this reference. */
SessionChangedCallback mSessionChangeCallback; SessionChangedCallback mSessionChangeCallback;
private PowerManager.WakeLock mWakeLock; private PowerManager.WakeLock mWakeLock;
private WifiManager.WifiLock mWifiLock; private WifiManager.WifiLock mWifiLock;
/** If the user has executed the {@link #ACTION_STOP_SERVICE} intent. */ /** If the user has executed the {@link #ACTION_STOP_SERVICE} intent. */
boolean mWantsToStop = false; boolean mWantsToStop = false;
@SuppressLint("Wakelock") @SuppressLint("Wakelock")
@Override @Override
public int onStartCommand(Intent intent, int flags, int startId) { public int onStartCommand(Intent intent, int flags, int startId) {
String action = intent.getAction(); String action = intent.getAction();
if (ACTION_STOP_SERVICE.equals(action)) { if (ACTION_STOP_SERVICE.equals(action)) {
mWantsToStop = true; mWantsToStop = true;
for (int i = 0; i < mTerminalSessions.size(); i++) for (int i = 0; i < mTerminalSessions.size(); i++)
mTerminalSessions.get(i).finishIfRunning(); mTerminalSessions.get(i).finishIfRunning();
stopSelf(); stopSelf();
} else if (ACTION_LOCK_WAKE.equals(action)) { } else if (ACTION_LOCK_WAKE.equals(action)) {
if (mWakeLock == null) { if (mWakeLock == null) {
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, EmulatorDebug.LOG_TAG); mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, EmulatorDebug.LOG_TAG);
mWakeLock.acquire(); mWakeLock.acquire();
} else { } else {
mWakeLock.release(); mWakeLock.release();
mWakeLock = null; mWakeLock = null;
} }
updateNotification(); updateNotification();
} else if (ACTION_LOCK_WIFI.equals(action)) { } else if (ACTION_LOCK_WIFI.equals(action)) {
if (mWifiLock == null) { if (mWifiLock == null) {
WifiManager wm = (WifiManager) getSystemService(Context.WIFI_SERVICE); WifiManager wm = (WifiManager) getSystemService(Context.WIFI_SERVICE);
mWifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, EmulatorDebug.LOG_TAG); mWifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, EmulatorDebug.LOG_TAG);
mWifiLock.acquire(); mWifiLock.acquire();
} else { } else {
mWifiLock.release(); mWifiLock.release();
mWifiLock = null; mWifiLock = null;
} }
updateNotification(); updateNotification();
} else if (ACTION_EXECUTE.equals(action)) { } else if (ACTION_EXECUTE.equals(action)) {
Uri executableUri = intent.getData(); Uri executableUri = intent.getData();
String executablePath = (executableUri == null ? null : executableUri.getPath()); String executablePath = (executableUri == null ? null : executableUri.getPath());
String[] arguments = (executableUri == null ? null : intent.getStringArrayExtra(EXTRA_ARGUMENTS)); String[] arguments = (executableUri == null ? null : intent.getStringArrayExtra(EXTRA_ARGUMENTS));
String cwd = intent.getStringExtra(EXTRA_CURRENT_WORKING_DIRECTORY); String cwd = intent.getStringExtra(EXTRA_CURRENT_WORKING_DIRECTORY);
TerminalSession newSession = createTermSession(executablePath, arguments, cwd, false); TerminalSession newSession = createTermSession(executablePath, arguments, cwd, false);
// Transform executable path to session name, e.g. "/bin/do-something.sh" => "do something.sh". // Transform executable path to session name, e.g. "/bin/do-something.sh" => "do something.sh".
if (executablePath != null) { if (executablePath != null) {
int lastSlash = executablePath.lastIndexOf('/'); int lastSlash = executablePath.lastIndexOf('/');
String name = (lastSlash == -1) ? executablePath : executablePath.substring(lastSlash + 1); String name = (lastSlash == -1) ? executablePath : executablePath.substring(lastSlash + 1);
name = name.replace('-', ' '); name = name.replace('-', ' ');
newSession.mSessionName = name; newSession.mSessionName = name;
} }
// Make the newly created session the current one to be displayed: // Make the newly created session the current one to be displayed:
TermuxPreferences.storeCurrentSession(this, newSession); TermuxPreferences.storeCurrentSession(this, newSession);
// Launch the main Termux app, which will now show to current session: // Launch the main Termux app, which will now show to current session:
startActivity(new Intent(this, TermuxActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); startActivity(new Intent(this, TermuxActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
} else if (action != null) { } else if (action != null) {
Log.e(EmulatorDebug.LOG_TAG, "Unknown TermuxService action: '" + action + "'"); Log.e(EmulatorDebug.LOG_TAG, "Unknown TermuxService action: '" + action + "'");
} }
// If this service really do get killed, there is no point restarting it automatically - let the user do on next // If this service really do get killed, there is no point restarting it automatically - let the user do on next
// start of {@link Term): // start of {@link Term):
return Service.START_NOT_STICKY; return Service.START_NOT_STICKY;
} }
@Override @Override
public IBinder onBind(Intent intent) { public IBinder onBind(Intent intent) {
return mBinder; return mBinder;
} }
@Override @Override
public void onCreate() { public void onCreate() {
startForeground(NOTIFICATION_ID, buildNotification()); startForeground(NOTIFICATION_ID, buildNotification());
} }
/** Update the shown foreground service notification after making any changes that affect it. */ /** Update the shown foreground service notification after making any changes that affect it. */
private void updateNotification() { private void updateNotification() {
if (mWakeLock == null && mWifiLock == null && getSessions().isEmpty()) { if (mWakeLock == null && mWifiLock == null && getSessions().isEmpty()) {
// Exit if we are updating after the user disabled all locks with no sessions. // Exit if we are updating after the user disabled all locks with no sessions.
stopSelf(); stopSelf();
} else { } else {
((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).notify(NOTIFICATION_ID, buildNotification()); ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).notify(NOTIFICATION_ID, buildNotification());
} }
} }
private Notification buildNotification() { private Notification buildNotification() {
Intent notifyIntent = new Intent(this, TermuxActivity.class); Intent notifyIntent = new Intent(this, TermuxActivity.class);
// PendingIntent#getActivity(): "Note that the activity will be started outside of the context of an existing // PendingIntent#getActivity(): "Note that the activity will be started outside of the context of an existing
// activity, so you must use the Intent.FLAG_ACTIVITY_NEW_TASK launch flag in the Intent": // activity, so you must use the Intent.FLAG_ACTIVITY_NEW_TASK launch flag in the Intent":
notifyIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); notifyIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notifyIntent, 0); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notifyIntent, 0);
int sessionCount = mTerminalSessions.size(); int sessionCount = mTerminalSessions.size();
String contentText = sessionCount + " terminal session" + (sessionCount == 1 ? "" : "s"); String contentText = sessionCount + " terminal session" + (sessionCount == 1 ? "" : "s");
boolean wakeLockHeld = mWakeLock != null; boolean wakeLockHeld = mWakeLock != null;
boolean wifiLockHeld = mWifiLock != null; boolean wifiLockHeld = mWifiLock != null;
if (wakeLockHeld && wifiLockHeld) { if (wakeLockHeld && wifiLockHeld) {
contentText += " (wake&wifi lock held)"; contentText += " (wake&wifi lock held)";
} else if (wakeLockHeld) { } else if (wakeLockHeld) {
contentText += " (wake lock held)"; contentText += " (wake lock held)";
} else if (wifiLockHeld) { } else if (wifiLockHeld) {
contentText += " (wifi lock held)"; contentText += " (wifi lock held)";
} }
Notification.Builder builder = new Notification.Builder(this); Notification.Builder builder = new Notification.Builder(this);
builder.setContentTitle(getText(R.string.application_name)); builder.setContentTitle(getText(R.string.application_name));
builder.setContentText(contentText); builder.setContentText(contentText);
builder.setSmallIcon(R.drawable.ic_service_notification); builder.setSmallIcon(R.drawable.ic_service_notification);
builder.setContentIntent(pendingIntent); builder.setContentIntent(pendingIntent);
builder.setOngoing(true); builder.setOngoing(true);
// If holding a wake or wifi lock consider the notification of high priority since it's using power, // If holding a wake or wifi lock consider the notification of high priority since it's using power,
// otherwise use a minimal priority since this is just a background service notification: // otherwise use a minimal priority since this is just a background service notification:
builder.setPriority((wakeLockHeld || wifiLockHeld) ? Notification.PRIORITY_HIGH : Notification.PRIORITY_MIN); builder.setPriority((wakeLockHeld || wifiLockHeld) ? Notification.PRIORITY_HIGH : Notification.PRIORITY_MIN);
// No need to show a timestamp: // No need to show a timestamp:
builder.setShowWhen(false); builder.setShowWhen(false);
// Background color for small notification icon: // Background color for small notification icon:
builder.setColor(0xFF000000); builder.setColor(0xFF000000);
Resources res = getResources(); Resources res = getResources();
Intent exitIntent = new Intent(this, TermuxService.class).setAction(ACTION_STOP_SERVICE); Intent exitIntent = new Intent(this, TermuxService.class).setAction(ACTION_STOP_SERVICE);
builder.addAction(android.R.drawable.ic_delete, res.getString(R.string.notification_action_exit), PendingIntent.getService(this, 0, exitIntent, 0)); builder.addAction(android.R.drawable.ic_delete, res.getString(R.string.notification_action_exit), PendingIntent.getService(this, 0, exitIntent, 0));
Intent toggleWakeLockIntent = new Intent(this, TermuxService.class).setAction(ACTION_LOCK_WAKE); Intent toggleWakeLockIntent = new Intent(this, TermuxService.class).setAction(ACTION_LOCK_WAKE);
builder.addAction(android.R.drawable.ic_lock_lock, res.getString(R.string.notification_action_wakelock), builder.addAction(android.R.drawable.ic_lock_lock, res.getString(R.string.notification_action_wakelock),
PendingIntent.getService(this, 0, toggleWakeLockIntent, 0)); PendingIntent.getService(this, 0, toggleWakeLockIntent, 0));
Intent toggleWifiLockIntent = new Intent(this, TermuxService.class).setAction(ACTION_LOCK_WIFI); Intent toggleWifiLockIntent = new Intent(this, TermuxService.class).setAction(ACTION_LOCK_WIFI);
builder.addAction(android.R.drawable.ic_lock_lock, res.getString(R.string.notification_action_wifilock), builder.addAction(android.R.drawable.ic_lock_lock, res.getString(R.string.notification_action_wifilock),
PendingIntent.getService(this, 0, toggleWifiLockIntent, 0)); PendingIntent.getService(this, 0, toggleWifiLockIntent, 0));
return builder.build(); return builder.build();
} }
@Override @Override
public void onDestroy() { public void onDestroy() {
if (mWakeLock != null) mWakeLock.release(); if (mWakeLock != null) mWakeLock.release();
if (mWifiLock != null) mWifiLock.release(); if (mWifiLock != null) mWifiLock.release();
stopForeground(true); stopForeground(true);
for (int i = 0; i < mTerminalSessions.size(); i++) for (int i = 0; i < mTerminalSessions.size(); i++)
mTerminalSessions.get(i).finishIfRunning(); mTerminalSessions.get(i).finishIfRunning();
mTerminalSessions.clear(); mTerminalSessions.clear();
} }
public List<TerminalSession> getSessions() { public List<TerminalSession> getSessions() {
return mTerminalSessions; return mTerminalSessions;
} }
TerminalSession createTermSession(String executablePath, String[] arguments, String cwd, boolean failSafe) { TerminalSession createTermSession(String executablePath, String[] arguments, String cwd, boolean failSafe) {
new File(HOME_PATH).mkdirs(); new File(HOME_PATH).mkdirs();
if (cwd == null) cwd = HOME_PATH; if (cwd == null) cwd = HOME_PATH;
final String termEnv = "TERM=xterm-256color"; final String termEnv = "TERM=xterm-256color";
final String homeEnv = "HOME=" + HOME_PATH; final String homeEnv = "HOME=" + HOME_PATH;
final String prefixEnv = "PREFIX=" + PREFIX_PATH; final String prefixEnv = "PREFIX=" + PREFIX_PATH;
final String androidRootEnv = "ANDROID_ROOT=" + System.getenv("ANDROID_ROOT"); final String androidRootEnv = "ANDROID_ROOT=" + System.getenv("ANDROID_ROOT");
final String androidDataEnv = "ANDROID_DATA=" + System.getenv("ANDROID_DATA"); final String androidDataEnv = "ANDROID_DATA=" + System.getenv("ANDROID_DATA");
// EXTERNAL_STORAGE is needed for /system/bin/am to work on at least // EXTERNAL_STORAGE is needed for /system/bin/am to work on at least
// Samsung S7 - see https://plus.google.com/110070148244138185604/posts/gp8Lk3aCGp3. // Samsung S7 - see https://plus.google.com/110070148244138185604/posts/gp8Lk3aCGp3.
final String externalStorageEnv = "EXTERNAL_STORAGE=" + System.getenv("EXTERNAL_STORAGE"); final String externalStorageEnv = "EXTERNAL_STORAGE=" + System.getenv("EXTERNAL_STORAGE");
String[] env; String[] env;
if (failSafe) { if (failSafe) {
// Keep the default path so that system binaries can be used in the failsafe session. // Keep the default path so that system binaries can be used in the failsafe session.
final String pathEnv = "PATH=" + System.getenv("PATH"); final String pathEnv = "PATH=" + System.getenv("PATH");
env = new String[] { termEnv, homeEnv, prefixEnv, androidRootEnv, androidDataEnv, pathEnv, externalStorageEnv }; env = new String[]{termEnv, homeEnv, prefixEnv, androidRootEnv, androidDataEnv, pathEnv, externalStorageEnv};
} else { } else {
final String ps1Env = "PS1=$ "; final String ps1Env = "PS1=$ ";
final String ldEnv = "LD_LIBRARY_PATH=" + PREFIX_PATH + "/lib"; final String ldEnv = "LD_LIBRARY_PATH=" + PREFIX_PATH + "/lib";
final String langEnv = "LANG=en_US.UTF-8"; final String langEnv = "LANG=en_US.UTF-8";
final String pathEnv = "PATH=" + PREFIX_PATH + "/bin:" + PREFIX_PATH + "/bin/applets"; final String pathEnv = "PATH=" + PREFIX_PATH + "/bin:" + PREFIX_PATH + "/bin/applets";
final String pwdEnv = "PWD=" + cwd; final String pwdEnv = "PWD=" + cwd;
env = new String[] { termEnv, homeEnv, prefixEnv, ps1Env, ldEnv, langEnv, pathEnv, pwdEnv, androidRootEnv, androidDataEnv, externalStorageEnv }; env = new String[]{termEnv, homeEnv, prefixEnv, ps1Env, ldEnv, langEnv, pathEnv, pwdEnv, androidRootEnv, androidDataEnv, externalStorageEnv};
} }
String shellName; String shellName;
if (executablePath == null) { if (executablePath == null) {
File shell = new File(HOME_PATH, ".termux/shell"); File shell = new File(HOME_PATH, ".termux/shell");
if (shell.exists()) { if (shell.exists()) {
try { try {
File canonicalFile = shell.getCanonicalFile(); File canonicalFile = shell.getCanonicalFile();
if (canonicalFile.isFile() && canonicalFile.canExecute()) { if (canonicalFile.isFile() && canonicalFile.canExecute()) {
executablePath = canonicalFile.getName().equals("busybox") ? (PREFIX_PATH + "/bin/ash") : canonicalFile.getAbsolutePath(); executablePath = canonicalFile.getName().equals("busybox") ? (PREFIX_PATH + "/bin/ash") : canonicalFile.getAbsolutePath();
} else { } else {
Log.w(EmulatorDebug.LOG_TAG, "$HOME/.termux/shell points to non-executable shell: " + canonicalFile.getAbsolutePath()); Log.w(EmulatorDebug.LOG_TAG, "$HOME/.termux/shell points to non-executable shell: " + canonicalFile.getAbsolutePath());
} }
} catch (IOException e) { } catch (IOException e) {
Log.e(EmulatorDebug.LOG_TAG, "Error checking $HOME/.termux/shell", e); Log.e(EmulatorDebug.LOG_TAG, "Error checking $HOME/.termux/shell", e);
} }
} }
if (executablePath == null) { if (executablePath == null) {
// Try bash, zsh and ash in that order: // Try bash, zsh and ash in that order:
for (String shellBinary : new String[] { "bash", "zsh", "ash" }) { for (String shellBinary : new String[]{"bash", "zsh", "ash"}) {
File shellFile = new File(PREFIX_PATH + "/bin/" + shellBinary); File shellFile = new File(PREFIX_PATH + "/bin/" + shellBinary);
if (shellFile.canExecute()) { if (shellFile.canExecute()) {
executablePath = shellFile.getAbsolutePath(); executablePath = shellFile.getAbsolutePath();
break; break;
} }
} }
} }
if (executablePath == null) { if (executablePath == null) {
// Fall back to system shell as last resort: // Fall back to system shell as last resort:
executablePath = "/system/bin/sh"; executablePath = "/system/bin/sh";
} }
String[] parts = executablePath.split("/"); String[] parts = executablePath.split("/");
shellName = "-" + parts[parts.length - 1]; shellName = "-" + parts[parts.length - 1];
} else { } else {
int lastSlashIndex = executablePath.lastIndexOf('/'); int lastSlashIndex = executablePath.lastIndexOf('/');
shellName = lastSlashIndex == -1 ? executablePath : executablePath.substring(lastSlashIndex + 1); shellName = lastSlashIndex == -1 ? executablePath : executablePath.substring(lastSlashIndex + 1);
} }
String[] args; String[] args;
if (arguments == null) { if (arguments == null) {
args = new String[] { shellName }; args = new String[]{shellName};
} else { } else {
args = new String[arguments.length + 1]; args = new String[arguments.length + 1];
args[0] = shellName; args[0] = shellName;
System.arraycopy(arguments, 0, args, 1, arguments.length); System.arraycopy(arguments, 0, args, 1, arguments.length);
} }
TerminalSession session = new TerminalSession(executablePath, cwd, args, env, this); TerminalSession session = new TerminalSession(executablePath, cwd, args, env, this);
mTerminalSessions.add(session); mTerminalSessions.add(session);
updateNotification(); updateNotification();
return session; return session;
} }
public int removeTermSession(TerminalSession sessionToRemove) { public int removeTermSession(TerminalSession sessionToRemove) {
int indexOfRemoved = mTerminalSessions.indexOf(sessionToRemove); int indexOfRemoved = mTerminalSessions.indexOf(sessionToRemove);
mTerminalSessions.remove(indexOfRemoved); mTerminalSessions.remove(indexOfRemoved);
if (mTerminalSessions.isEmpty() && mWakeLock == null) { if (mTerminalSessions.isEmpty() && mWakeLock == null) {
// Finish if there are no sessions left and the wake lock is not held, otherwise keep the service alive if // Finish if there are no sessions left and the wake lock is not held, otherwise keep the service alive if
// holding wake lock since there may be daemon processes (e.g. sshd) running. // holding wake lock since there may be daemon processes (e.g. sshd) running.
stopSelf(); stopSelf();
} else { } else {
updateNotification(); updateNotification();
} }
return indexOfRemoved; return indexOfRemoved;
} }
@Override @Override
public void onTitleChanged(TerminalSession changedSession) { public void onTitleChanged(TerminalSession changedSession) {
if (mSessionChangeCallback != null) mSessionChangeCallback.onTitleChanged(changedSession); if (mSessionChangeCallback != null) mSessionChangeCallback.onTitleChanged(changedSession);
} }
@Override @Override
public void onSessionFinished(final TerminalSession finishedSession) { public void onSessionFinished(final TerminalSession finishedSession) {
if (mSessionChangeCallback != null) mSessionChangeCallback.onSessionFinished(finishedSession); if (mSessionChangeCallback != null)
} mSessionChangeCallback.onSessionFinished(finishedSession);
}
@Override @Override
public void onTextChanged(TerminalSession changedSession) { public void onTextChanged(TerminalSession changedSession) {
if (mSessionChangeCallback != null) mSessionChangeCallback.onTextChanged(changedSession); if (mSessionChangeCallback != null) mSessionChangeCallback.onTextChanged(changedSession);
} }
@Override @Override
public void onClipboardText(TerminalSession session, String text) { public void onClipboardText(TerminalSession session, String text) {
if (mSessionChangeCallback != null) mSessionChangeCallback.onClipboardText(session, text); if (mSessionChangeCallback != null) mSessionChangeCallback.onClipboardText(session, text);
} }
@Override @Override
public void onBell(TerminalSession session) { public void onBell(TerminalSession session) {
if (mSessionChangeCallback != null) mSessionChangeCallback.onBell(session); if (mSessionChangeCallback != null) mSessionChangeCallback.onBell(session);
} }
@Override @Override
public void onColorsChanged(TerminalSession session) { public void onColorsChanged(TerminalSession session) {

View File

@@ -23,9 +23,9 @@ import java.util.LinkedList;
/** /**
* A document provider for the Storage Access Framework which exposes the files in the * A document provider for the Storage Access Framework which exposes the files in the
* $HOME/ folder to other apps. * $HOME/ folder to other apps.
* <p> * <p/>
* Note that this replaces providing an activity matching the ACTION_GET_CONTENT intent: * Note that this replaces providing an activity matching the ACTION_GET_CONTENT intent:
* <p> * <p/>
* "A document provider and ACTION_GET_CONTENT should be considered mutually exclusive. If you * "A document provider and ACTION_GET_CONTENT should be considered mutually exclusive. If you
* support both of them simultaneously, your app will appear twice in the system picker UI, * support both of them simultaneously, your app will appear twice in the system picker UI,
* offering two different ways of accessing your stored data. This would be confusing for users." * offering two different ways of accessing your stored data. This would be confusing for users."
@@ -172,7 +172,7 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
/** /**
* Get the document id given a file. This document id must be consistent across time as other * Get the document id given a file. This document id must be consistent across time as other
* applications may save the ID and use it to reference documents later. * applications may save the ID and use it to reference documents later.
* <p> * <p/>
* The reverse of @{link #getFileForDocId}. * The reverse of @{link #getFileForDocId}.
*/ */
private static String getDocIdForFile(File file) { private static String getDocIdForFile(File file) {

View File

@@ -3,106 +3,106 @@ package com.termux.terminal;
/** A circular byte buffer allowing one producer and one consumer thread. */ /** A circular byte buffer allowing one producer and one consumer thread. */
final class ByteQueue { final class ByteQueue {
private final byte[] mBuffer; private final byte[] mBuffer;
private int mHead; private int mHead;
private int mStoredBytes; private int mStoredBytes;
private boolean mOpen = true; private boolean mOpen = true;
public ByteQueue(int size) { public ByteQueue(int size) {
mBuffer = new byte[size]; mBuffer = new byte[size];
} }
public synchronized void close() { public synchronized void close() {
mOpen = false; mOpen = false;
notify(); notify();
} }
public synchronized int read(byte[] buffer, boolean block) { public synchronized int read(byte[] buffer, boolean block) {
while (mStoredBytes == 0 && mOpen) { while (mStoredBytes == 0 && mOpen) {
if (block) { if (block) {
try { try {
wait(); wait();
} catch (InterruptedException e) { } catch (InterruptedException e) {
// Ignore. // Ignore.
} }
} else { } else {
return 0; return 0;
} }
} }
if (!mOpen) return -1; if (!mOpen) return -1;
int totalRead = 0; int totalRead = 0;
int bufferLength = mBuffer.length; int bufferLength = mBuffer.length;
boolean wasFull = bufferLength == mStoredBytes; boolean wasFull = bufferLength == mStoredBytes;
int length = buffer.length; int length = buffer.length;
int offset = 0; int offset = 0;
while (length > 0 && mStoredBytes > 0) { while (length > 0 && mStoredBytes > 0) {
int oneRun = Math.min(bufferLength - mHead, mStoredBytes); int oneRun = Math.min(bufferLength - mHead, mStoredBytes);
int bytesToCopy = Math.min(length, oneRun); int bytesToCopy = Math.min(length, oneRun);
System.arraycopy(mBuffer, mHead, buffer, offset, bytesToCopy); System.arraycopy(mBuffer, mHead, buffer, offset, bytesToCopy);
mHead += bytesToCopy; mHead += bytesToCopy;
if (mHead >= bufferLength) mHead = 0; if (mHead >= bufferLength) mHead = 0;
mStoredBytes -= bytesToCopy; mStoredBytes -= bytesToCopy;
length -= bytesToCopy; length -= bytesToCopy;
offset += bytesToCopy; offset += bytesToCopy;
totalRead += bytesToCopy; totalRead += bytesToCopy;
} }
if (wasFull) notify(); if (wasFull) notify();
return totalRead; return totalRead;
} }
/** /**
* Attempt to write the specified portion of the provided buffer to the queue. * Attempt to write the specified portion of the provided buffer to the queue.
* * <p/>
* Returns whether the output was totally written, false if it was closed before. * Returns whether the output was totally written, false if it was closed before.
*/ */
public boolean write(byte[] buffer, int offset, int lengthToWrite) { public boolean write(byte[] buffer, int offset, int lengthToWrite) {
if (lengthToWrite + offset > buffer.length) { if (lengthToWrite + offset > buffer.length) {
throw new IllegalArgumentException("length + offset > buffer.length"); throw new IllegalArgumentException("length + offset > buffer.length");
} else if (lengthToWrite <= 0) { } else if (lengthToWrite <= 0) {
throw new IllegalArgumentException("length <= 0"); throw new IllegalArgumentException("length <= 0");
} }
final int bufferLength = mBuffer.length; final int bufferLength = mBuffer.length;
synchronized (this) { synchronized (this) {
while (lengthToWrite > 0) { while (lengthToWrite > 0) {
while (bufferLength == mStoredBytes && mOpen) { while (bufferLength == mStoredBytes && mOpen) {
try { try {
wait(); wait();
} catch (InterruptedException e) { } catch (InterruptedException e) {
// Ignore. // Ignore.
} }
} }
if (!mOpen) return false; if (!mOpen) return false;
final boolean wasEmpty = mStoredBytes == 0; final boolean wasEmpty = mStoredBytes == 0;
int bytesToWriteBeforeWaiting = Math.min(lengthToWrite, bufferLength - mStoredBytes); int bytesToWriteBeforeWaiting = Math.min(lengthToWrite, bufferLength - mStoredBytes);
lengthToWrite -= bytesToWriteBeforeWaiting; lengthToWrite -= bytesToWriteBeforeWaiting;
while (bytesToWriteBeforeWaiting > 0) { while (bytesToWriteBeforeWaiting > 0) {
int tail = mHead + mStoredBytes; int tail = mHead + mStoredBytes;
int oneRun; int oneRun;
if (tail >= bufferLength) { if (tail >= bufferLength) {
// Buffer: [.............] // Buffer: [.............]
// ________________H_______T // ________________H_______T
// => // =>
// Buffer: [.............] // Buffer: [.............]
// ___________T____H // ___________T____H
// onRun= _____----_ // onRun= _____----_
tail = tail - bufferLength; tail = tail - bufferLength;
oneRun = mHead - tail; oneRun = mHead - tail;
} else { } else {
oneRun = bufferLength - tail; oneRun = bufferLength - tail;
} }
int bytesToCopy = Math.min(oneRun, bytesToWriteBeforeWaiting); int bytesToCopy = Math.min(oneRun, bytesToWriteBeforeWaiting);
System.arraycopy(buffer, offset, mBuffer, tail, bytesToCopy); System.arraycopy(buffer, offset, mBuffer, tail, bytesToCopy);
offset += bytesToCopy; offset += bytesToCopy;
bytesToWriteBeforeWaiting -= bytesToCopy; bytesToWriteBeforeWaiting -= bytesToCopy;
mStoredBytes += bytesToCopy; mStoredBytes += bytesToCopy;
} }
if (wasEmpty) notify(); if (wasEmpty) notify();
} }
} }
return true; return true;
} }
} }

View File

@@ -4,7 +4,7 @@ import android.util.Log;
public final class EmulatorDebug { public final class EmulatorDebug {
/** The tag to use with {@link Log}. */ /** The tag to use with {@link Log}. */
public static final String LOG_TAG = "termux"; public static final String LOG_TAG = "termux";
} }

View File

@@ -5,42 +5,37 @@ package com.termux.terminal;
*/ */
final class JNI { final class JNI {
static { static {
System.loadLibrary("termux"); System.loadLibrary("termux");
} }
/** /**
* Create a subprocess. Differs from {@link ProcessBuilder} in that a pseudoterminal is used to communicate with the * Create a subprocess. Differs from {@link ProcessBuilder} in that a pseudoterminal is used to communicate with the
* subprocess. * subprocess.
* * <p/>
* Callers are responsible for calling {@link #close(int)} on the returned file descriptor. * Callers are responsible for calling {@link #close(int)} on the returned file descriptor.
* *
* @param cmd * @param cmd The command to execute
* The command to execute * @param cwd The current working directory for the executed command
* @param cwd * @param args An array of arguments to the command
* The current working directory for the executed command * @param envVars An array of strings of the form "VAR=value" to be added to the environment of the process
* @param args * @param processId A one-element array to which the process ID of the started process will be written.
* An array of arguments to the command * @return the file descriptor resulting from opening /dev/ptmx master device. The sub process will have opened the
* @param envVars * slave device counterpart (/dev/pts/$N) and have it as stdint, stdout and stderr.
* An array of strings of the form "VAR=value" to be added to the environment of the process */
* @param processId public static native int createSubprocess(String cmd, String cwd, String[] args, String[] envVars, int[] processId, int rows, int columns);
* A one-element array to which the process ID of the started process will be written.
* @return the file descriptor resulting from opening /dev/ptmx master device. The sub process will have opened the
* slave device counterpart (/dev/pts/$N) and have it as stdint, stdout and stderr.
*/
public static native int createSubprocess(String cmd, String cwd, String[] args, String[] envVars, int[] processId, int rows, int columns);
/** Set the window size for a given pty, which allows connected programs to learn how large their screen is. */ /** 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); public static native void setPtyWindowSize(int fd, int rows, int cols);
/** /**
* Causes the calling thread to wait for the process associated with the receiver to finish executing. * Causes the calling thread to wait for the process associated with the receiver to finish executing.
* *
* @return if >= 0, the exit status of the process. If < 0, the signal causing the process to stop negated. * @return if >= 0, the exit status of the process. If < 0, the signal causing the process to stop negated.
*/ */
public static native int waitFor(int processId); public static native int waitFor(int processId);
/** Close a file descriptor through the close(2) system call. */ /** Close a file descriptor through the close(2) system call. */
public static native void close(int fileDescriptor); public static native void close(int fileDescriptor);
} }

View File

@@ -1,5 +1,10 @@
package com.termux.terminal; package com.termux.terminal;
import android.view.KeyEvent;
import java.util.HashMap;
import java.util.Map;
import static android.view.KeyEvent.KEYCODE_BREAK; import static android.view.KeyEvent.KEYCODE_BREAK;
import static android.view.KeyEvent.KEYCODE_DEL; import static android.view.KeyEvent.KEYCODE_DEL;
import static android.view.KeyEvent.KEYCODE_DPAD_CENTER; import static android.view.KeyEvent.KEYCODE_DPAD_CENTER;
@@ -22,6 +27,7 @@ import static android.view.KeyEvent.KEYCODE_F7;
import static android.view.KeyEvent.KEYCODE_F8; import static android.view.KeyEvent.KEYCODE_F8;
import static android.view.KeyEvent.KEYCODE_F9; import static android.view.KeyEvent.KEYCODE_F9;
import static android.view.KeyEvent.KEYCODE_FORWARD_DEL; import static android.view.KeyEvent.KEYCODE_FORWARD_DEL;
import static android.view.KeyEvent.KEYCODE_HOME;
import static android.view.KeyEvent.KEYCODE_INSERT; import static android.view.KeyEvent.KEYCODE_INSERT;
import static android.view.KeyEvent.KEYCODE_MOVE_END; import static android.view.KeyEvent.KEYCODE_MOVE_END;
import static android.view.KeyEvent.KEYCODE_NUMPAD_0; import static android.view.KeyEvent.KEYCODE_NUMPAD_0;
@@ -47,264 +53,259 @@ import static android.view.KeyEvent.KEYCODE_PAGE_DOWN;
import static android.view.KeyEvent.KEYCODE_PAGE_UP; import static android.view.KeyEvent.KEYCODE_PAGE_UP;
import static android.view.KeyEvent.KEYCODE_SYSRQ; import static android.view.KeyEvent.KEYCODE_SYSRQ;
import static android.view.KeyEvent.KEYCODE_TAB; import static android.view.KeyEvent.KEYCODE_TAB;
import static android.view.KeyEvent.KEYCODE_HOME;
import java.util.HashMap;
import java.util.Map;
import android.view.KeyEvent;
public final class KeyHandler { public final class KeyHandler {
public static final int KEYMOD_ALT = 0x80000000; public static final int KEYMOD_ALT = 0x80000000;
public static final int KEYMOD_CTRL = 0x40000000; public static final int KEYMOD_CTRL = 0x40000000;
public static final int KEYMOD_SHIFT = 0x20000000; public static final int KEYMOD_SHIFT = 0x20000000;
private static final Map<String, Integer> TERMCAP_TO_KEYCODE = new HashMap<>(); private static final Map<String, Integer> TERMCAP_TO_KEYCODE = new HashMap<>();
static {
// terminfo: http://pubs.opengroup.org/onlinepubs/7990989799/xcurses/terminfo.html
// termcap: http://man7.org/linux/man-pages/man5/termcap.5.html
TERMCAP_TO_KEYCODE.put("%i", KEYMOD_SHIFT | KEYCODE_DPAD_RIGHT);
TERMCAP_TO_KEYCODE.put("#2", KEYMOD_SHIFT | KEYCODE_HOME); // Shifted home
TERMCAP_TO_KEYCODE.put("#4", KEYMOD_SHIFT | KEYCODE_DPAD_LEFT);
TERMCAP_TO_KEYCODE.put("*7", KEYMOD_SHIFT | KEYCODE_MOVE_END); // Shifted end key
TERMCAP_TO_KEYCODE.put("k1", KEYCODE_F1); static {
TERMCAP_TO_KEYCODE.put("k2", KEYCODE_F2); // terminfo: http://pubs.opengroup.org/onlinepubs/7990989799/xcurses/terminfo.html
TERMCAP_TO_KEYCODE.put("k3", KEYCODE_F3); // termcap: http://man7.org/linux/man-pages/man5/termcap.5.html
TERMCAP_TO_KEYCODE.put("k4", KEYCODE_F4); TERMCAP_TO_KEYCODE.put("%i", KEYMOD_SHIFT | KEYCODE_DPAD_RIGHT);
TERMCAP_TO_KEYCODE.put("k5", KEYCODE_F5); TERMCAP_TO_KEYCODE.put("#2", KEYMOD_SHIFT | KEYCODE_HOME); // Shifted home
TERMCAP_TO_KEYCODE.put("k6", KEYCODE_F6); TERMCAP_TO_KEYCODE.put("#4", KEYMOD_SHIFT | KEYCODE_DPAD_LEFT);
TERMCAP_TO_KEYCODE.put("k7", KEYCODE_F7); TERMCAP_TO_KEYCODE.put("*7", KEYMOD_SHIFT | KEYCODE_MOVE_END); // Shifted end key
TERMCAP_TO_KEYCODE.put("k8", KEYCODE_F8);
TERMCAP_TO_KEYCODE.put("k9", KEYCODE_F9);
TERMCAP_TO_KEYCODE.put("k;", KEYCODE_F10);
TERMCAP_TO_KEYCODE.put("F1", KEYCODE_F11);
TERMCAP_TO_KEYCODE.put("F2", KEYCODE_F12);
TERMCAP_TO_KEYCODE.put("F3", KEYMOD_SHIFT | KEYCODE_F1);
TERMCAP_TO_KEYCODE.put("F4", KEYMOD_SHIFT | KEYCODE_F2);
TERMCAP_TO_KEYCODE.put("F5", KEYMOD_SHIFT | KEYCODE_F3);
TERMCAP_TO_KEYCODE.put("F6", KEYMOD_SHIFT | KEYCODE_F4);
TERMCAP_TO_KEYCODE.put("F7", KEYMOD_SHIFT | KEYCODE_F5);
TERMCAP_TO_KEYCODE.put("F8", KEYMOD_SHIFT | KEYCODE_F6);
TERMCAP_TO_KEYCODE.put("F9", KEYMOD_SHIFT | KEYCODE_F7);
TERMCAP_TO_KEYCODE.put("FA", KEYMOD_SHIFT | KEYCODE_F8);
TERMCAP_TO_KEYCODE.put("FB", KEYMOD_SHIFT | KEYCODE_F9);
TERMCAP_TO_KEYCODE.put("FC", KEYMOD_SHIFT | KEYCODE_F10);
TERMCAP_TO_KEYCODE.put("FD", KEYMOD_SHIFT | KEYCODE_F11);
TERMCAP_TO_KEYCODE.put("FE", KEYMOD_SHIFT | KEYCODE_F12);
TERMCAP_TO_KEYCODE.put("kb", KEYCODE_DEL); // backspace key TERMCAP_TO_KEYCODE.put("k1", KEYCODE_F1);
TERMCAP_TO_KEYCODE.put("k2", KEYCODE_F2);
TERMCAP_TO_KEYCODE.put("k3", KEYCODE_F3);
TERMCAP_TO_KEYCODE.put("k4", KEYCODE_F4);
TERMCAP_TO_KEYCODE.put("k5", KEYCODE_F5);
TERMCAP_TO_KEYCODE.put("k6", KEYCODE_F6);
TERMCAP_TO_KEYCODE.put("k7", KEYCODE_F7);
TERMCAP_TO_KEYCODE.put("k8", KEYCODE_F8);
TERMCAP_TO_KEYCODE.put("k9", KEYCODE_F9);
TERMCAP_TO_KEYCODE.put("k;", KEYCODE_F10);
TERMCAP_TO_KEYCODE.put("F1", KEYCODE_F11);
TERMCAP_TO_KEYCODE.put("F2", KEYCODE_F12);
TERMCAP_TO_KEYCODE.put("F3", KEYMOD_SHIFT | KEYCODE_F1);
TERMCAP_TO_KEYCODE.put("F4", KEYMOD_SHIFT | KEYCODE_F2);
TERMCAP_TO_KEYCODE.put("F5", KEYMOD_SHIFT | KEYCODE_F3);
TERMCAP_TO_KEYCODE.put("F6", KEYMOD_SHIFT | KEYCODE_F4);
TERMCAP_TO_KEYCODE.put("F7", KEYMOD_SHIFT | KEYCODE_F5);
TERMCAP_TO_KEYCODE.put("F8", KEYMOD_SHIFT | KEYCODE_F6);
TERMCAP_TO_KEYCODE.put("F9", KEYMOD_SHIFT | KEYCODE_F7);
TERMCAP_TO_KEYCODE.put("FA", KEYMOD_SHIFT | KEYCODE_F8);
TERMCAP_TO_KEYCODE.put("FB", KEYMOD_SHIFT | KEYCODE_F9);
TERMCAP_TO_KEYCODE.put("FC", KEYMOD_SHIFT | KEYCODE_F10);
TERMCAP_TO_KEYCODE.put("FD", KEYMOD_SHIFT | KEYCODE_F11);
TERMCAP_TO_KEYCODE.put("FE", KEYMOD_SHIFT | KEYCODE_F12);
TERMCAP_TO_KEYCODE.put("kd", KEYCODE_DPAD_DOWN); // terminfo=kcud1, down-arrow key TERMCAP_TO_KEYCODE.put("kb", KEYCODE_DEL); // backspace key
TERMCAP_TO_KEYCODE.put("kh", KeyEvent.KEYCODE_HOME);
TERMCAP_TO_KEYCODE.put("kl", KEYCODE_DPAD_LEFT);
TERMCAP_TO_KEYCODE.put("kr", KEYCODE_DPAD_RIGHT);
// K1=Upper left of keypad: TERMCAP_TO_KEYCODE.put("kd", KEYCODE_DPAD_DOWN); // terminfo=kcud1, down-arrow key
// t_K1 <kHome> keypad home key TERMCAP_TO_KEYCODE.put("kh", KeyEvent.KEYCODE_HOME);
// t_K3 <kPageUp> keypad page-up key TERMCAP_TO_KEYCODE.put("kl", KEYCODE_DPAD_LEFT);
// t_K4 <kEnd> keypad end key TERMCAP_TO_KEYCODE.put("kr", KEYCODE_DPAD_RIGHT);
// t_K5 <kPageDown> keypad page-down key
TERMCAP_TO_KEYCODE.put("K1", KeyEvent.KEYCODE_HOME);
TERMCAP_TO_KEYCODE.put("K3", KeyEvent.KEYCODE_PAGE_UP);
TERMCAP_TO_KEYCODE.put("K4", KeyEvent.KEYCODE_MOVE_END);
TERMCAP_TO_KEYCODE.put("K5", KeyEvent.KEYCODE_PAGE_DOWN);
TERMCAP_TO_KEYCODE.put("ku", KEYCODE_DPAD_UP); // K1=Upper left of keypad:
// t_K1 <kHome> keypad home key
// t_K3 <kPageUp> keypad page-up key
// t_K4 <kEnd> keypad end key
// t_K5 <kPageDown> keypad page-down key
TERMCAP_TO_KEYCODE.put("K1", KeyEvent.KEYCODE_HOME);
TERMCAP_TO_KEYCODE.put("K3", KeyEvent.KEYCODE_PAGE_UP);
TERMCAP_TO_KEYCODE.put("K4", KeyEvent.KEYCODE_MOVE_END);
TERMCAP_TO_KEYCODE.put("K5", KeyEvent.KEYCODE_PAGE_DOWN);
TERMCAP_TO_KEYCODE.put("kB", KEYMOD_SHIFT | KEYCODE_TAB); // termcap=kB, terminfo=kcbt: Back-tab TERMCAP_TO_KEYCODE.put("ku", KEYCODE_DPAD_UP);
TERMCAP_TO_KEYCODE.put("kD", KEYCODE_FORWARD_DEL); // terminfo=kdch1, delete-character key
TERMCAP_TO_KEYCODE.put("kDN", KEYMOD_SHIFT | KEYCODE_DPAD_DOWN); // non-standard shifted arrow down
TERMCAP_TO_KEYCODE.put("kF", KEYMOD_SHIFT | KEYCODE_DPAD_DOWN); // terminfo=kind, scroll-forward key
TERMCAP_TO_KEYCODE.put("kI", KEYCODE_INSERT);
TERMCAP_TO_KEYCODE.put("kN", KEYCODE_PAGE_UP);
TERMCAP_TO_KEYCODE.put("kP", KEYCODE_PAGE_DOWN);
TERMCAP_TO_KEYCODE.put("kR", KEYMOD_SHIFT | KEYCODE_DPAD_UP); // terminfo=kri, scroll-backward key
TERMCAP_TO_KEYCODE.put("kUP", KEYMOD_SHIFT | KEYCODE_DPAD_UP); // non-standard shifted up
TERMCAP_TO_KEYCODE.put("@7", KEYCODE_MOVE_END); TERMCAP_TO_KEYCODE.put("kB", KEYMOD_SHIFT | KEYCODE_TAB); // termcap=kB, terminfo=kcbt: Back-tab
TERMCAP_TO_KEYCODE.put("@8", KEYCODE_NUMPAD_ENTER); TERMCAP_TO_KEYCODE.put("kD", KEYCODE_FORWARD_DEL); // terminfo=kdch1, delete-character key
} TERMCAP_TO_KEYCODE.put("kDN", KEYMOD_SHIFT | KEYCODE_DPAD_DOWN); // non-standard shifted arrow down
TERMCAP_TO_KEYCODE.put("kF", KEYMOD_SHIFT | KEYCODE_DPAD_DOWN); // terminfo=kind, scroll-forward key
TERMCAP_TO_KEYCODE.put("kI", KEYCODE_INSERT);
TERMCAP_TO_KEYCODE.put("kN", KEYCODE_PAGE_UP);
TERMCAP_TO_KEYCODE.put("kP", KEYCODE_PAGE_DOWN);
TERMCAP_TO_KEYCODE.put("kR", KEYMOD_SHIFT | KEYCODE_DPAD_UP); // terminfo=kri, scroll-backward key
TERMCAP_TO_KEYCODE.put("kUP", KEYMOD_SHIFT | KEYCODE_DPAD_UP); // non-standard shifted up
static String getCodeFromTermcap(String termcap, boolean cursorKeysApplication, boolean keypadApplication) { TERMCAP_TO_KEYCODE.put("@7", KEYCODE_MOVE_END);
Integer keyCodeAndMod = TERMCAP_TO_KEYCODE.get(termcap); TERMCAP_TO_KEYCODE.put("@8", KEYCODE_NUMPAD_ENTER);
if (keyCodeAndMod == null) return null; }
int keyCode = keyCodeAndMod;
int keyMod = 0;
if ((keyCode & KEYMOD_SHIFT) != 0) {
keyMod |= KEYMOD_SHIFT;
keyCode &= ~KEYMOD_SHIFT;
}
if ((keyCode & KEYMOD_CTRL) != 0) {
keyMod |= KEYMOD_CTRL;
keyCode &= ~KEYMOD_CTRL;
}
if ((keyCode & KEYMOD_ALT) != 0) {
keyMod |= KEYMOD_ALT;
keyCode &= ~KEYMOD_ALT;
}
return getCode(keyCode, keyMod, cursorKeysApplication, keypadApplication);
}
public static String getCode(int keyCode, int keyMode, boolean cursorApp, boolean keypadApplication) { static String getCodeFromTermcap(String termcap, boolean cursorKeysApplication, boolean keypadApplication) {
switch (keyCode) { Integer keyCodeAndMod = TERMCAP_TO_KEYCODE.get(termcap);
case KEYCODE_DPAD_CENTER: if (keyCodeAndMod == null) return null;
return "\015"; int keyCode = keyCodeAndMod;
int keyMod = 0;
if ((keyCode & KEYMOD_SHIFT) != 0) {
keyMod |= KEYMOD_SHIFT;
keyCode &= ~KEYMOD_SHIFT;
}
if ((keyCode & KEYMOD_CTRL) != 0) {
keyMod |= KEYMOD_CTRL;
keyCode &= ~KEYMOD_CTRL;
}
if ((keyCode & KEYMOD_ALT) != 0) {
keyMod |= KEYMOD_ALT;
keyCode &= ~KEYMOD_ALT;
}
return getCode(keyCode, keyMod, cursorKeysApplication, keypadApplication);
}
case KEYCODE_DPAD_UP: public static String getCode(int keyCode, int keyMode, boolean cursorApp, boolean keypadApplication) {
return (keyMode == 0) ? (cursorApp ? "\033OA" : "\033[A") : transformForModifiers("\033[1", keyMode, 'A'); switch (keyCode) {
case KEYCODE_DPAD_DOWN: case KEYCODE_DPAD_CENTER:
return (keyMode == 0) ? (cursorApp ? "\033OB" : "\033[B") : transformForModifiers("\033[1", keyMode, 'B'); return "\015";
case KEYCODE_DPAD_RIGHT:
return (keyMode == 0) ? (cursorApp ? "\033OC" : "\033[C") : transformForModifiers("\033[1", keyMode, 'C');
case KEYCODE_DPAD_LEFT:
return (keyMode == 0) ? (cursorApp ? "\033OD" : "\033[D") : transformForModifiers("\033[1", keyMode, 'D');
case KeyEvent.KEYCODE_HOME: case KEYCODE_DPAD_UP:
return (keyMode == 0) ? (cursorApp ? "\033OH" : "\033[H") : transformForModifiers("\033[1", keyMode, 'H'); return (keyMode == 0) ? (cursorApp ? "\033OA" : "\033[A") : transformForModifiers("\033[1", keyMode, 'A');
case KEYCODE_MOVE_END: case KEYCODE_DPAD_DOWN:
return (keyMode == 0) ? (cursorApp ? "\033OF" : "\033[F") : transformForModifiers("\033[1", keyMode, 'F'); return (keyMode == 0) ? (cursorApp ? "\033OB" : "\033[B") : transformForModifiers("\033[1", keyMode, 'B');
case KEYCODE_DPAD_RIGHT:
return (keyMode == 0) ? (cursorApp ? "\033OC" : "\033[C") : transformForModifiers("\033[1", keyMode, 'C');
case KEYCODE_DPAD_LEFT:
return (keyMode == 0) ? (cursorApp ? "\033OD" : "\033[D") : transformForModifiers("\033[1", keyMode, 'D');
// An xterm can send function keys F1 to F4 in two modes: vt100 compatible or case KeyEvent.KEYCODE_HOME:
// not. Because Vim may not know what the xterm is sending, both types of keys return (keyMode == 0) ? (cursorApp ? "\033OH" : "\033[H") : transformForModifiers("\033[1", keyMode, 'H');
// are recognized. The same happens for the <Home> and <End> keys. case KEYCODE_MOVE_END:
// normal vt100 ~ return (keyMode == 0) ? (cursorApp ? "\033OF" : "\033[F") : transformForModifiers("\033[1", keyMode, 'F');
// <F1> t_k1 <Esc>[11~ <xF1> <Esc>OP *<xF1>-xterm*
// <F2> t_k2 <Esc>[12~ <xF2> <Esc>OQ *<xF2>-xterm*
// <F3> t_k3 <Esc>[13~ <xF3> <Esc>OR *<xF3>-xterm*
// <F4> t_k4 <Esc>[14~ <xF4> <Esc>OS *<xF4>-xterm*
// <Home> t_kh <Esc>[7~ <xHome> <Esc>OH *<xHome>-xterm*
// <End> t_@7 <Esc>[4~ <xEnd> <Esc>OF *<xEnd>-xterm*
case KEYCODE_F1:
return (keyMode == 0) ? "\033OP" : transformForModifiers("\033[1", keyMode, 'P');
case KEYCODE_F2:
return (keyMode == 0) ? "\033OQ" : transformForModifiers("\033[1", keyMode, 'Q');
case KEYCODE_F3:
return (keyMode == 0) ? "\033OR" : transformForModifiers("\033[1", keyMode, 'R');
case KEYCODE_F4:
return (keyMode == 0) ? "\033OS" : transformForModifiers("\033[1", keyMode, 'S');
case KEYCODE_F5:
return transformForModifiers("\033[15", keyMode, '~');
case KEYCODE_F6:
return transformForModifiers("\033[17", keyMode, '~');
case KEYCODE_F7:
return transformForModifiers("\033[18", keyMode, '~');
case KEYCODE_F8:
return transformForModifiers("\033[19", keyMode, '~');
case KEYCODE_F9:
return transformForModifiers("\033[20", keyMode, '~');
case KEYCODE_F10:
return transformForModifiers("\033[21", keyMode, '~');
case KEYCODE_F11:
return transformForModifiers("\033[23", keyMode, '~');
case KEYCODE_F12:
return transformForModifiers("\033[24", keyMode, '~');
case KEYCODE_SYSRQ: // An xterm can send function keys F1 to F4 in two modes: vt100 compatible or
return "\033[32~"; // Sys Request / Print // not. Because Vim may not know what the xterm is sending, both types of keys
// Is this Scroll lock? case Cancel: return "\033[33~"; // are recognized. The same happens for the <Home> and <End> keys.
case KEYCODE_BREAK: // normal vt100 ~
return "\033[34~"; // Pause/Break // <F1> t_k1 <Esc>[11~ <xF1> <Esc>OP *<xF1>-xterm*
// <F2> t_k2 <Esc>[12~ <xF2> <Esc>OQ *<xF2>-xterm*
// <F3> t_k3 <Esc>[13~ <xF3> <Esc>OR *<xF3>-xterm*
// <F4> t_k4 <Esc>[14~ <xF4> <Esc>OS *<xF4>-xterm*
// <Home> t_kh <Esc>[7~ <xHome> <Esc>OH *<xHome>-xterm*
// <End> t_@7 <Esc>[4~ <xEnd> <Esc>OF *<xEnd>-xterm*
case KEYCODE_F1:
return (keyMode == 0) ? "\033OP" : transformForModifiers("\033[1", keyMode, 'P');
case KEYCODE_F2:
return (keyMode == 0) ? "\033OQ" : transformForModifiers("\033[1", keyMode, 'Q');
case KEYCODE_F3:
return (keyMode == 0) ? "\033OR" : transformForModifiers("\033[1", keyMode, 'R');
case KEYCODE_F4:
return (keyMode == 0) ? "\033OS" : transformForModifiers("\033[1", keyMode, 'S');
case KEYCODE_F5:
return transformForModifiers("\033[15", keyMode, '~');
case KEYCODE_F6:
return transformForModifiers("\033[17", keyMode, '~');
case KEYCODE_F7:
return transformForModifiers("\033[18", keyMode, '~');
case KEYCODE_F8:
return transformForModifiers("\033[19", keyMode, '~');
case KEYCODE_F9:
return transformForModifiers("\033[20", keyMode, '~');
case KEYCODE_F10:
return transformForModifiers("\033[21", keyMode, '~');
case KEYCODE_F11:
return transformForModifiers("\033[23", keyMode, '~');
case KEYCODE_F12:
return transformForModifiers("\033[24", keyMode, '~');
case KEYCODE_ESCAPE: case KEYCODE_SYSRQ:
case KeyEvent.KEYCODE_BACK: return "\033[32~"; // Sys Request / Print
return "\033"; // Is this Scroll lock? case Cancel: return "\033[33~";
case KEYCODE_BREAK:
return "\033[34~"; // Pause/Break
case KEYCODE_INSERT: case KEYCODE_ESCAPE:
return transformForModifiers("\033[2", keyMode, '~'); case KeyEvent.KEYCODE_BACK:
case KEYCODE_FORWARD_DEL: return "\033";
return transformForModifiers("\033[3", keyMode, '~');
case KEYCODE_NUMPAD_DOT: case KEYCODE_INSERT:
return keypadApplication ? "\033On" : "\033[3~"; return transformForModifiers("\033[2", keyMode, '~');
case KEYCODE_FORWARD_DEL:
return transformForModifiers("\033[3", keyMode, '~');
case KEYCODE_PAGE_UP: case KEYCODE_NUMPAD_DOT:
return "\033[5~"; return keypadApplication ? "\033On" : "\033[3~";
case KEYCODE_PAGE_DOWN:
return "\033[6~";
case KEYCODE_DEL:
// Yes, this needs to U+007F and not U+0008!
return "\u007F";
case KEYCODE_NUM_LOCK:
return "\033OP";
case KeyEvent.KEYCODE_SPACE: case KEYCODE_PAGE_UP:
// If ctrl is not down, return null so that it goes through normal input processing (which may e.g. cause a return "\033[5~";
// combining accent to be written): case KEYCODE_PAGE_DOWN:
return ((keyMode & KEYMOD_CTRL) == 0) ? null : "\0"; return "\033[6~";
case KEYCODE_TAB: case KEYCODE_DEL:
// This is back-tab when shifted: // Yes, this needs to U+007F and not U+0008!
return (keyMode & KEYMOD_SHIFT) == 0 ? "\011" : "\033[Z"; return "\u007F";
case KEYCODE_ENTER: case KEYCODE_NUM_LOCK:
return ((keyMode & KEYMOD_ALT) == 0) ? "\r" : "\033\r"; return "\033OP";
case KEYCODE_NUMPAD_ENTER: case KeyEvent.KEYCODE_SPACE:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'M') : "\n"; // If ctrl is not down, return null so that it goes through normal input processing (which may e.g. cause a
case KEYCODE_NUMPAD_MULTIPLY: // combining accent to be written):
return keypadApplication ? transformForModifiers("\033O", keyMode, 'j') : "*"; return ((keyMode & KEYMOD_CTRL) == 0) ? null : "\0";
case KEYCODE_NUMPAD_ADD: case KEYCODE_TAB:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'k') : "+"; // This is back-tab when shifted:
case KEYCODE_NUMPAD_COMMA: return (keyMode & KEYMOD_SHIFT) == 0 ? "\011" : "\033[Z";
return ","; case KEYCODE_ENTER:
case KEYCODE_NUMPAD_SUBTRACT: return ((keyMode & KEYMOD_ALT) == 0) ? "\r" : "\033\r";
return keypadApplication ? transformForModifiers("\033O", keyMode, 'm') : "-";
case KEYCODE_NUMPAD_DIVIDE:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'o') : "/";
case KEYCODE_NUMPAD_0:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'p') : "1";
case KEYCODE_NUMPAD_1:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'q') : "1";
case KEYCODE_NUMPAD_2:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'r') : "2";
case KEYCODE_NUMPAD_3:
return keypadApplication ? transformForModifiers("\033O", keyMode, 's') : "3";
case KEYCODE_NUMPAD_4:
return keypadApplication ? transformForModifiers("\033O", keyMode, 't') : "4";
case KEYCODE_NUMPAD_5:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'u') : "5";
case KEYCODE_NUMPAD_6:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'v') : "6";
case KEYCODE_NUMPAD_7:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'w') : "7";
case KEYCODE_NUMPAD_8:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'x') : "8";
case KEYCODE_NUMPAD_9:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'y') : "9";
case KEYCODE_NUMPAD_EQUALS:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'X') : "=";
}
return null; case KEYCODE_NUMPAD_ENTER:
} return keypadApplication ? transformForModifiers("\033O", keyMode, 'M') : "\n";
case KEYCODE_NUMPAD_MULTIPLY:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'j') : "*";
case KEYCODE_NUMPAD_ADD:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'k') : "+";
case KEYCODE_NUMPAD_COMMA:
return ",";
case KEYCODE_NUMPAD_SUBTRACT:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'm') : "-";
case KEYCODE_NUMPAD_DIVIDE:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'o') : "/";
case KEYCODE_NUMPAD_0:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'p') : "1";
case KEYCODE_NUMPAD_1:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'q') : "1";
case KEYCODE_NUMPAD_2:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'r') : "2";
case KEYCODE_NUMPAD_3:
return keypadApplication ? transformForModifiers("\033O", keyMode, 's') : "3";
case KEYCODE_NUMPAD_4:
return keypadApplication ? transformForModifiers("\033O", keyMode, 't') : "4";
case KEYCODE_NUMPAD_5:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'u') : "5";
case KEYCODE_NUMPAD_6:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'v') : "6";
case KEYCODE_NUMPAD_7:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'w') : "7";
case KEYCODE_NUMPAD_8:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'x') : "8";
case KEYCODE_NUMPAD_9:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'y') : "9";
case KEYCODE_NUMPAD_EQUALS:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'X') : "=";
}
private static String transformForModifiers(String start, int keymod, char lastChar) { return null;
int modifier; }
switch (keymod) {
case KEYMOD_SHIFT: private static String transformForModifiers(String start, int keymod, char lastChar) {
modifier = 2; int modifier;
break; switch (keymod) {
case KEYMOD_ALT: case KEYMOD_SHIFT:
modifier = 3; modifier = 2;
break; break;
case (KEYMOD_SHIFT | KEYMOD_ALT): case KEYMOD_ALT:
modifier = 4; modifier = 3;
break; break;
case KEYMOD_CTRL: case (KEYMOD_SHIFT | KEYMOD_ALT):
modifier = 5; modifier = 4;
break; break;
case KEYMOD_SHIFT | KEYMOD_CTRL: case KEYMOD_CTRL:
modifier = 6; modifier = 5;
break; break;
case KEYMOD_ALT | KEYMOD_CTRL: case KEYMOD_SHIFT | KEYMOD_CTRL:
modifier = 7; modifier = 6;
break; break;
case KEYMOD_SHIFT | KEYMOD_ALT | KEYMOD_CTRL: case KEYMOD_ALT | KEYMOD_CTRL:
modifier = 8; modifier = 7;
break; break;
default: case KEYMOD_SHIFT | KEYMOD_ALT | KEYMOD_CTRL:
return start + lastChar; modifier = 8;
} break;
return start + (";" + modifier) + lastChar; default:
} return start + lastChar;
}
return start + (";" + modifier) + lastChar;
}
} }

View File

@@ -3,441 +3,425 @@ package com.termux.terminal;
/** /**
* A circular buffer of {@link TerminalRow}:s which keeps notes about what is visible on a logical screen and the scroll * A circular buffer of {@link TerminalRow}:s which keeps notes about what is visible on a logical screen and the scroll
* history. * history.
* * <p/>
* See {@link #externalToInternalRow(int)} for how to map from logical screen rows to array indices. * See {@link #externalToInternalRow(int)} for how to map from logical screen rows to array indices.
*/ */
public final class TerminalBuffer { public final class TerminalBuffer {
TerminalRow[] mLines; TerminalRow[] mLines;
/** The length of {@link #mLines}. */ /** The length of {@link #mLines}. */
int mTotalRows; int mTotalRows;
/** The number of rows and columns visible on the screen. */ /** The number of rows and columns visible on the screen. */
int mScreenRows, mColumns; int mScreenRows, mColumns;
/** The number of rows kept in history. */ /** The number of rows kept in history. */
private int mActiveTranscriptRows = 0; private int mActiveTranscriptRows = 0;
/** The index in the circular buffer where the visible screen starts. */ /** The index in the circular buffer where the visible screen starts. */
private int mScreenFirstRow = 0; private int mScreenFirstRow = 0;
/** /**
* Create a transcript screen. * Create a transcript screen.
* *
* @param columns * @param columns the width of the screen in characters.
* the width of the screen in characters. * @param totalRows the height of the entire text area, in rows of text.
* @param totalRows * @param screenRows the height of just the screen, not including the transcript that holds lines that have scrolled off
* the height of the entire text area, in rows of text. * the top of the screen.
* @param screenRows */
* the height of just the screen, not including the transcript that holds lines that have scrolled off public TerminalBuffer(int columns, int totalRows, int screenRows) {
* the top of the screen. mColumns = columns;
*/ mTotalRows = totalRows;
public TerminalBuffer(int columns, int totalRows, int screenRows) { mScreenRows = screenRows;
mColumns = columns; mLines = new TerminalRow[totalRows];
mTotalRows = totalRows;
mScreenRows = screenRows;
mLines = new TerminalRow[totalRows];
blockSet(0, 0, columns, screenRows, ' ', TextStyle.NORMAL); blockSet(0, 0, columns, screenRows, ' ', TextStyle.NORMAL);
} }
public String getTranscriptText() { public String getTranscriptText() {
return getSelectedText(0, -getActiveTranscriptRows(), mColumns, mScreenRows).trim(); return getSelectedText(0, -getActiveTranscriptRows(), mColumns, mScreenRows).trim();
} }
public String getSelectedText(int selX1, int selY1, int selX2, int selY2) { public String getSelectedText(int selX1, int selY1, int selX2, int selY2) {
final StringBuilder builder = new StringBuilder(); final StringBuilder builder = new StringBuilder();
final int columns = mColumns; final int columns = mColumns;
if (selY1 < -getActiveTranscriptRows()) selY1 = -getActiveTranscriptRows(); if (selY1 < -getActiveTranscriptRows()) selY1 = -getActiveTranscriptRows();
if (selY2 >= mScreenRows) selY2 = mScreenRows - 1; if (selY2 >= mScreenRows) selY2 = mScreenRows - 1;
for (int row = selY1; row <= selY2; row++) { for (int row = selY1; row <= selY2; row++) {
int x1 = (row == selY1) ? selX1 : 0; int x1 = (row == selY1) ? selX1 : 0;
int x2; int x2;
if (row == selY2) { if (row == selY2) {
x2 = selX2 + 1; x2 = selX2 + 1;
if (x2 > columns) x2 = columns; if (x2 > columns) x2 = columns;
} else { } else {
x2 = columns; x2 = columns;
} }
TerminalRow lineObject = mLines[externalToInternalRow(row)]; TerminalRow lineObject = mLines[externalToInternalRow(row)];
int x1Index = lineObject.findStartOfColumn(x1); int x1Index = lineObject.findStartOfColumn(x1);
int x2Index = (x2 < mColumns) ? lineObject.findStartOfColumn(x2) : lineObject.getSpaceUsed(); int x2Index = (x2 < mColumns) ? lineObject.findStartOfColumn(x2) : lineObject.getSpaceUsed();
if (x2Index == x1Index) { if (x2Index == x1Index) {
// Selected the start of a wide character. // Selected the start of a wide character.
x2Index = lineObject.findStartOfColumn(x2+1); x2Index = lineObject.findStartOfColumn(x2 + 1);
} }
char[] line = lineObject.mText; char[] line = lineObject.mText;
int lastPrintingCharIndex = -1; int lastPrintingCharIndex = -1;
int i; int i;
boolean rowLineWrap = getLineWrap(row); boolean rowLineWrap = getLineWrap(row);
if (rowLineWrap && x2 == columns) { if (rowLineWrap && x2 == columns) {
// If the line was wrapped, we shouldn't lose trailing space: // If the line was wrapped, we shouldn't lose trailing space:
lastPrintingCharIndex = x2Index - 1; lastPrintingCharIndex = x2Index - 1;
} else { } else {
for (i = x1Index; i < x2Index; ++i) { for (i = x1Index; i < x2Index; ++i) {
char c = line[i]; char c = line[i];
if (c != ' ') lastPrintingCharIndex = i; if (c != ' ') lastPrintingCharIndex = i;
} }
} }
if (lastPrintingCharIndex != -1) builder.append(line, x1Index, lastPrintingCharIndex - x1Index + 1); if (lastPrintingCharIndex != -1)
if (!rowLineWrap && row < selY2 && row < mScreenRows - 1) builder.append('\n'); builder.append(line, x1Index, lastPrintingCharIndex - x1Index + 1);
} if (!rowLineWrap && row < selY2 && row < mScreenRows - 1) builder.append('\n');
return builder.toString(); }
} return builder.toString();
}
public int getActiveTranscriptRows() { public int getActiveTranscriptRows() {
return mActiveTranscriptRows; return mActiveTranscriptRows;
} }
public int getActiveRows() { public int getActiveRows() {
return mActiveTranscriptRows + mScreenRows; return mActiveTranscriptRows + mScreenRows;
} }
/** /**
* Convert a row value from the public external coordinate system to our internal private coordinate system. * Convert a row value from the public external coordinate system to our internal private coordinate system.
* * <p/>
* <ul> * <ul>
* <li>External coordinate system: -mActiveTranscriptRows to mScreenRows-1, with the screen being 0..mScreenRows-1. * <li>External coordinate system: -mActiveTranscriptRows to mScreenRows-1, with the screen being 0..mScreenRows-1.
* <li>Internal coordinate system: the mScreenRows lines starting at mScreenFirstRow comprise the screen, while the * <li>Internal coordinate system: the mScreenRows lines starting at mScreenFirstRow comprise the screen, while the
* mActiveTranscriptRows lines ending at mScreenFirstRow-1 form the transcript (as a circular buffer). * mActiveTranscriptRows lines ending at mScreenFirstRow-1 form the transcript (as a circular buffer).
* </ul> * </ul>
* * <p/>
* External <---> Internal: * External <---> Internal:
* * <p/>
* <pre> * <pre>
* [ ... ] [ ... ] * [ ... ] [ ... ]
* [ -mActiveTranscriptRows ] [ mScreenFirstRow - mActiveTranscriptRows ] * [ -mActiveTranscriptRows ] [ mScreenFirstRow - mActiveTranscriptRows ]
* [ ... ] [ ... ] * [ ... ] [ ... ]
* [ 0 (visible screen starts here) ] <-----> [ mScreenFirstRow ] * [ 0 (visible screen starts here) ] <-----> [ mScreenFirstRow ]
* [ ... ] [ ... ] * [ ... ] [ ... ]
* [ mScreenRows-1 ] [ mScreenFirstRow + mScreenRows-1 ] * [ mScreenRows-1 ] [ mScreenFirstRow + mScreenRows-1 ]
* </pre> * </pre>
* *
* @param externalRow * @param externalRow a row in the external coordinate system.
* a row in the external coordinate system. * @return The row corresponding to the input argument in the private coordinate system.
* @return The row corresponding to the input argument in the private coordinate system. */
*/ public int externalToInternalRow(int externalRow) {
public int externalToInternalRow(int externalRow) { if (externalRow < -mActiveTranscriptRows || externalRow > mScreenRows)
if (externalRow < -mActiveTranscriptRows || externalRow > mScreenRows) throw new IllegalArgumentException("extRow=" + externalRow + ", mScreenRows=" + mScreenRows + ", mActiveTranscriptRows=" + mActiveTranscriptRows);
throw new IllegalArgumentException("extRow=" + externalRow + ", mScreenRows=" + mScreenRows + ", mActiveTranscriptRows=" + mActiveTranscriptRows); final int internalRow = mScreenFirstRow + externalRow;
final int internalRow = mScreenFirstRow + externalRow; return (internalRow < 0) ? (mTotalRows + internalRow) : (internalRow % mTotalRows);
return (internalRow < 0) ? (mTotalRows + internalRow) : (internalRow % mTotalRows); }
}
public void setLineWrap(int row) { public void setLineWrap(int row) {
mLines[externalToInternalRow(row)].mLineWrap = true; mLines[externalToInternalRow(row)].mLineWrap = true;
} }
public boolean getLineWrap(int row) { public boolean getLineWrap(int row) {
return mLines[externalToInternalRow(row)].mLineWrap; return mLines[externalToInternalRow(row)].mLineWrap;
} }
public void clearLineWrap(int row) { public void clearLineWrap(int row) {
mLines[externalToInternalRow(row)].mLineWrap = false; mLines[externalToInternalRow(row)].mLineWrap = false;
} }
/** /**
* Resize the screen which this transcript backs. Currently, this only works if the number of columns does not * 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). * change or the rows expand (that is, it only works when shrinking the number of rows).
* *
* @param newColumns * @param newColumns The number of columns the screen should have.
* The number of columns the screen should have. * @param newRows The number of rows the screen should have.
* @param newRows * @param cursor An int[2] containing the (column, row) cursor location.
* The number of rows the screen should have. */
* @param cursor public void resize(int newColumns, int newRows, int newTotalRows, int[] cursor, int currentStyle, boolean altScreen) {
* An int[2] containing the (column, row) cursor location. // newRows > mTotalRows should not normally happen since mTotalRows is TRANSCRIPT_ROWS (10000):
*/ if (newColumns == mColumns && newRows <= mTotalRows) {
public void resize(int newColumns, int newRows, int newTotalRows, int[] cursor, int currentStyle, boolean altScreen) { // Fast resize where just the rows changed.
// newRows > mTotalRows should not normally happen since mTotalRows is TRANSCRIPT_ROWS (10000): int shiftDownOfTopRow = mScreenRows - newRows;
if (newColumns == mColumns && newRows <= mTotalRows) { if (shiftDownOfTopRow > 0 && shiftDownOfTopRow < mScreenRows) {
// Fast resize where just the rows changed. // Shrinking. Check if we can skip blank rows at bottom below cursor.
int shiftDownOfTopRow = mScreenRows - newRows; for (int i = mScreenRows - 1; i > 0; i--) {
if (shiftDownOfTopRow > 0 && shiftDownOfTopRow < mScreenRows) { if (cursor[1] >= i) break;
// Shrinking. Check if we can skip blank rows at bottom below cursor. int r = externalToInternalRow(i);
for (int i = mScreenRows - 1; i > 0; i--) { if (mLines[r] == null || mLines[r].isBlank()) {
if (cursor[1] >= i) break; if (--shiftDownOfTopRow == 0) break;
int r = externalToInternalRow(i); }
if (mLines[r] == null || mLines[r].isBlank()) { }
if (--shiftDownOfTopRow == 0) break; } else if (shiftDownOfTopRow < 0) {
} // Negative shift down = expanding. Only move screen up if there is transcript to show:
} int actualShift = Math.max(shiftDownOfTopRow, -mActiveTranscriptRows);
} else if (shiftDownOfTopRow < 0) { if (shiftDownOfTopRow != actualShift) {
// Negative shift down = expanding. Only move screen up if there is transcript to show: // The new lines revealed by the resizing are not all from the transcript. Blank the below ones.
int actualShift = Math.max(shiftDownOfTopRow, -mActiveTranscriptRows); for (int i = 0; i < actualShift - shiftDownOfTopRow; i++)
if (shiftDownOfTopRow != actualShift) { allocateFullLineIfNecessary((mScreenFirstRow + mScreenRows + i) % mTotalRows).clear(currentStyle);
// The new lines revealed by the resizing are not all from the transcript. Blank the below ones. shiftDownOfTopRow = actualShift;
for (int i = 0; i < actualShift - shiftDownOfTopRow; i++) }
allocateFullLineIfNecessary((mScreenFirstRow + mScreenRows + i) % mTotalRows).clear(currentStyle); }
shiftDownOfTopRow = actualShift; mScreenFirstRow += shiftDownOfTopRow;
} mScreenFirstRow = (mScreenFirstRow < 0) ? (mScreenFirstRow + mTotalRows) : (mScreenFirstRow % mTotalRows);
} mTotalRows = newTotalRows;
mScreenFirstRow += shiftDownOfTopRow; mActiveTranscriptRows = altScreen ? 0 : Math.max(0, mActiveTranscriptRows + shiftDownOfTopRow);
mScreenFirstRow = (mScreenFirstRow < 0) ? (mScreenFirstRow + mTotalRows) : (mScreenFirstRow % mTotalRows); cursor[1] -= shiftDownOfTopRow;
mTotalRows = newTotalRows; mScreenRows = newRows;
mActiveTranscriptRows = altScreen ? 0 : Math.max(0, mActiveTranscriptRows + shiftDownOfTopRow); } else {
cursor[1] -= shiftDownOfTopRow; // Copy away old state and update new:
mScreenRows = newRows; TerminalRow[] oldLines = mLines;
} else { mLines = new TerminalRow[newTotalRows];
// Copy away old state and update new: for (int i = 0; i < newTotalRows; i++)
TerminalRow[] oldLines = mLines; mLines[i] = new TerminalRow(newColumns, currentStyle);
mLines = new TerminalRow[newTotalRows];
for (int i = 0; i < newTotalRows; i++)
mLines[i] = new TerminalRow(newColumns, currentStyle);
final int oldActiveTranscriptRows = mActiveTranscriptRows; final int oldActiveTranscriptRows = mActiveTranscriptRows;
final int oldScreenFirstRow = mScreenFirstRow; final int oldScreenFirstRow = mScreenFirstRow;
final int oldScreenRows = mScreenRows; final int oldScreenRows = mScreenRows;
final int oldTotalRows = mTotalRows; final int oldTotalRows = mTotalRows;
mTotalRows = newTotalRows; mTotalRows = newTotalRows;
mScreenRows = newRows; mScreenRows = newRows;
mActiveTranscriptRows = mScreenFirstRow = 0; mActiveTranscriptRows = mScreenFirstRow = 0;
mColumns = newColumns; mColumns = newColumns;
int newCursorRow = -1; int newCursorRow = -1;
int newCursorColumn = -1; int newCursorColumn = -1;
int oldCursorRow = cursor[1]; int oldCursorRow = cursor[1];
int oldCursorColumn = cursor[0]; int oldCursorColumn = cursor[0];
boolean newCursorPlaced = false; boolean newCursorPlaced = false;
int currentOutputExternalRow = 0; int currentOutputExternalRow = 0;
int currentOutputExternalColumn = 0; int currentOutputExternalColumn = 0;
// Loop over every character in the initial state. // Loop over every character in the initial state.
// Blank lines should be skipped only if at end of transcript (just as is done in the "fast" resize), so we // Blank lines should be skipped only if at end of transcript (just as is done in the "fast" resize), so we
// keep track how many blank lines we have skipped if we later on find a non-blank line. // keep track how many blank lines we have skipped if we later on find a non-blank line.
int skippedBlankLines = 0; int skippedBlankLines = 0;
for (int externalOldRow = -oldActiveTranscriptRows; externalOldRow < oldScreenRows; externalOldRow++) { for (int externalOldRow = -oldActiveTranscriptRows; externalOldRow < oldScreenRows; externalOldRow++) {
// Do what externalToInternalRow() does but for the old state: // Do what externalToInternalRow() does but for the old state:
int internalOldRow = oldScreenFirstRow + externalOldRow; int internalOldRow = oldScreenFirstRow + externalOldRow;
internalOldRow = (internalOldRow < 0) ? (oldTotalRows + internalOldRow) : (internalOldRow % oldTotalRows); internalOldRow = (internalOldRow < 0) ? (oldTotalRows + internalOldRow) : (internalOldRow % oldTotalRows);
TerminalRow oldLine = oldLines[internalOldRow]; TerminalRow oldLine = oldLines[internalOldRow];
boolean cursorAtThisRow = externalOldRow == oldCursorRow; boolean cursorAtThisRow = externalOldRow == oldCursorRow;
// The cursor may only be on a non-null line, which we should not skip: // The cursor may only be on a non-null line, which we should not skip:
if (oldLine == null || (!(!newCursorPlaced && cursorAtThisRow)) && oldLine.isBlank()) { if (oldLine == null || (!(!newCursorPlaced && cursorAtThisRow)) && oldLine.isBlank()) {
skippedBlankLines++; skippedBlankLines++;
continue; continue;
} else if (skippedBlankLines > 0) { } else if (skippedBlankLines > 0) {
// After skipping some blank lines we encounter a non-blank line. Insert the skipped blank lines. // After skipping some blank lines we encounter a non-blank line. Insert the skipped blank lines.
for (int i = 0; i < skippedBlankLines; i++) { for (int i = 0; i < skippedBlankLines; i++) {
if (currentOutputExternalRow == mScreenRows - 1) { if (currentOutputExternalRow == mScreenRows - 1) {
scrollDownOneLine(0, mScreenRows, currentStyle); scrollDownOneLine(0, mScreenRows, currentStyle);
} else { } else {
currentOutputExternalRow++; currentOutputExternalRow++;
} }
currentOutputExternalColumn = 0; currentOutputExternalColumn = 0;
} }
skippedBlankLines = 0; skippedBlankLines = 0;
} }
int lastNonSpaceIndex = 0; int lastNonSpaceIndex = 0;
boolean justToCursor = false; boolean justToCursor = false;
if (cursorAtThisRow || oldLine.mLineWrap) { if (cursorAtThisRow || oldLine.mLineWrap) {
// Take the whole line, either because of cursor on it, or if line wrapping. // Take the whole line, either because of cursor on it, or if line wrapping.
lastNonSpaceIndex = oldLine.getSpaceUsed(); lastNonSpaceIndex = oldLine.getSpaceUsed();
if (cursorAtThisRow) justToCursor = true; if (cursorAtThisRow) justToCursor = true;
} else { } else {
for (int i = 0; i < oldLine.getSpaceUsed(); i++) for (int i = 0; i < oldLine.getSpaceUsed(); i++)
// NEWLY INTRODUCED BUG! Should not index oldLine.mStyle with char indices // NEWLY INTRODUCED BUG! Should not index oldLine.mStyle with char indices
if (oldLine.mText[i] != ' '/* || oldLine.mStyle[i] != currentStyle */) lastNonSpaceIndex = i + 1; if (oldLine.mText[i] != ' '/* || oldLine.mStyle[i] != currentStyle */)
} lastNonSpaceIndex = i + 1;
}
int currentOldCol = 0; int currentOldCol = 0;
int styleAtCol = 0; int styleAtCol = 0;
for (int i = 0; i < lastNonSpaceIndex; i++) { for (int i = 0; i < lastNonSpaceIndex; i++) {
// Note that looping over java character, not cells. // Note that looping over java character, not cells.
char c = oldLine.mText[i]; char c = oldLine.mText[i];
int codePoint = (Character.isHighSurrogate(c)) ? Character.toCodePoint(c, oldLine.mText[++i]) : c; int codePoint = (Character.isHighSurrogate(c)) ? Character.toCodePoint(c, oldLine.mText[++i]) : c;
int displayWidth = WcWidth.width(codePoint); int displayWidth = WcWidth.width(codePoint);
// Use the last style if this is a zero-width character: // Use the last style if this is a zero-width character:
if (displayWidth > 0) styleAtCol = oldLine.getStyle(currentOldCol); if (displayWidth > 0) styleAtCol = oldLine.getStyle(currentOldCol);
// Line wrap as necessary: // Line wrap as necessary:
if (currentOutputExternalColumn + displayWidth > mColumns) { if (currentOutputExternalColumn + displayWidth > mColumns) {
setLineWrap(currentOutputExternalRow); setLineWrap(currentOutputExternalRow);
if (currentOutputExternalRow == mScreenRows - 1) { if (currentOutputExternalRow == mScreenRows - 1) {
if (newCursorPlaced) newCursorRow--; if (newCursorPlaced) newCursorRow--;
scrollDownOneLine(0, mScreenRows, currentStyle); scrollDownOneLine(0, mScreenRows, currentStyle);
} else { } else {
currentOutputExternalRow++; currentOutputExternalRow++;
} }
currentOutputExternalColumn = 0; currentOutputExternalColumn = 0;
} }
int offsetDueToCombiningChar = ((displayWidth <= 0 && currentOutputExternalColumn > 0) ? 1 : 0); int offsetDueToCombiningChar = ((displayWidth <= 0 && currentOutputExternalColumn > 0) ? 1 : 0);
int outputColumn = currentOutputExternalColumn - offsetDueToCombiningChar; int outputColumn = currentOutputExternalColumn - offsetDueToCombiningChar;
setChar(outputColumn, currentOutputExternalRow, codePoint, styleAtCol); setChar(outputColumn, currentOutputExternalRow, codePoint, styleAtCol);
if (displayWidth > 0) { if (displayWidth > 0) {
if (oldCursorRow == externalOldRow && oldCursorColumn == currentOldCol) { if (oldCursorRow == externalOldRow && oldCursorColumn == currentOldCol) {
newCursorColumn = currentOutputExternalColumn; newCursorColumn = currentOutputExternalColumn;
newCursorRow = currentOutputExternalRow; newCursorRow = currentOutputExternalRow;
newCursorPlaced = true; newCursorPlaced = true;
} }
currentOldCol += displayWidth; currentOldCol += displayWidth;
currentOutputExternalColumn += displayWidth; currentOutputExternalColumn += displayWidth;
if (justToCursor && newCursorPlaced) break; if (justToCursor && newCursorPlaced) break;
} }
} }
// Old row has been copied. Check if we need to insert newline if old line was not wrapping: // Old row has been copied. Check if we need to insert newline if old line was not wrapping:
if (externalOldRow != (oldScreenRows - 1) && !oldLine.mLineWrap) { if (externalOldRow != (oldScreenRows - 1) && !oldLine.mLineWrap) {
if (currentOutputExternalRow == mScreenRows - 1) { if (currentOutputExternalRow == mScreenRows - 1) {
if (newCursorPlaced) newCursorRow--; if (newCursorPlaced) newCursorRow--;
scrollDownOneLine(0, mScreenRows, currentStyle); scrollDownOneLine(0, mScreenRows, currentStyle);
} else { } else {
currentOutputExternalRow++; currentOutputExternalRow++;
} }
currentOutputExternalColumn = 0; currentOutputExternalColumn = 0;
} }
} }
cursor[0] = newCursorColumn; cursor[0] = newCursorColumn;
cursor[1] = newCursorRow; cursor[1] = newCursorRow;
} }
// Handle cursor scrolling off screen: // Handle cursor scrolling off screen:
if (cursor[0] < 0 || cursor[1] < 0) cursor[0] = cursor[1] = 0; if (cursor[0] < 0 || cursor[1] < 0) cursor[0] = cursor[1] = 0;
} }
/** /**
* Block copy lines and associated metadata from one location to another in the circular buffer, taking wraparound * Block copy lines and associated metadata from one location to another in the circular buffer, taking wraparound
* into account. * into account.
* *
* @param srcInternal * @param srcInternal The first line to be copied.
* The first line to be copied. * @param len The number of lines to be copied.
* @param len */
* The number of lines to be copied. private void blockCopyLinesDown(int srcInternal, int len) {
*/ if (len == 0) return;
private void blockCopyLinesDown(int srcInternal, int len) { int totalRows = mTotalRows;
if (len == 0) return;
int totalRows = mTotalRows;
int start = len - 1; int start = len - 1;
// Save away line to be overwritten: // Save away line to be overwritten:
TerminalRow lineToBeOverWritten = mLines[(srcInternal + start + 1) % totalRows]; TerminalRow lineToBeOverWritten = mLines[(srcInternal + start + 1) % totalRows];
// Do the copy from bottom to top. // Do the copy from bottom to top.
for (int i = start; i >= 0; --i) for (int i = start; i >= 0; --i)
mLines[(srcInternal + i + 1) % totalRows] = mLines[(srcInternal + i) % totalRows]; mLines[(srcInternal + i + 1) % totalRows] = mLines[(srcInternal + i) % totalRows];
// Put back overwritten line, now above the block: // Put back overwritten line, now above the block:
mLines[(srcInternal) % totalRows] = lineToBeOverWritten; mLines[(srcInternal) % totalRows] = lineToBeOverWritten;
} }
/** /**
* Scroll the screen down one line. To scroll the whole screen of a 24 line screen, the arguments would be (0, 24). * Scroll the screen down one line. To scroll the whole screen of a 24 line screen, the arguments would be (0, 24).
* *
* @param topMargin * @param topMargin First line that is scrolled.
* First line that is scrolled. * @param bottomMargin One line after the last line that is scrolled.
* @param bottomMargin * @param style the style for the newly exposed line.
* One line after the last line that is scrolled. */
* @param style public void scrollDownOneLine(int topMargin, int bottomMargin, int style) {
* the style for the newly exposed line. if (topMargin > bottomMargin - 1 || topMargin < 0 || bottomMargin > mScreenRows)
*/ throw new IllegalArgumentException("topMargin=" + topMargin + ", bottomMargin=" + bottomMargin + ", mScreenRows=" + mScreenRows);
public void scrollDownOneLine(int topMargin, int bottomMargin, int style) {
if (topMargin > bottomMargin - 1 || topMargin < 0 || bottomMargin > mScreenRows)
throw new IllegalArgumentException("topMargin=" + topMargin + ", bottomMargin=" + bottomMargin + ", mScreenRows=" + mScreenRows);
// Copy the fixed topMargin lines one line down so that they remain on screen in same position: // Copy the fixed topMargin lines one line down so that they remain on screen in same position:
blockCopyLinesDown(mScreenFirstRow, topMargin); blockCopyLinesDown(mScreenFirstRow, topMargin);
// Copy the fixed mScreenRows-bottomMargin lines one line down so that they remain on screen in same // Copy the fixed mScreenRows-bottomMargin lines one line down so that they remain on screen in same
// position: // position:
blockCopyLinesDown(externalToInternalRow(bottomMargin), mScreenRows - bottomMargin); blockCopyLinesDown(externalToInternalRow(bottomMargin), mScreenRows - bottomMargin);
// Update the screen location in the ring buffer: // Update the screen location in the ring buffer:
mScreenFirstRow = (mScreenFirstRow + 1) % mTotalRows; mScreenFirstRow = (mScreenFirstRow + 1) % mTotalRows;
// Note that the history has grown if not already full: // Note that the history has grown if not already full:
if (mActiveTranscriptRows < mTotalRows - mScreenRows) mActiveTranscriptRows++; if (mActiveTranscriptRows < mTotalRows - mScreenRows) mActiveTranscriptRows++;
// Blank the newly revealed line above the bottom margin: // Blank the newly revealed line above the bottom margin:
int blankRow = externalToInternalRow(bottomMargin - 1); int blankRow = externalToInternalRow(bottomMargin - 1);
if (mLines[blankRow] == null) { if (mLines[blankRow] == null) {
mLines[blankRow] = new TerminalRow(mColumns, style); mLines[blankRow] = new TerminalRow(mColumns, style);
} else { } else {
mLines[blankRow].clear(style); mLines[blankRow].clear(style);
} }
} }
/** /**
* Block copy characters from one position in the screen to another. The two positions can overlap. All characters * Block copy characters from one position in the screen to another. The two positions can overlap. All characters
* of the source and destination must be within the bounds of the screen, or else an InvalidParameterException will * of the source and destination must be within the bounds of the screen, or else an InvalidParameterException will
* be thrown. * be thrown.
* *
* @param sx * @param sx source X coordinate
* source X coordinate * @param sy source Y coordinate
* @param sy * @param w width
* source Y coordinate * @param h height
* @param w * @param dx destination X coordinate
* width * @param dy destination Y coordinate
* @param h */
* height public void blockCopy(int sx, int sy, int w, int h, int dx, int dy) {
* @param dx if (w == 0) return;
* destination X coordinate if (sx < 0 || sx + w > mColumns || sy < 0 || sy + h > mScreenRows || dx < 0 || dx + w > mColumns || dy < 0 || dy + h > mScreenRows)
* @param dy throw new IllegalArgumentException();
* destination Y coordinate boolean copyingUp = sy > dy;
*/ for (int y = 0; y < h; y++) {
public void blockCopy(int sx, int sy, int w, int h, int dx, int dy) { int y2 = copyingUp ? y : (h - (y + 1));
if (w == 0) return; TerminalRow sourceRow = allocateFullLineIfNecessary(externalToInternalRow(sy + y2));
if (sx < 0 || sx + w > mColumns || sy < 0 || sy + h > mScreenRows || dx < 0 || dx + w > mColumns || dy < 0 || dy + h > mScreenRows) allocateFullLineIfNecessary(externalToInternalRow(dy + y2)).copyInterval(sourceRow, sx, sx + w, dx);
throw new IllegalArgumentException(); }
boolean copyingUp = sy > dy; }
for (int y = 0; y < h; y++) {
int y2 = copyingUp ? y : (h - (y + 1));
TerminalRow sourceRow = allocateFullLineIfNecessary(externalToInternalRow(sy + y2));
allocateFullLineIfNecessary(externalToInternalRow(dy + y2)).copyInterval(sourceRow, sx, sx + w, dx);
}
}
/** /**
* Block set characters. All characters must be within the bounds of the screen, or else and * Block set characters. All characters must be within the bounds of the screen, or else and
* InvalidParemeterException will be thrown. Typically this is called with a "val" argument of 32 to clear a block * InvalidParemeterException will be thrown. Typically this is called with a "val" argument of 32 to clear a block
* of characters. * of characters.
*/ */
public void blockSet(int sx, int sy, int w, int h, int val, int style) { public void blockSet(int sx, int sy, int w, int h, int val, int style) {
if (sx < 0 || sx + w > mColumns || sy < 0 || sy + h > mScreenRows) { if (sx < 0 || sx + w > mColumns || sy < 0 || sy + h > mScreenRows) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
"Illegal arguments! blockSet(" + sx + ", " + sy + ", " + w + ", " + h + ", " + val + ", " + mColumns + ", " + mScreenRows + ")"); "Illegal arguments! blockSet(" + sx + ", " + sy + ", " + w + ", " + h + ", " + val + ", " + mColumns + ", " + mScreenRows + ")");
} }
for (int y = 0; y < h; y++) for (int y = 0; y < h; y++)
for (int x = 0; x < w; x++) for (int x = 0; x < w; x++)
setChar(sx + x, sy + y, val, style); setChar(sx + x, sy + y, val, style);
} }
public TerminalRow allocateFullLineIfNecessary(int row) { public TerminalRow allocateFullLineIfNecessary(int row) {
return (mLines[row] == null) ? (mLines[row] = new TerminalRow(mColumns, 0)) : mLines[row]; return (mLines[row] == null) ? (mLines[row] = new TerminalRow(mColumns, 0)) : mLines[row];
} }
public void setChar(int column, int row, int codePoint, int style) { public void setChar(int column, int row, int codePoint, int style) {
if (row >= mScreenRows || column >= mColumns) if (row >= mScreenRows || column >= mColumns)
throw new IllegalArgumentException("row=" + row + ", column=" + column + ", mScreenRows=" + mScreenRows + ", mColumns=" + mColumns); throw new IllegalArgumentException("row=" + row + ", column=" + column + ", mScreenRows=" + mScreenRows + ", mColumns=" + mColumns);
row = externalToInternalRow(row); row = externalToInternalRow(row);
allocateFullLineIfNecessary(row).setChar(column, codePoint, style); allocateFullLineIfNecessary(row).setChar(column, codePoint, style);
} }
public int getStyleAt(int externalRow, int column) { public int getStyleAt(int externalRow, int column) {
return allocateFullLineIfNecessary(externalToInternalRow(externalRow)).getStyle(column); return allocateFullLineIfNecessary(externalToInternalRow(externalRow)).getStyle(column);
} }
/** Support for http://vt100.net/docs/vt510-rm/DECCARA and http://vt100.net/docs/vt510-rm/DECCARA */ /** Support for http://vt100.net/docs/vt510-rm/DECCARA and http://vt100.net/docs/vt510-rm/DECCARA */
public void setOrClearEffect(int bits, boolean setOrClear, boolean reverse, boolean rectangular, int leftMargin, int rightMargin, int top, int left, public void setOrClearEffect(int bits, boolean setOrClear, boolean reverse, boolean rectangular, int leftMargin, int rightMargin, int top, int left,
int bottom, int right) { int bottom, int right) {
for (int y = top; y < bottom; y++) { for (int y = top; y < bottom; y++) {
TerminalRow line = mLines[externalToInternalRow(y)]; TerminalRow line = mLines[externalToInternalRow(y)];
int startOfLine = (rectangular || y == top) ? left : leftMargin; int startOfLine = (rectangular || y == top) ? left : leftMargin;
int endOfLine = (rectangular || y + 1 == bottom) ? right : rightMargin; int endOfLine = (rectangular || y + 1 == bottom) ? right : rightMargin;
for (int x = startOfLine; x < endOfLine; x++) { for (int x = startOfLine; x < endOfLine; x++) {
int currentStyle = line.getStyle(x); int currentStyle = line.getStyle(x);
int foreColor = TextStyle.decodeForeColor(currentStyle); int foreColor = TextStyle.decodeForeColor(currentStyle);
int backColor = TextStyle.decodeBackColor(currentStyle); int backColor = TextStyle.decodeBackColor(currentStyle);
int effect = TextStyle.decodeEffect(currentStyle); int effect = TextStyle.decodeEffect(currentStyle);
if (reverse) { if (reverse) {
// Clear out the bits to reverse and add them back in reversed: // Clear out the bits to reverse and add them back in reversed:
effect = (effect & ~bits) | (bits & ~effect); effect = (effect & ~bits) | (bits & ~effect);
} else if (setOrClear) { } else if (setOrClear) {
effect |= bits; effect |= bits;
} else { } else {
effect &= ~bits; effect &= ~bits;
} }
line.mStyle[x] = TextStyle.encode(foreColor, backColor, effect); line.mStyle[x] = TextStyle.encode(foreColor, backColor, effect);
} }
} }
} }
} }

View File

@@ -6,97 +6,98 @@ import java.util.Properties;
/** /**
* Color scheme for a terminal with default colors, which may be overridden (and then reset) from the shell using * Color scheme for a terminal with default colors, which may be overridden (and then reset) from the shell using
* Operating System Control (OSC) sequences. * Operating System Control (OSC) sequences.
* *
* @see TerminalColors * @see TerminalColors
*/ */
public final class TerminalColorScheme { public final class TerminalColorScheme {
/** http://upload.wikimedia.org/wikipedia/en/1/15/Xterm_256color_chart.svg, but with blue color brighter. */ /** http://upload.wikimedia.org/wikipedia/en/1/15/Xterm_256color_chart.svg, but with blue color brighter. */
private static final int[] DEFAULT_COLORSCHEME = { private static final int[] DEFAULT_COLORSCHEME = {
// 16 original colors. First 8 are dim. // 16 original colors. First 8 are dim.
0xff000000, // black 0xff000000, // black
0xffcd0000, // dim red 0xffcd0000, // dim red
0xff00cd00, // dim green 0xff00cd00, // dim green
0xffcdcd00, // dim yellow 0xffcdcd00, // dim yellow
0xff6495ed, // dim blue 0xff6495ed, // dim blue
0xffcd00cd, // dim magenta 0xffcd00cd, // dim magenta
0xff00cdcd, // dim cyan 0xff00cdcd, // dim cyan
0xffe5e5e5, // dim white 0xffe5e5e5, // dim white
// Second 8 are bright: // Second 8 are bright:
0xff7f7f7f, // medium grey 0xff7f7f7f, // medium grey
0xffff0000, // bright red 0xffff0000, // bright red
0xff00ff00, // bright green 0xff00ff00, // bright green
0xffffff00, // bright yellow 0xffffff00, // bright yellow
0xff5c5cff, // light blue 0xff5c5cff, // light blue
0xffff00ff, // bright magenta 0xffff00ff, // bright magenta
0xff00ffff, // bright cyan 0xff00ffff, // bright cyan
0xffffffff, // bright white 0xffffffff, // bright white
// 216 color cube, six shades of each color: // 216 color cube, six shades of each color:
0xff000000, 0xff00005f, 0xff000087, 0xff0000af, 0xff0000d7, 0xff0000ff, 0xff005f00, 0xff005f5f, 0xff005f87, 0xff005faf, 0xff005fd7, 0xff005fff, 0xff000000, 0xff00005f, 0xff000087, 0xff0000af, 0xff0000d7, 0xff0000ff, 0xff005f00, 0xff005f5f, 0xff005f87, 0xff005faf, 0xff005fd7, 0xff005fff,
0xff008700, 0xff00875f, 0xff008787, 0xff0087af, 0xff0087d7, 0xff0087ff, 0xff00af00, 0xff00af5f, 0xff00af87, 0xff00afaf, 0xff00afd7, 0xff00afff, 0xff008700, 0xff00875f, 0xff008787, 0xff0087af, 0xff0087d7, 0xff0087ff, 0xff00af00, 0xff00af5f, 0xff00af87, 0xff00afaf, 0xff00afd7, 0xff00afff,
0xff00d700, 0xff00d75f, 0xff00d787, 0xff00d7af, 0xff00d7d7, 0xff00d7ff, 0xff00ff00, 0xff00ff5f, 0xff00ff87, 0xff00ffaf, 0xff00ffd7, 0xff00ffff, 0xff00d700, 0xff00d75f, 0xff00d787, 0xff00d7af, 0xff00d7d7, 0xff00d7ff, 0xff00ff00, 0xff00ff5f, 0xff00ff87, 0xff00ffaf, 0xff00ffd7, 0xff00ffff,
0xff5f0000, 0xff5f005f, 0xff5f0087, 0xff5f00af, 0xff5f00d7, 0xff5f00ff, 0xff5f5f00, 0xff5f5f5f, 0xff5f5f87, 0xff5f5faf, 0xff5f5fd7, 0xff5f5fff, 0xff5f0000, 0xff5f005f, 0xff5f0087, 0xff5f00af, 0xff5f00d7, 0xff5f00ff, 0xff5f5f00, 0xff5f5f5f, 0xff5f5f87, 0xff5f5faf, 0xff5f5fd7, 0xff5f5fff,
0xff5f8700, 0xff5f875f, 0xff5f8787, 0xff5f87af, 0xff5f87d7, 0xff5f87ff, 0xff5faf00, 0xff5faf5f, 0xff5faf87, 0xff5fafaf, 0xff5fafd7, 0xff5fafff, 0xff5f8700, 0xff5f875f, 0xff5f8787, 0xff5f87af, 0xff5f87d7, 0xff5f87ff, 0xff5faf00, 0xff5faf5f, 0xff5faf87, 0xff5fafaf, 0xff5fafd7, 0xff5fafff,
0xff5fd700, 0xff5fd75f, 0xff5fd787, 0xff5fd7af, 0xff5fd7d7, 0xff5fd7ff, 0xff5fff00, 0xff5fff5f, 0xff5fff87, 0xff5fffaf, 0xff5fffd7, 0xff5fffff, 0xff5fd700, 0xff5fd75f, 0xff5fd787, 0xff5fd7af, 0xff5fd7d7, 0xff5fd7ff, 0xff5fff00, 0xff5fff5f, 0xff5fff87, 0xff5fffaf, 0xff5fffd7, 0xff5fffff,
0xff870000, 0xff87005f, 0xff870087, 0xff8700af, 0xff8700d7, 0xff8700ff, 0xff875f00, 0xff875f5f, 0xff875f87, 0xff875faf, 0xff875fd7, 0xff875fff, 0xff870000, 0xff87005f, 0xff870087, 0xff8700af, 0xff8700d7, 0xff8700ff, 0xff875f00, 0xff875f5f, 0xff875f87, 0xff875faf, 0xff875fd7, 0xff875fff,
0xff878700, 0xff87875f, 0xff878787, 0xff8787af, 0xff8787d7, 0xff8787ff, 0xff87af00, 0xff87af5f, 0xff87af87, 0xff87afaf, 0xff87afd7, 0xff87afff, 0xff878700, 0xff87875f, 0xff878787, 0xff8787af, 0xff8787d7, 0xff8787ff, 0xff87af00, 0xff87af5f, 0xff87af87, 0xff87afaf, 0xff87afd7, 0xff87afff,
0xff87d700, 0xff87d75f, 0xff87d787, 0xff87d7af, 0xff87d7d7, 0xff87d7ff, 0xff87ff00, 0xff87ff5f, 0xff87ff87, 0xff87ffaf, 0xff87ffd7, 0xff87ffff, 0xff87d700, 0xff87d75f, 0xff87d787, 0xff87d7af, 0xff87d7d7, 0xff87d7ff, 0xff87ff00, 0xff87ff5f, 0xff87ff87, 0xff87ffaf, 0xff87ffd7, 0xff87ffff,
0xffaf0000, 0xffaf005f, 0xffaf0087, 0xffaf00af, 0xffaf00d7, 0xffaf00ff, 0xffaf5f00, 0xffaf5f5f, 0xffaf5f87, 0xffaf5faf, 0xffaf5fd7, 0xffaf5fff, 0xffaf0000, 0xffaf005f, 0xffaf0087, 0xffaf00af, 0xffaf00d7, 0xffaf00ff, 0xffaf5f00, 0xffaf5f5f, 0xffaf5f87, 0xffaf5faf, 0xffaf5fd7, 0xffaf5fff,
0xffaf8700, 0xffaf875f, 0xffaf8787, 0xffaf87af, 0xffaf87d7, 0xffaf87ff, 0xffafaf00, 0xffafaf5f, 0xffafaf87, 0xffafafaf, 0xffafafd7, 0xffafafff, 0xffaf8700, 0xffaf875f, 0xffaf8787, 0xffaf87af, 0xffaf87d7, 0xffaf87ff, 0xffafaf00, 0xffafaf5f, 0xffafaf87, 0xffafafaf, 0xffafafd7, 0xffafafff,
0xffafd700, 0xffafd75f, 0xffafd787, 0xffafd7af, 0xffafd7d7, 0xffafd7ff, 0xffafff00, 0xffafff5f, 0xffafff87, 0xffafffaf, 0xffafffd7, 0xffafffff, 0xffafd700, 0xffafd75f, 0xffafd787, 0xffafd7af, 0xffafd7d7, 0xffafd7ff, 0xffafff00, 0xffafff5f, 0xffafff87, 0xffafffaf, 0xffafffd7, 0xffafffff,
0xffd70000, 0xffd7005f, 0xffd70087, 0xffd700af, 0xffd700d7, 0xffd700ff, 0xffd75f00, 0xffd75f5f, 0xffd75f87, 0xffd75faf, 0xffd75fd7, 0xffd75fff, 0xffd70000, 0xffd7005f, 0xffd70087, 0xffd700af, 0xffd700d7, 0xffd700ff, 0xffd75f00, 0xffd75f5f, 0xffd75f87, 0xffd75faf, 0xffd75fd7, 0xffd75fff,
0xffd78700, 0xffd7875f, 0xffd78787, 0xffd787af, 0xffd787d7, 0xffd787ff, 0xffd7af00, 0xffd7af5f, 0xffd7af87, 0xffd7afaf, 0xffd7afd7, 0xffd7afff, 0xffd78700, 0xffd7875f, 0xffd78787, 0xffd787af, 0xffd787d7, 0xffd787ff, 0xffd7af00, 0xffd7af5f, 0xffd7af87, 0xffd7afaf, 0xffd7afd7, 0xffd7afff,
0xffd7d700, 0xffd7d75f, 0xffd7d787, 0xffd7d7af, 0xffd7d7d7, 0xffd7d7ff, 0xffd7ff00, 0xffd7ff5f, 0xffd7ff87, 0xffd7ffaf, 0xffd7ffd7, 0xffd7ffff, 0xffd7d700, 0xffd7d75f, 0xffd7d787, 0xffd7d7af, 0xffd7d7d7, 0xffd7d7ff, 0xffd7ff00, 0xffd7ff5f, 0xffd7ff87, 0xffd7ffaf, 0xffd7ffd7, 0xffd7ffff,
0xffff0000, 0xffff005f, 0xffff0087, 0xffff00af, 0xffff00d7, 0xffff00ff, 0xffff5f00, 0xffff5f5f, 0xffff5f87, 0xffff5faf, 0xffff5fd7, 0xffff5fff, 0xffff0000, 0xffff005f, 0xffff0087, 0xffff00af, 0xffff00d7, 0xffff00ff, 0xffff5f00, 0xffff5f5f, 0xffff5f87, 0xffff5faf, 0xffff5fd7, 0xffff5fff,
0xffff8700, 0xffff875f, 0xffff8787, 0xffff87af, 0xffff87d7, 0xffff87ff, 0xffffaf00, 0xffffaf5f, 0xffffaf87, 0xffffafaf, 0xffffafd7, 0xffffafff, 0xffff8700, 0xffff875f, 0xffff8787, 0xffff87af, 0xffff87d7, 0xffff87ff, 0xffffaf00, 0xffffaf5f, 0xffffaf87, 0xffffafaf, 0xffffafd7, 0xffffafff,
0xffffd700, 0xffffd75f, 0xffffd787, 0xffffd7af, 0xffffd7d7, 0xffffd7ff, 0xffffff00, 0xffffff5f, 0xffffff87, 0xffffffaf, 0xffffffd7, 0xffffffff, 0xffffd700, 0xffffd75f, 0xffffd787, 0xffffd7af, 0xffffd7d7, 0xffffd7ff, 0xffffff00, 0xffffff5f, 0xffffff87, 0xffffffaf, 0xffffffd7, 0xffffffff,
// 24 grey scale ramp: // 24 grey scale ramp:
0xff080808, 0xff121212, 0xff1c1c1c, 0xff262626, 0xff303030, 0xff3a3a3a, 0xff444444, 0xff4e4e4e, 0xff585858, 0xff626262, 0xff6c6c6c, 0xff767676, 0xff080808, 0xff121212, 0xff1c1c1c, 0xff262626, 0xff303030, 0xff3a3a3a, 0xff444444, 0xff4e4e4e, 0xff585858, 0xff626262, 0xff6c6c6c, 0xff767676,
0xff808080, 0xff8a8a8a, 0xff949494, 0xff9e9e9e, 0xffa8a8a8, 0xffb2b2b2, 0xffbcbcbc, 0xffc6c6c6, 0xffd0d0d0, 0xffdadada, 0xffe4e4e4, 0xffeeeeee, 0xff808080, 0xff8a8a8a, 0xff949494, 0xff9e9e9e, 0xffa8a8a8, 0xffb2b2b2, 0xffbcbcbc, 0xffc6c6c6, 0xffd0d0d0, 0xffdadada, 0xffe4e4e4, 0xffeeeeee,
// COLOR_INDEX_DEFAULT_FOREGROUND, COLOR_INDEX_DEFAULT_BACKGROUND and COLOR_INDEX_DEFAULT_CURSOR: // COLOR_INDEX_DEFAULT_FOREGROUND, COLOR_INDEX_DEFAULT_BACKGROUND and COLOR_INDEX_DEFAULT_CURSOR:
0xffffffff, 0xff000000, 0xffffffff }; 0xffffffff, 0xff000000, 0xffffffff};
public final int[] mDefaultColors = new int[TextStyle.NUM_INDEXED_COLORS]; public final int[] mDefaultColors = new int[TextStyle.NUM_INDEXED_COLORS];
public TerminalColorScheme() { public TerminalColorScheme() {
reset(); reset();
} }
private void reset() { private void reset() {
System.arraycopy(DEFAULT_COLORSCHEME, 0, mDefaultColors, 0, TextStyle.NUM_INDEXED_COLORS); System.arraycopy(DEFAULT_COLORSCHEME, 0, mDefaultColors, 0, TextStyle.NUM_INDEXED_COLORS);
} }
public void updateWith(Properties props) { public void updateWith(Properties props) {
reset(); reset();
for (Map.Entry<Object, Object> entries : props.entrySet()) { for (Map.Entry<Object, Object> entries : props.entrySet()) {
String key = (String) entries.getKey(); String key = (String) entries.getKey();
String value = (String) entries.getValue(); String value = (String) entries.getValue();
int colorIndex; int colorIndex;
if (key.equals("foreground")) { if (key.equals("foreground")) {
colorIndex = TextStyle.COLOR_INDEX_FOREGROUND; colorIndex = TextStyle.COLOR_INDEX_FOREGROUND;
} else if (key.equals("background")) { } else if (key.equals("background")) {
colorIndex = TextStyle.COLOR_INDEX_BACKGROUND; colorIndex = TextStyle.COLOR_INDEX_BACKGROUND;
} else if (key.equals("cursor")) { } else if (key.equals("cursor")) {
colorIndex = TextStyle.COLOR_INDEX_CURSOR; colorIndex = TextStyle.COLOR_INDEX_CURSOR;
} else if (key.startsWith("color")) { } else if (key.startsWith("color")) {
try { try {
colorIndex = Integer.parseInt(key.substring(5)); colorIndex = Integer.parseInt(key.substring(5));
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid property: '" + key + "'"); throw new IllegalArgumentException("Invalid property: '" + key + "'");
} }
} else { } else {
throw new IllegalArgumentException("Invalid property: '" + key + "'"); throw new IllegalArgumentException("Invalid property: '" + key + "'");
} }
int colorValue = TerminalColors.parse(value); int colorValue = TerminalColors.parse(value);
if (colorValue == 0) throw new IllegalArgumentException("Property '" + key + "' has invalid color: '" + value + "'"); if (colorValue == 0)
throw new IllegalArgumentException("Property '" + key + "' has invalid color: '" + value + "'");
mDefaultColors[colorIndex] = colorValue; mDefaultColors[colorIndex] = colorValue;
} }
} }
} }

View File

@@ -3,74 +3,74 @@ package com.termux.terminal;
/** Current terminal colors (if different from default). */ /** Current terminal colors (if different from default). */
public final class TerminalColors { public final class TerminalColors {
/** Static data - a bit ugly but ok for now. */ /** Static data - a bit ugly but ok for now. */
public static final TerminalColorScheme COLOR_SCHEME = new TerminalColorScheme(); public static final TerminalColorScheme COLOR_SCHEME = new TerminalColorScheme();
/** /**
* The current terminal colors, which are normally set from the color theme, but may be set dynamically with the OSC * The current terminal colors, which are normally set from the color theme, but may be set dynamically with the OSC
* 4 control sequence. * 4 control sequence.
*/ */
public final int[] mCurrentColors = new int[TextStyle.NUM_INDEXED_COLORS]; public final int[] mCurrentColors = new int[TextStyle.NUM_INDEXED_COLORS];
/** Create a new instance with default colors from the theme. */ /** Create a new instance with default colors from the theme. */
public TerminalColors() { public TerminalColors() {
reset(); reset();
} }
/** Reset a particular indexed color with the default color from the color theme. */ /** Reset a particular indexed color with the default color from the color theme. */
public void reset(int index) { public void reset(int index) {
mCurrentColors[index] = COLOR_SCHEME.mDefaultColors[index]; mCurrentColors[index] = COLOR_SCHEME.mDefaultColors[index];
} }
/** Reset all indexed colors with the default color from the color theme. */ /** Reset all indexed colors with the default color from the color theme. */
public void reset() { public void reset() {
System.arraycopy(COLOR_SCHEME.mDefaultColors, 0, mCurrentColors, 0, TextStyle.NUM_INDEXED_COLORS); System.arraycopy(COLOR_SCHEME.mDefaultColors, 0, mCurrentColors, 0, TextStyle.NUM_INDEXED_COLORS);
} }
/** /**
* Parse color according to http://manpages.ubuntu.com/manpages/intrepid/man3/XQueryColor.3.html * Parse color according to http://manpages.ubuntu.com/manpages/intrepid/man3/XQueryColor.3.html
* * <p/>
* Highest bit is set if successful, so return value is 0xFF${R}${G}${B}. Return 0 if failed. * Highest bit is set if successful, so return value is 0xFF${R}${G}${B}. Return 0 if failed.
*/ */
static int parse(String c) { static int parse(String c) {
try { try {
int skipInitial, skipBetween; int skipInitial, skipBetween;
if (c.charAt(0) == '#') { if (c.charAt(0) == '#') {
// #RGB, #RRGGBB, #RRRGGGBBB or #RRRRGGGGBBBB. Most significant bits. // #RGB, #RRGGBB, #RRRGGGBBB or #RRRRGGGGBBBB. Most significant bits.
skipInitial = 1; skipInitial = 1;
skipBetween = 0; skipBetween = 0;
} else if (c.startsWith("rgb:")) { } else if (c.startsWith("rgb:")) {
// rgb:<red>/<green>/<blue> where <red>, <green>, <blue> := h | hh | hhh | hhhh. Scaled. // rgb:<red>/<green>/<blue> where <red>, <green>, <blue> := h | hh | hhh | hhhh. Scaled.
skipInitial = 4; skipInitial = 4;
skipBetween = 1; skipBetween = 1;
} else { } else {
return 0; return 0;
} }
int charsForColors = c.length() - skipInitial - 2 * skipBetween; int charsForColors = c.length() - skipInitial - 2 * skipBetween;
if (charsForColors % 3 != 0) return 0; // Unequal lengths. if (charsForColors % 3 != 0) return 0; // Unequal lengths.
int componentLength = charsForColors / 3; int componentLength = charsForColors / 3;
double mult = 255 / (Math.pow(2, componentLength * 4) - 1); double mult = 255 / (Math.pow(2, componentLength * 4) - 1);
int currentPosition = skipInitial; int currentPosition = skipInitial;
String rString = c.substring(currentPosition, currentPosition + componentLength); String rString = c.substring(currentPosition, currentPosition + componentLength);
currentPosition += componentLength + skipBetween; currentPosition += componentLength + skipBetween;
String gString = c.substring(currentPosition, currentPosition + componentLength); String gString = c.substring(currentPosition, currentPosition + componentLength);
currentPosition += componentLength + skipBetween; currentPosition += componentLength + skipBetween;
String bString = c.substring(currentPosition, currentPosition + componentLength); String bString = c.substring(currentPosition, currentPosition + componentLength);
int r = (int) (Integer.parseInt(rString, 16) * mult); int r = (int) (Integer.parseInt(rString, 16) * mult);
int g = (int) (Integer.parseInt(gString, 16) * mult); int g = (int) (Integer.parseInt(gString, 16) * mult);
int b = (int) (Integer.parseInt(bString, 16) * mult); int b = (int) (Integer.parseInt(bString, 16) * mult);
return 0xFF << 24 | r << 16 | g << 8 | b; return 0xFF << 24 | r << 16 | g << 8 | b;
} catch (NumberFormatException | IndexOutOfBoundsException e) { } catch (NumberFormatException | IndexOutOfBoundsException e) {
return 0; return 0;
} }
} }
/** Try parse a color from a text parameter and into a specified index. */ /** Try parse a color from a text parameter and into a specified index. */
public void tryParseColor(int intoIndex, String textParameter) { public void tryParseColor(int intoIndex, String textParameter) {
int c = parse(textParameter); int c = parse(textParameter);
if (c != 0) mCurrentColors[intoIndex] = c; if (c != 0) mCurrentColors[intoIndex] = c;
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -5,23 +5,23 @@ import java.nio.charset.StandardCharsets;
/** A client which receives callbacks from events triggered by feeding input to a {@link TerminalEmulator}. */ /** A client which receives callbacks from events triggered by feeding input to a {@link TerminalEmulator}. */
public abstract class TerminalOutput { public abstract class TerminalOutput {
/** Write a string using the UTF-8 encoding to the terminal client. */ /** Write a string using the UTF-8 encoding to the terminal client. */
public final void write(String data) { public final void write(String data) {
byte[] bytes = data.getBytes(StandardCharsets.UTF_8); byte[] bytes = data.getBytes(StandardCharsets.UTF_8);
write(bytes, 0, bytes.length); write(bytes, 0, bytes.length);
} }
/** Write bytes to the terminal client. */ /** Write bytes to the terminal client. */
public abstract void write(byte[] data, int offset, int count); public abstract void write(byte[] data, int offset, int count);
/** Notify the terminal client that the terminal title has changed. */ /** Notify the terminal client that the terminal title has changed. */
public abstract void titleChanged(String oldTitle, String newTitle); public abstract void titleChanged(String oldTitle, String newTitle);
/** Notify the terminal client that the terminal title has changed. */ /** Notify the terminal client that the terminal title has changed. */
public abstract void clipboardText(String text); public abstract void clipboardText(String text);
/** Notify the terminal client that a bell character (ASCII 7, bell, BEL, \a, ^G)) has been received. */ /** Notify the terminal client that a bell character (ASCII 7, bell, BEL, \a, ^G)) has been received. */
public abstract void onBell(); public abstract void onBell();
public abstract void onColorsChanged(); public abstract void onColorsChanged();

View File

@@ -4,229 +4,229 @@ import java.util.Arrays;
/** /**
* A row in a terminal, composed of a fixed number of cells. * A row in a terminal, composed of a fixed number of cells.
* * <p/>
* The text in the row is stored in a char[] array, {@link #mText}, for quick access during rendering. * The text in the row is stored in a char[] array, {@link #mText}, for quick access during rendering.
*/ */
public final class TerminalRow { public final class TerminalRow {
private static final float SPARE_CAPACITY_FACTOR = 1.5f; private static final float SPARE_CAPACITY_FACTOR = 1.5f;
/** The number of columns in this terminal row. */ /** The number of columns in this terminal row. */
private final int mColumns; private final int mColumns;
/** The text filling this terminal row. */ /** The text filling this terminal row. */
public char[] mText; public char[] mText;
/** The number of java char:s used in {@link #mText}. */ /** The number of java char:s used in {@link #mText}. */
private short mSpaceUsed; private short mSpaceUsed;
/** If this row has been line wrapped due to text output at the end of line. */ /** If this row has been line wrapped due to text output at the end of line. */
boolean mLineWrap; boolean mLineWrap;
/** The style bits of each cell in the row. See {@link TextStyle}. */ /** The style bits of each cell in the row. See {@link TextStyle}. */
final int[] mStyle; final int[] mStyle;
/** Construct a blank row (containing only whitespace, ' ') with a specified style. */ /** Construct a blank row (containing only whitespace, ' ') with a specified style. */
public TerminalRow(int columns, int style) { public TerminalRow(int columns, int style) {
mColumns = columns; mColumns = columns;
mText = new char[(int) (SPARE_CAPACITY_FACTOR * columns)]; mText = new char[(int) (SPARE_CAPACITY_FACTOR * columns)];
mStyle = new int[columns]; mStyle = new int[columns];
clear(style); clear(style);
} }
/** NOTE: The sourceX2 is exclusive. */ /** NOTE: The sourceX2 is exclusive. */
public void copyInterval(TerminalRow line, int sourceX1, int sourceX2, int destinationX) { public void copyInterval(TerminalRow line, int sourceX1, int sourceX2, int destinationX) {
final int x1 = line.findStartOfColumn(sourceX1); final int x1 = line.findStartOfColumn(sourceX1);
final int x2 = line.findStartOfColumn(sourceX2); final int x2 = line.findStartOfColumn(sourceX2);
boolean startingFromSecondHalfOfWideChar = (sourceX1 > 0 && line.wideDisplayCharacterStartingAt(sourceX1 - 1)); boolean startingFromSecondHalfOfWideChar = (sourceX1 > 0 && line.wideDisplayCharacterStartingAt(sourceX1 - 1));
final char[] sourceChars = (this == line) ? Arrays.copyOf(line.mText, line.mText.length) : line.mText; final char[] sourceChars = (this == line) ? Arrays.copyOf(line.mText, line.mText.length) : line.mText;
int latestNonCombiningWidth = 0; int latestNonCombiningWidth = 0;
for (int i = x1; i < x2; i++) { for (int i = x1; i < x2; i++) {
char sourceChar = sourceChars[i]; char sourceChar = sourceChars[i];
int codePoint = Character.isHighSurrogate(sourceChar) ? Character.toCodePoint(sourceChar, sourceChars[++i]) : sourceChar; int codePoint = Character.isHighSurrogate(sourceChar) ? Character.toCodePoint(sourceChar, sourceChars[++i]) : sourceChar;
if (startingFromSecondHalfOfWideChar) { if (startingFromSecondHalfOfWideChar) {
// Just treat copying second half of wide char as copying whitespace. // Just treat copying second half of wide char as copying whitespace.
codePoint = ' '; codePoint = ' ';
startingFromSecondHalfOfWideChar = false; startingFromSecondHalfOfWideChar = false;
} }
int w = WcWidth.width(codePoint); int w = WcWidth.width(codePoint);
if (w > 0) { if (w > 0) {
destinationX += latestNonCombiningWidth; destinationX += latestNonCombiningWidth;
sourceX1 += latestNonCombiningWidth; sourceX1 += latestNonCombiningWidth;
latestNonCombiningWidth = w; latestNonCombiningWidth = w;
} }
setChar(destinationX, codePoint, line.getStyle(sourceX1)); setChar(destinationX, codePoint, line.getStyle(sourceX1));
} }
} }
public int getSpaceUsed() { public int getSpaceUsed() {
return mSpaceUsed; return mSpaceUsed;
} }
/** Note that the column may end of second half of wide character. */ /** Note that the column may end of second half of wide character. */
public int findStartOfColumn(int column) { public int findStartOfColumn(int column) {
if (column == mColumns) return getSpaceUsed(); if (column == mColumns) return getSpaceUsed();
int currentColumn = 0; int currentColumn = 0;
int currentCharIndex = 0; int currentCharIndex = 0;
while (true) { // 0<2 1 < 2 while (true) { // 0<2 1 < 2
int newCharIndex = currentCharIndex; int newCharIndex = currentCharIndex;
char c = mText[newCharIndex++]; // cci=1, cci=2 char c = mText[newCharIndex++]; // cci=1, cci=2
boolean isHigh = Character.isHighSurrogate(c); boolean isHigh = Character.isHighSurrogate(c);
int codePoint = isHigh ? Character.toCodePoint(c, mText[newCharIndex++]) : c; int codePoint = isHigh ? Character.toCodePoint(c, mText[newCharIndex++]) : c;
int wcwidth = WcWidth.width(codePoint); // 1, 2 int wcwidth = WcWidth.width(codePoint); // 1, 2
if (wcwidth > 0) { if (wcwidth > 0) {
currentColumn += wcwidth; currentColumn += wcwidth;
if (currentColumn == column) { if (currentColumn == column) {
while (newCharIndex < mSpaceUsed) { while (newCharIndex < mSpaceUsed) {
// Skip combining chars. // Skip combining chars.
if (Character.isHighSurrogate(mText[newCharIndex])) { if (Character.isHighSurrogate(mText[newCharIndex])) {
if (WcWidth.width(Character.toCodePoint(mText[newCharIndex], mText[newCharIndex + 1])) <= 0) { if (WcWidth.width(Character.toCodePoint(mText[newCharIndex], mText[newCharIndex + 1])) <= 0) {
newCharIndex += 2; newCharIndex += 2;
} else { } else {
break; break;
} }
} else if (WcWidth.width(mText[newCharIndex]) <= 0) { } else if (WcWidth.width(mText[newCharIndex]) <= 0) {
newCharIndex++; newCharIndex++;
} else { } else {
break; break;
} }
} }
return newCharIndex; return newCharIndex;
} else if (currentColumn > column) { } else if (currentColumn > column) {
// Wide column going past end. // Wide column going past end.
return currentCharIndex; return currentCharIndex;
} }
} }
currentCharIndex = newCharIndex; currentCharIndex = newCharIndex;
} }
} }
private boolean wideDisplayCharacterStartingAt(int column) { private boolean wideDisplayCharacterStartingAt(int column) {
for (int currentCharIndex = 0, currentColumn = 0; currentCharIndex < mSpaceUsed;) { for (int currentCharIndex = 0, currentColumn = 0; currentCharIndex < mSpaceUsed; ) {
char c = mText[currentCharIndex++]; char c = mText[currentCharIndex++];
int codePoint = Character.isHighSurrogate(c) ? Character.toCodePoint(c, mText[currentCharIndex++]) : c; int codePoint = Character.isHighSurrogate(c) ? Character.toCodePoint(c, mText[currentCharIndex++]) : c;
int wcwidth = WcWidth.width(codePoint); int wcwidth = WcWidth.width(codePoint);
if (wcwidth > 0) { if (wcwidth > 0) {
if (currentColumn == column && wcwidth == 2) return true; if (currentColumn == column && wcwidth == 2) return true;
currentColumn += wcwidth; currentColumn += wcwidth;
if (currentColumn > column) return false; if (currentColumn > column) return false;
} }
} }
return false; return false;
} }
public void clear(int style) { public void clear(int style) {
Arrays.fill(mText, ' '); Arrays.fill(mText, ' ');
Arrays.fill(mStyle, style); Arrays.fill(mStyle, style);
mSpaceUsed = (short) mColumns; mSpaceUsed = (short) mColumns;
} }
// https://github.com/steven676/Android-Terminal-Emulator/commit/9a47042620bec87617f0b4f5d50568535668fe26 // https://github.com/steven676/Android-Terminal-Emulator/commit/9a47042620bec87617f0b4f5d50568535668fe26
public void setChar(int columnToSet, int codePoint, int style) { public void setChar(int columnToSet, int codePoint, int style) {
mStyle[columnToSet] = style; mStyle[columnToSet] = style;
final int newCodePointDisplayWidth = WcWidth.width(codePoint); final int newCodePointDisplayWidth = WcWidth.width(codePoint);
final boolean newIsCombining = newCodePointDisplayWidth <= 0; final boolean newIsCombining = newCodePointDisplayWidth <= 0;
boolean wasExtraColForWideChar = (columnToSet > 0) && wideDisplayCharacterStartingAt(columnToSet - 1); boolean wasExtraColForWideChar = (columnToSet > 0) && wideDisplayCharacterStartingAt(columnToSet - 1);
if (newIsCombining) { if (newIsCombining) {
// When standing at second half of wide character and inserting combining: // When standing at second half of wide character and inserting combining:
if (wasExtraColForWideChar) columnToSet--; if (wasExtraColForWideChar) columnToSet--;
} else { } else {
// Check if we are overwriting the second half of a wide character starting at the previous column: // Check if we are overwriting the second half of a wide character starting at the previous column:
if (wasExtraColForWideChar) setChar(columnToSet - 1, ' ', style); if (wasExtraColForWideChar) setChar(columnToSet - 1, ' ', style);
// Check if we are overwriting the first half of a wide character starting at the next column: // Check if we are overwriting the first half of a wide character starting at the next column:
boolean overwritingWideCharInNextColumn = newCodePointDisplayWidth == 2 && wideDisplayCharacterStartingAt(columnToSet + 1); boolean overwritingWideCharInNextColumn = newCodePointDisplayWidth == 2 && wideDisplayCharacterStartingAt(columnToSet + 1);
if (overwritingWideCharInNextColumn) setChar(columnToSet + 1, ' ', style); if (overwritingWideCharInNextColumn) setChar(columnToSet + 1, ' ', style);
} }
char[] text = mText; char[] text = mText;
final int oldStartOfColumnIndex = findStartOfColumn(columnToSet); final int oldStartOfColumnIndex = findStartOfColumn(columnToSet);
final int oldCodePointDisplayWidth = WcWidth.width(text, oldStartOfColumnIndex); final int oldCodePointDisplayWidth = WcWidth.width(text, oldStartOfColumnIndex);
// Get the number of elements in the mText array this column uses now // Get the number of elements in the mText array this column uses now
int oldCharactersUsedForColumn; int oldCharactersUsedForColumn;
if (columnToSet + oldCodePointDisplayWidth < mColumns) { if (columnToSet + oldCodePointDisplayWidth < mColumns) {
oldCharactersUsedForColumn = findStartOfColumn(columnToSet + oldCodePointDisplayWidth) - oldStartOfColumnIndex; oldCharactersUsedForColumn = findStartOfColumn(columnToSet + oldCodePointDisplayWidth) - oldStartOfColumnIndex;
} else { } else {
// Last character. // Last character.
oldCharactersUsedForColumn = mSpaceUsed - oldStartOfColumnIndex; oldCharactersUsedForColumn = mSpaceUsed - oldStartOfColumnIndex;
} }
// Find how many chars this column will need // Find how many chars this column will need
int newCharactersUsedForColumn = Character.charCount(codePoint); int newCharactersUsedForColumn = Character.charCount(codePoint);
if (newIsCombining) { if (newIsCombining) {
// Combining characters are added to the contents of the column instead of overwriting them, so that they // Combining characters are added to the contents of the column instead of overwriting them, so that they
// modify the existing contents. // modify the existing contents.
// FIXME: Put a limit of combining characters. // FIXME: Put a limit of combining characters.
// FIXME: Unassigned characters also get width=0. // FIXME: Unassigned characters also get width=0.
newCharactersUsedForColumn += oldCharactersUsedForColumn; newCharactersUsedForColumn += oldCharactersUsedForColumn;
} }
int oldNextColumnIndex = oldStartOfColumnIndex + oldCharactersUsedForColumn; int oldNextColumnIndex = oldStartOfColumnIndex + oldCharactersUsedForColumn;
int newNextColumnIndex = oldStartOfColumnIndex + newCharactersUsedForColumn; int newNextColumnIndex = oldStartOfColumnIndex + newCharactersUsedForColumn;
final int javaCharDifference = newCharactersUsedForColumn - oldCharactersUsedForColumn; final int javaCharDifference = newCharactersUsedForColumn - oldCharactersUsedForColumn;
if (javaCharDifference > 0) { if (javaCharDifference > 0) {
// Shift the rest of the line right. // Shift the rest of the line right.
int oldCharactersAfterColumn = mSpaceUsed - oldNextColumnIndex; int oldCharactersAfterColumn = mSpaceUsed - oldNextColumnIndex;
if (mSpaceUsed + javaCharDifference > text.length) { if (mSpaceUsed + javaCharDifference > text.length) {
// We need to grow the array // We need to grow the array
char[] newText = new char[text.length + mColumns]; char[] newText = new char[text.length + mColumns];
System.arraycopy(text, 0, newText, 0, oldStartOfColumnIndex + oldCharactersUsedForColumn); System.arraycopy(text, 0, newText, 0, oldStartOfColumnIndex + oldCharactersUsedForColumn);
System.arraycopy(text, oldNextColumnIndex, newText, newNextColumnIndex, oldCharactersAfterColumn); System.arraycopy(text, oldNextColumnIndex, newText, newNextColumnIndex, oldCharactersAfterColumn);
mText = text = newText; mText = text = newText;
} else { } else {
System.arraycopy(text, oldNextColumnIndex, text, newNextColumnIndex, oldCharactersAfterColumn); System.arraycopy(text, oldNextColumnIndex, text, newNextColumnIndex, oldCharactersAfterColumn);
} }
} else if (javaCharDifference < 0) { } else if (javaCharDifference < 0) {
// Shift the rest of the line left. // Shift the rest of the line left.
System.arraycopy(text, oldNextColumnIndex, text, newNextColumnIndex, mSpaceUsed - oldNextColumnIndex); System.arraycopy(text, oldNextColumnIndex, text, newNextColumnIndex, mSpaceUsed - oldNextColumnIndex);
} }
mSpaceUsed += javaCharDifference; mSpaceUsed += javaCharDifference;
// Store char. A combining character is stored at the end of the existing contents so that it modifies them: // 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. //noinspection ResultOfMethodCallIgnored - since we already now how many java chars is used.
Character.toChars(codePoint, text, oldStartOfColumnIndex + (newIsCombining ? oldCharactersUsedForColumn : 0)); Character.toChars(codePoint, text, oldStartOfColumnIndex + (newIsCombining ? oldCharactersUsedForColumn : 0));
if (oldCodePointDisplayWidth == 2 && newCodePointDisplayWidth == 1) { if (oldCodePointDisplayWidth == 2 && newCodePointDisplayWidth == 1) {
// Replace second half of wide char with a space. Which mean that we actually add a ' ' java character. // Replace second half of wide char with a space. Which mean that we actually add a ' ' java character.
if (mSpaceUsed + 1 > text.length) { if (mSpaceUsed + 1 > text.length) {
char[] newText = new char[text.length + mColumns]; char[] newText = new char[text.length + mColumns];
System.arraycopy(text, 0, newText, 0, newNextColumnIndex); System.arraycopy(text, 0, newText, 0, newNextColumnIndex);
System.arraycopy(text, newNextColumnIndex, newText, newNextColumnIndex + 1, mSpaceUsed - newNextColumnIndex); System.arraycopy(text, newNextColumnIndex, newText, newNextColumnIndex + 1, mSpaceUsed - newNextColumnIndex);
mText = text = newText; mText = text = newText;
} else { } else {
System.arraycopy(text, newNextColumnIndex, text, newNextColumnIndex + 1, mSpaceUsed - newNextColumnIndex); System.arraycopy(text, newNextColumnIndex, text, newNextColumnIndex + 1, mSpaceUsed - newNextColumnIndex);
} }
text[newNextColumnIndex] = ' '; text[newNextColumnIndex] = ' ';
++mSpaceUsed; ++mSpaceUsed;
} else if (oldCodePointDisplayWidth == 1 && newCodePointDisplayWidth == 2) { } else if (oldCodePointDisplayWidth == 1 && newCodePointDisplayWidth == 2) {
if (columnToSet == mColumns - 1) { if (columnToSet == mColumns - 1) {
throw new IllegalArgumentException("Cannot put wide character in last column"); throw new IllegalArgumentException("Cannot put wide character in last column");
} else if (columnToSet == mColumns - 2) { } else if (columnToSet == mColumns - 2) {
// Truncate the line to the second part of this wide char: // Truncate the line to the second part of this wide char:
mSpaceUsed = (short) newNextColumnIndex; mSpaceUsed = (short) newNextColumnIndex;
} else { } else {
// Overwrite the contents of the next column, which mean we actually remove java characters. Due to the // Overwrite the contents of the next column, which mean we actually remove java characters. Due to the
// check at the beginning of this method we know that we are not overwriting a wide char. // check at the beginning of this method we know that we are not overwriting a wide char.
int newNextNextColumnIndex = newNextColumnIndex + (Character.isHighSurrogate(mText[newNextColumnIndex]) ? 2 : 1); int newNextNextColumnIndex = newNextColumnIndex + (Character.isHighSurrogate(mText[newNextColumnIndex]) ? 2 : 1);
int nextLen = newNextNextColumnIndex - newNextColumnIndex; int nextLen = newNextNextColumnIndex - newNextColumnIndex;
// Shift the array leftwards. // Shift the array leftwards.
System.arraycopy(text, newNextNextColumnIndex, text, newNextColumnIndex, mSpaceUsed - newNextNextColumnIndex); System.arraycopy(text, newNextNextColumnIndex, text, newNextColumnIndex, mSpaceUsed - newNextNextColumnIndex);
mSpaceUsed -= nextLen; mSpaceUsed -= nextLen;
} }
} }
} }
boolean isBlank() { boolean isBlank() {
for (int charIndex = 0, charLen = getSpaceUsed(); charIndex < charLen; charIndex++) for (int charIndex = 0, charLen = getSpaceUsed(); charIndex < charLen; charIndex++)
if (mText[charIndex] != ' ') return false; if (mText[charIndex] != ' ') return false;
return true; return true;
} }
public final int getStyle(int column) { public final int getStyle(int column) {
return mStyle[column]; return mStyle[column];
} }
} }

View File

@@ -19,202 +19,200 @@ import java.util.UUID;
/** /**
* A terminal session, consisting of a process coupled to a terminal interface. * A terminal session, consisting of a process coupled to a terminal interface.
* <p> * <p/>
* The subprocess will be executed by the constructor, and when the size is made known by a call to * The subprocess will be executed by the constructor, and when the size is made known by a call to
* {@link #updateSize(int, int)} terminal emulation will begin and threads will be spawned to handle the subprocess I/O. * {@link #updateSize(int, int)} terminal emulation will begin and threads will be spawned to handle the subprocess I/O.
* All terminal emulation and callback methods will be performed on the main thread. * All terminal emulation and callback methods will be performed on the main thread.
* <p> * <p/>
* The child process may be exited forcefully by using the {@link #finishIfRunning()} method. * The child process may be exited forcefully by using the {@link #finishIfRunning()} method.
* * <p/>
* NOTE: The terminal session may outlive the EmulatorView, so be careful with callbacks! * NOTE: The terminal session may outlive the EmulatorView, so be careful with callbacks!
*/ */
public final class TerminalSession extends TerminalOutput { public final class TerminalSession extends TerminalOutput {
/** Callback to be invoked when a {@link TerminalSession} changes. */ /** Callback to be invoked when a {@link TerminalSession} changes. */
public interface SessionChangedCallback { public interface SessionChangedCallback {
void onTextChanged(TerminalSession changedSession); void onTextChanged(TerminalSession changedSession);
void onTitleChanged(TerminalSession changedSession); void onTitleChanged(TerminalSession changedSession);
void onSessionFinished(TerminalSession finishedSession); void onSessionFinished(TerminalSession finishedSession);
void onClipboardText(TerminalSession session, String text); void onClipboardText(TerminalSession session, String text);
void onBell(TerminalSession session); void onBell(TerminalSession session);
void onColorsChanged(TerminalSession session); void onColorsChanged(TerminalSession session);
} }
private static FileDescriptor wrapFileDescriptor(int fileDescriptor) { private static FileDescriptor wrapFileDescriptor(int fileDescriptor) {
FileDescriptor result = new FileDescriptor(); FileDescriptor result = new FileDescriptor();
try { try {
Field descriptorField; Field descriptorField;
try { try {
descriptorField = FileDescriptor.class.getDeclaredField("descriptor"); descriptorField = FileDescriptor.class.getDeclaredField("descriptor");
} catch (NoSuchFieldException e) { } catch (NoSuchFieldException e) {
// For desktop java: // For desktop java:
descriptorField = FileDescriptor.class.getDeclaredField("fd"); descriptorField = FileDescriptor.class.getDeclaredField("fd");
} }
descriptorField.setAccessible(true); descriptorField.setAccessible(true);
descriptorField.set(result, fileDescriptor); descriptorField.set(result, fileDescriptor);
} catch (NoSuchFieldException | IllegalAccessException | IllegalArgumentException e) { } catch (NoSuchFieldException | IllegalAccessException | IllegalArgumentException e) {
Log.wtf(EmulatorDebug.LOG_TAG, "Error accessing FileDescriptor#descriptor private field", e); Log.wtf(EmulatorDebug.LOG_TAG, "Error accessing FileDescriptor#descriptor private field", e);
System.exit(1); System.exit(1);
} }
return result; return result;
} }
private static final int MSG_NEW_INPUT = 1; private static final int MSG_NEW_INPUT = 1;
private static final int MSG_PROCESS_EXITED = 4; private static final int MSG_PROCESS_EXITED = 4;
public final String mHandle = UUID.randomUUID().toString(); public final String mHandle = UUID.randomUUID().toString();
TerminalEmulator mEmulator; TerminalEmulator mEmulator;
/** /**
* A queue written to from a separate thread when the process outputs, and read by main thread to process by * A queue written to from a separate thread when the process outputs, and read by main thread to process by
* terminal emulator. * terminal emulator.
*/ */
final ByteQueue mProcessToTerminalIOQueue = new ByteQueue(4096); final ByteQueue mProcessToTerminalIOQueue = new ByteQueue(4096);
/** /**
* A queue written to from the main thread due to user interaction, and read by another thread which forwards by * A queue written to from the main thread due to user interaction, and read by another thread which forwards by
* writing to the {@link #mTerminalFileDescriptor}. * writing to the {@link #mTerminalFileDescriptor}.
*/ */
final ByteQueue mTerminalToProcessIOQueue = new ByteQueue(4096); final ByteQueue mTerminalToProcessIOQueue = new ByteQueue(4096);
/** Buffer to write translate code points into utf8 before writing to mTerminalToProcessIOQueue */ /** Buffer to write translate code points into utf8 before writing to mTerminalToProcessIOQueue */
private final byte[] mUtf8InputBuffer = new byte[5]; private final byte[] mUtf8InputBuffer = new byte[5];
/** Callback which gets notified when a session finishes or changes title. */ /** Callback which gets notified when a session finishes or changes title. */
final SessionChangedCallback mChangeCallback; final SessionChangedCallback mChangeCallback;
/** The pid of the shell process. 0 if not started and -1 if finished running. */ /** The pid of the shell process. 0 if not started and -1 if finished running. */
int mShellPid; int mShellPid;
/** The exit status of the shell process. Only valid if ${@link #mShellPid} is -1. */ /** The exit status of the shell process. Only valid if ${@link #mShellPid} is -1. */
int mShellExitStatus; int mShellExitStatus;
/** /**
* The file descriptor referencing the master half of a pseudo-terminal pair, resulting from calling * The file descriptor referencing the master half of a pseudo-terminal pair, resulting from calling
* {@link JNI#createSubprocess(String, String, String[], String[], int[], int, int)}. * {@link JNI#createSubprocess(String, String, String[], String[], int[], int, int)}.
*/ */
private int mTerminalFileDescriptor; private int mTerminalFileDescriptor;
/** Set by the application for user identification of session, not by terminal. */ /** Set by the application for user identification of session, not by terminal. */
public String mSessionName; public String mSessionName;
@SuppressLint("HandlerLeak") @SuppressLint("HandlerLeak")
final Handler mMainThreadHandler = new Handler() { final Handler mMainThreadHandler = new Handler() {
final byte[] mReceiveBuffer = new byte[4 * 1024]; final byte[] mReceiveBuffer = new byte[4 * 1024];
@Override @Override
public void handleMessage(Message msg) { public void handleMessage(Message msg) {
if (msg.what == MSG_NEW_INPUT && isRunning()) { if (msg.what == MSG_NEW_INPUT && isRunning()) {
int bytesRead = mProcessToTerminalIOQueue.read(mReceiveBuffer, false); int bytesRead = mProcessToTerminalIOQueue.read(mReceiveBuffer, false);
if (bytesRead > 0) { if (bytesRead > 0) {
mEmulator.append(mReceiveBuffer, bytesRead); mEmulator.append(mReceiveBuffer, bytesRead);
notifyScreenUpdate(); notifyScreenUpdate();
} }
} else if (msg.what == MSG_PROCESS_EXITED) { } else if (msg.what == MSG_PROCESS_EXITED) {
int exitCode = (Integer) msg.obj; int exitCode = (Integer) msg.obj;
cleanupResources(exitCode); cleanupResources(exitCode);
mChangeCallback.onSessionFinished(TerminalSession.this); mChangeCallback.onSessionFinished(TerminalSession.this);
String exitDescription = "\r\n[Process completed"; String exitDescription = "\r\n[Process completed";
if (exitCode > 0) { if (exitCode > 0) {
// Non-zero process exit. // Non-zero process exit.
exitDescription += " with code " + exitCode; exitDescription += " with code " + exitCode;
} else if (exitCode < 0) { } else if (exitCode < 0) {
// Negated signal. // Negated signal.
exitDescription += " with signal " + (-exitCode); exitDescription += " with signal " + (-exitCode);
} }
exitDescription += " - press Enter to close]"; exitDescription += " - press Enter to close]";
byte[] bytesToWrite = exitDescription.getBytes(StandardCharsets.UTF_8); byte[] bytesToWrite = exitDescription.getBytes(StandardCharsets.UTF_8);
mEmulator.append(bytesToWrite, bytesToWrite.length); mEmulator.append(bytesToWrite, bytesToWrite.length);
notifyScreenUpdate(); notifyScreenUpdate();
} }
} }
}; };
private final String mShellPath; private final String mShellPath;
private final String mCwd; private final String mCwd;
private final String[] mArgs; private final String[] mArgs;
private final String[] mEnv; private final String[] mEnv;
public TerminalSession(String shellPath, String cwd, String[] args, String[] env, SessionChangedCallback changeCallback) { public TerminalSession(String shellPath, String cwd, String[] args, String[] env, SessionChangedCallback changeCallback) {
mChangeCallback = changeCallback; mChangeCallback = changeCallback;
this.mShellPath = shellPath; this.mShellPath = shellPath;
this.mCwd = cwd; this.mCwd = cwd;
this.mArgs = args; this.mArgs = args;
this.mEnv = env; this.mEnv = env;
} }
/** Inform the attached pty of the new size and reflow or initialize the emulator. */ /** Inform the attached pty of the new size and reflow or initialize the emulator. */
public void updateSize(int columns, int rows) { public void updateSize(int columns, int rows) {
if (mEmulator == null) { if (mEmulator == null) {
initializeEmulator(columns, rows); initializeEmulator(columns, rows);
} else { } else {
JNI.setPtyWindowSize(mTerminalFileDescriptor, rows, columns); JNI.setPtyWindowSize(mTerminalFileDescriptor, rows, columns);
mEmulator.resize(columns, rows); mEmulator.resize(columns, rows);
} }
} }
/** The terminal title as set through escape sequences or null if none set. */ /** The terminal title as set through escape sequences or null if none set. */
public String getTitle() { public String getTitle() {
return (mEmulator == null) ? null : mEmulator.getTitle(); return (mEmulator == null) ? null : mEmulator.getTitle();
} }
/** /**
* Set the terminal emulator's window size and start terminal emulation. * Set the terminal emulator's window size and start terminal emulation.
* *
* @param columns * @param columns The number of columns in the terminal window.
* The number of columns in the terminal window. * @param rows The number of rows in the terminal window.
* @param rows */
* The number of rows in the terminal window. public void initializeEmulator(int columns, int rows) {
*/ mEmulator = new TerminalEmulator(this, columns, rows, /* transcript= */5000);
public void initializeEmulator(int columns, int rows) {
mEmulator = new TerminalEmulator(this, columns, rows, /* transcript= */5000);
int[] processId = new int[1]; int[] processId = new int[1];
mTerminalFileDescriptor = JNI.createSubprocess(mShellPath, mCwd, mArgs, mEnv, processId, rows, columns); mTerminalFileDescriptor = JNI.createSubprocess(mShellPath, mCwd, mArgs, mEnv, processId, rows, columns);
mShellPid = processId[0]; mShellPid = processId[0];
final FileDescriptor terminalFileDescriptorWrapped = wrapFileDescriptor(mTerminalFileDescriptor); final FileDescriptor terminalFileDescriptorWrapped = wrapFileDescriptor(mTerminalFileDescriptor);
new Thread("TermSessionInputReader[pid=" + mShellPid + "]") { new Thread("TermSessionInputReader[pid=" + mShellPid + "]") {
@Override @Override
public void run() { public void run() {
try (InputStream termIn = new FileInputStream(terminalFileDescriptorWrapped)) { try (InputStream termIn = new FileInputStream(terminalFileDescriptorWrapped)) {
final byte[] buffer = new byte[4096]; final byte[] buffer = new byte[4096];
while (true) { while (true) {
int read = termIn.read(buffer); int read = termIn.read(buffer);
if (read == -1) return; if (read == -1) return;
if (!mProcessToTerminalIOQueue.write(buffer, 0, read)) return; if (!mProcessToTerminalIOQueue.write(buffer, 0, read)) return;
mMainThreadHandler.sendEmptyMessage(MSG_NEW_INPUT); mMainThreadHandler.sendEmptyMessage(MSG_NEW_INPUT);
} }
} catch (Exception e) { } catch (Exception e) {
// Ignore, just shutting down. // Ignore, just shutting down.
} }
} }
}.start(); }.start();
new Thread("TermSessionOutputWriter[pid=" + mShellPid + "]") { new Thread("TermSessionOutputWriter[pid=" + mShellPid + "]") {
@Override @Override
public void run() { public void run() {
final byte[] buffer = new byte[4096]; final byte[] buffer = new byte[4096];
try (FileOutputStream termOut = new FileOutputStream(terminalFileDescriptorWrapped)) { try (FileOutputStream termOut = new FileOutputStream(terminalFileDescriptorWrapped)) {
while (true) { while (true) {
int bytesToWrite = mTerminalToProcessIOQueue.read(buffer, true); int bytesToWrite = mTerminalToProcessIOQueue.read(buffer, true);
if (bytesToWrite == -1) return; if (bytesToWrite == -1) return;
termOut.write(buffer, 0, bytesToWrite); termOut.write(buffer, 0, bytesToWrite);
} }
} catch (IOException e) { } catch (IOException e) {
// Ignore. // Ignore.
} }
} }
}.start(); }.start();
new Thread("TermSessionWaiter[pid=" + mShellPid + "]") { new Thread("TermSessionWaiter[pid=" + mShellPid + "]") {
@Override @Override
@@ -226,63 +224,63 @@ public final class TerminalSession extends TerminalOutput {
} }
/** Write data to the shell process. */ /** Write data to the shell process. */
@Override @Override
public void write(byte[] data, int offset, int count) { public void write(byte[] data, int offset, int count) {
if (mShellPid > 0) 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. */ /** Write the Unicode code point to the terminal encoded in UTF-8. */
public void writeCodePoint(boolean prependEscape, int codePoint) { public void writeCodePoint(boolean prependEscape, int codePoint) {
if (codePoint > 1114111 || (codePoint >= 0xD800 && codePoint <= 0xDFFF)) { if (codePoint > 1114111 || (codePoint >= 0xD800 && codePoint <= 0xDFFF)) {
// 1114111 (= 2**16 + 1024**2 - 1) is the highest code point, [0xD800,0xDFFF] is the surrogate range. // 1114111 (= 2**16 + 1024**2 - 1) is the highest code point, [0xD800,0xDFFF] is the surrogate range.
throw new IllegalArgumentException("Invalid code point: " + codePoint); throw new IllegalArgumentException("Invalid code point: " + codePoint);
} }
int bufferPosition = 0; int bufferPosition = 0;
if (prependEscape) mUtf8InputBuffer[bufferPosition++] = 27; if (prependEscape) mUtf8InputBuffer[bufferPosition++] = 27;
if (codePoint <= /* 7 bits */0b1111111) { if (codePoint <= /* 7 bits */0b1111111) {
mUtf8InputBuffer[bufferPosition++] = (byte) codePoint; mUtf8InputBuffer[bufferPosition++] = (byte) codePoint;
} else if (codePoint <= /* 11 bits */0b11111111111) { } else if (codePoint <= /* 11 bits */0b11111111111) {
/* 110xxxxx leading byte with leading 5 bits */ /* 110xxxxx leading byte with leading 5 bits */
mUtf8InputBuffer[bufferPosition++] = (byte) (0b11000000 | (codePoint >> 6)); mUtf8InputBuffer[bufferPosition++] = (byte) (0b11000000 | (codePoint >> 6));
/* 10xxxxxx continuation byte with following 6 bits */ /* 10xxxxxx continuation byte with following 6 bits */
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | (codePoint & 0b111111)); mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | (codePoint & 0b111111));
} else if (codePoint <= /* 16 bits */0b1111111111111111) { } else if (codePoint <= /* 16 bits */0b1111111111111111) {
/* 1110xxxx leading byte with leading 4 bits */ /* 1110xxxx leading byte with leading 4 bits */
mUtf8InputBuffer[bufferPosition++] = (byte) (0b11100000 | (codePoint >> 12)); mUtf8InputBuffer[bufferPosition++] = (byte) (0b11100000 | (codePoint >> 12));
/* 10xxxxxx continuation byte with following 6 bits */ /* 10xxxxxx continuation byte with following 6 bits */
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | ((codePoint >> 6) & 0b111111)); mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | ((codePoint >> 6) & 0b111111));
/* 10xxxxxx continuation byte with following 6 bits */ /* 10xxxxxx continuation byte with following 6 bits */
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | (codePoint & 0b111111)); mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | (codePoint & 0b111111));
} else { /* We have checked codePoint <= 1114111 above, so we have max 21 bits = 0b111111111111111111111 */ } else { /* We have checked codePoint <= 1114111 above, so we have max 21 bits = 0b111111111111111111111 */
/* 11110xxx leading byte with leading 3 bits */ /* 11110xxx leading byte with leading 3 bits */
mUtf8InputBuffer[bufferPosition++] = (byte) (0b11110000 | (codePoint >> 18)); mUtf8InputBuffer[bufferPosition++] = (byte) (0b11110000 | (codePoint >> 18));
/* 10xxxxxx continuation byte with following 6 bits */ /* 10xxxxxx continuation byte with following 6 bits */
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | ((codePoint >> 12) & 0b111111)); mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | ((codePoint >> 12) & 0b111111));
/* 10xxxxxx continuation byte with following 6 bits */ /* 10xxxxxx continuation byte with following 6 bits */
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | ((codePoint >> 6) & 0b111111)); mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | ((codePoint >> 6) & 0b111111));
/* 10xxxxxx continuation byte with following 6 bits */ /* 10xxxxxx continuation byte with following 6 bits */
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | (codePoint & 0b111111)); mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | (codePoint & 0b111111));
} }
write(mUtf8InputBuffer, 0, bufferPosition); write(mUtf8InputBuffer, 0, bufferPosition);
} }
public TerminalEmulator getEmulator() { public TerminalEmulator getEmulator() {
return mEmulator; return mEmulator;
} }
/** Notify the {@link #mChangeCallback} that the screen has changed. */ /** Notify the {@link #mChangeCallback} that the screen has changed. */
protected void notifyScreenUpdate() { protected void notifyScreenUpdate() {
mChangeCallback.onTextChanged(this); mChangeCallback.onTextChanged(this);
} }
/** Reset state for terminal emulator state. */ /** Reset state for terminal emulator state. */
public void reset() { public void reset() {
mEmulator.reset(); mEmulator.reset();
notifyScreenUpdate(); notifyScreenUpdate();
} }
/** Finish this terminal session by sending SIGKILL to the shell. */ /** Finish this terminal session by sending SIGKILL to the shell. */
public void finishIfRunning() { public void finishIfRunning() {
@@ -295,48 +293,50 @@ public final class TerminalSession extends TerminalOutput {
} }
} }
/** Cleanup resources when the process exits. */ /** Cleanup resources when the process exits. */
void cleanupResources(int exitStatus) { void cleanupResources(int exitStatus) {
synchronized (this) { synchronized (this) {
mShellPid = -1; mShellPid = -1;
mShellExitStatus = exitStatus; mShellExitStatus = exitStatus;
} }
// Stop the reader and writer threads, and close the I/O streams // Stop the reader and writer threads, and close the I/O streams
mTerminalToProcessIOQueue.close(); mTerminalToProcessIOQueue.close();
mProcessToTerminalIOQueue.close(); mProcessToTerminalIOQueue.close();
JNI.close(mTerminalFileDescriptor); JNI.close(mTerminalFileDescriptor);
} }
@Override @Override
public void titleChanged(String oldTitle, String newTitle) { public void titleChanged(String oldTitle, String newTitle) {
mChangeCallback.onTitleChanged(this); mChangeCallback.onTitleChanged(this);
} }
public synchronized boolean isRunning() { public synchronized boolean isRunning() {
return mShellPid != -1; return mShellPid != -1;
} }
/** Only valid if not {@link #isRunning()}. */ /** Only valid if not {@link #isRunning()}. */
public synchronized int getExitStatus() { public synchronized int getExitStatus() {
return mShellExitStatus; return mShellExitStatus;
} }
@Override @Override
public void clipboardText(String text) { public void clipboardText(String text) {
mChangeCallback.onClipboardText(this, text); mChangeCallback.onClipboardText(this, text);
} }
@Override @Override
public void onBell() { public void onBell() {
mChangeCallback.onBell(this); mChangeCallback.onBell(this);
} }
@Override @Override
public void onColorsChanged() { public void onColorsChanged() {
mChangeCallback.onColorsChanged(this); mChangeCallback.onColorsChanged(this);
} }
public int getPid() { return mShellPid; } public int getPid() {
return mShellPid;
}
} }

View File

@@ -3,53 +3,53 @@ package com.termux.terminal;
/** /**
* Encodes effects, foreground and background colors into a 32 bit integer, which are stored for each cell in a terminal * Encodes effects, foreground and background colors into a 32 bit integer, which are stored for each cell in a terminal
* row in {@link TerminalRow#mStyle}. * row in {@link TerminalRow#mStyle}.
* * <p/>
* The foreground and background colors take 9 bits each, leaving (32-9-9)=14 bits for effect flags. Using 9 for now * The foreground and background colors take 9 bits each, leaving (32-9-9)=14 bits for effect flags. Using 9 for now
* (the different CHARACTER_ATTRIBUTE_* bits). * (the different CHARACTER_ATTRIBUTE_* bits).
*/ */
public final class TextStyle { public final class TextStyle {
public final static int CHARACTER_ATTRIBUTE_BOLD = 1; public final static int CHARACTER_ATTRIBUTE_BOLD = 1;
public final static int CHARACTER_ATTRIBUTE_ITALIC = 1 << 1; public final static int CHARACTER_ATTRIBUTE_ITALIC = 1 << 1;
public final static int CHARACTER_ATTRIBUTE_UNDERLINE = 1 << 2; public final static int CHARACTER_ATTRIBUTE_UNDERLINE = 1 << 2;
public final static int CHARACTER_ATTRIBUTE_BLINK = 1 << 3; public final static int CHARACTER_ATTRIBUTE_BLINK = 1 << 3;
public final static int CHARACTER_ATTRIBUTE_INVERSE = 1 << 4; public final static int CHARACTER_ATTRIBUTE_INVERSE = 1 << 4;
public final static int CHARACTER_ATTRIBUTE_INVISIBLE = 1 << 5; public final static int CHARACTER_ATTRIBUTE_INVISIBLE = 1 << 5;
public final static int CHARACTER_ATTRIBUTE_STRIKETHROUGH = 1 << 6; public final static int CHARACTER_ATTRIBUTE_STRIKETHROUGH = 1 << 6;
/** /**
* The selective erase control functions (DECSED and DECSEL) can only erase characters defined as erasable. * The selective erase control functions (DECSED and DECSEL) can only erase characters defined as erasable.
* * <p/>
* This bit is set if DECSCA (Select Character Protection Attribute) has been used to define the characters that * This bit is set if DECSCA (Select Character Protection Attribute) has been used to define the characters that
* come after it as erasable from the screen. * come after it as erasable from the screen.
*/ */
public final static int CHARACTER_ATTRIBUTE_PROTECTED = 1 << 7; public final static int CHARACTER_ATTRIBUTE_PROTECTED = 1 << 7;
/** Dim colors. Also known as faint or half intensity. */ /** Dim colors. Also known as faint or half intensity. */
public final static int CHARACTER_ATTRIBUTE_DIM = 1 << 8; public final static int CHARACTER_ATTRIBUTE_DIM = 1 << 8;
public final static int COLOR_INDEX_FOREGROUND = 256; public final static int COLOR_INDEX_FOREGROUND = 256;
public final static int COLOR_INDEX_BACKGROUND = 257; public final static int COLOR_INDEX_BACKGROUND = 257;
public final static int COLOR_INDEX_CURSOR = 258; public final static int COLOR_INDEX_CURSOR = 258;
/** The 256 standard color entries and the three special (foreground, background and cursor) ones. */ /** The 256 standard color entries and the three special (foreground, background and cursor) ones. */
public final static int NUM_INDEXED_COLORS = 259; public final static int NUM_INDEXED_COLORS = 259;
/** Normal foreground and background colors and no effects. */ /** Normal foreground and background colors and no effects. */
final static int NORMAL = encode(COLOR_INDEX_FOREGROUND, COLOR_INDEX_BACKGROUND, 0); final static int NORMAL = encode(COLOR_INDEX_FOREGROUND, COLOR_INDEX_BACKGROUND, 0);
static int encode(int foreColor, int backColor, int effect) { static int encode(int foreColor, int backColor, int effect) {
return ((effect & 0b111111111) << 18) | ((foreColor & 0b111111111) << 9) | (backColor & 0b111111111); return ((effect & 0b111111111) << 18) | ((foreColor & 0b111111111) << 9) | (backColor & 0b111111111);
} }
public static int decodeForeColor(int encodedColor) { public static int decodeForeColor(int encodedColor) {
return (encodedColor >> 9) & 0b111111111; return (encodedColor >> 9) & 0b111111111;
} }
public static int decodeBackColor(int encodedColor) { public static int decodeBackColor(int encodedColor) {
return encodedColor & 0b111111111; return encodedColor & 0b111111111;
} }
public static int decodeEffect(int encodedColor) { public static int decodeEffect(int encodedColor) {
return (encodedColor >> 18) & 0b111111111; return (encodedColor >> 18) & 0b111111111;
} }
} }

View File

@@ -2,107 +2,107 @@ package com.termux.terminal;
/** /**
* wcwidth() implementation from http://git.musl-libc.org/cgit/musl/tree/src/ctype * wcwidth() implementation from http://git.musl-libc.org/cgit/musl/tree/src/ctype
* * <p/>
* Modified to return 0 instead of -1. * Modified to return 0 instead of -1.
*/ */
public final class WcWidth { public final class WcWidth {
private static final short table[] = { 16, 16, 16, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 16, 16, 32, 16, 16, 16, 33, 34, 35, 36, 37, 38, private static final short table[] = {16, 16, 16, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 16, 16, 32, 16, 16, 16, 33, 34, 35, 36, 37, 38,
39, 16, 16, 40, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 41, 42, 16, 16, 43, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 39, 16, 16, 40, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 41, 42, 16, 16, 43, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 44, 16, 45, 46, 47, 48, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 44, 16, 45, 46, 47, 48, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
49, 16, 16, 50, 51, 16, 52, 16, 16, 16, 16, 16, 16, 16, 16, 53, 16, 16, 16, 16, 16, 54, 55, 16, 16, 16, 16, 56, 16, 16, 16, 16, 16, 16, 16, 16, 16, 49, 16, 16, 50, 51, 16, 52, 16, 16, 16, 16, 16, 16, 16, 16, 53, 16, 16, 16, 16, 16, 54, 55, 16, 16, 16, 16, 56, 16, 16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 57, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 57, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 58, 59, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 58, 59, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 16, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
248, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 254, 255, 255, 255, 255, 191, 182, 0, 0, 0, 248, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 254, 255, 255, 255, 255, 191, 182, 0, 0, 0,
0, 0, 0, 0, 31, 0, 255, 7, 0, 0, 0, 0, 0, 248, 255, 255, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 191, 159, 61, 0, 0, 0, 128, 2, 0, 0, 0, 0, 0, 0, 0, 31, 0, 255, 7, 0, 0, 0, 0, 0, 248, 255, 255, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 191, 159, 61, 0, 0, 0, 128, 2, 0, 0, 0,
255, 255, 255, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 255, 1, 0, 0, 0, 0, 0, 0, 248, 15, 0, 0, 0, 192, 251, 239, 62, 0, 0, 0, 0, 0, 14, 0, 0, 0, 0, 255, 255, 255, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 255, 1, 0, 0, 0, 0, 0, 0, 248, 15, 0, 0, 0, 192, 251, 239, 62, 0, 0, 0, 0, 0, 14, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 240, 255, 255, 127, 7, 0, 0, 0, 0, 0, 0, 20, 254, 33, 254, 0, 12, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 16, 30, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 240, 255, 255, 127, 7, 0, 0, 0, 0, 0, 0, 20, 254, 33, 254, 0, 12, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 16, 30, 32, 0,
0, 12, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 16, 134, 57, 2, 0, 0, 0, 35, 0, 6, 0, 0, 0, 0, 0, 0, 16, 190, 33, 0, 0, 12, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 144, 0, 12, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 16, 134, 57, 2, 0, 0, 0, 35, 0, 6, 0, 0, 0, 0, 0, 0, 16, 190, 33, 0, 0, 12, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 144,
30, 32, 64, 0, 12, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 1, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 193, 61, 96, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 30, 32, 64, 0, 12, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 1, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 193, 61, 96, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 144, 64, 48, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 30, 32, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 92, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 144, 64, 48, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 30, 32, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 92, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 242, 7, 128, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 242, 27, 0, 63, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 160, 2, 0, 0, 0, 0, 0, 0, 254, 0, 0, 0, 242, 7, 128, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 242, 27, 0, 63, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 160, 2, 0, 0, 0, 0, 0, 0, 254,
127, 223, 224, 255, 254, 255, 255, 255, 31, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 253, 102, 0, 0, 0, 195, 1, 0, 30, 0, 100, 32, 0, 32, 0, 0, 127, 223, 224, 255, 254, 255, 255, 255, 31, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 253, 102, 0, 0, 0, 195, 1, 0, 30, 0, 100, 32, 0, 32, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0,
28, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 176, 63, 64, 254, 15, 32, 0, 0, 0, 0, 0, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 176, 63, 64, 254, 15, 32, 0, 0, 0, 0, 0, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 135, 1, 4, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 135, 1, 4, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
128, 1, 0, 0, 0, 0, 0, 0, 64, 127, 229, 31, 248, 159, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 208, 23, 4, 0, 0, 0, 0, 128, 1, 0, 0, 0, 0, 0, 0, 64, 127, 229, 31, 248, 159, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 208, 23, 4, 0, 0, 0, 0,
248, 15, 0, 3, 0, 0, 0, 60, 11, 0, 0, 0, 0, 0, 0, 64, 163, 3, 0, 0, 0, 0, 0, 0, 240, 207, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 248, 15, 0, 3, 0, 0, 0, 60, 11, 0, 0, 0, 0, 0, 0, 64, 163, 3, 0, 0, 0, 0, 0, 0, 240, 207, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
247, 255, 253, 33, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 127, 0, 0, 240, 0, 248, 0, 0, 247, 255, 253, 33, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 127, 0, 0, 240, 0, 248, 0, 0,
0, 124, 0, 0, 0, 0, 0, 0, 31, 252, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 124, 0, 0, 0, 0, 0, 0, 31, 252, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255,
255, 0, 0, 0, 0, 0, 60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 255, 0, 0, 0, 0, 0, 60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128,
247, 63, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 68, 8, 0, 0, 96, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 247, 63, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 68, 8, 0, 0, 96, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0,
255, 255, 3, 0, 0, 0, 0, 0, 192, 63, 0, 0, 128, 255, 3, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 200, 19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 126, 102, 255, 255, 3, 0, 0, 0, 0, 0, 192, 63, 0, 0, 128, 255, 3, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 200, 19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 126, 102,
0, 8, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 157, 193, 2, 0, 0, 0, 0, 48, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 157, 193, 2, 0, 0, 0, 0, 48, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 32, 33, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 32, 33, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0,
127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 110, 240, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 110, 240, 0,
0, 0, 0, 0, 135, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 255, 127, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 135, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 255, 127, 0, 0, 0, 0, 0, 0, 0, 3, 0,
0, 0, 0, 0, 120, 38, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 128, 239, 31, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 192, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 120, 38, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 128, 239, 31, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 192, 127, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 40, 191, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 40, 191, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 128, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 3, 248, 255, 231, 15, 0, 0, 0, 60, 0, 0, 0, 0, 0, 0, 0, 0, 128, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 3, 248, 255, 231, 15, 0, 0, 0, 60, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }; 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,};
private static final short wtable[] = { 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 18, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, private static final short wtable[] = {16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 18, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 19, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 20, 21, 22, 23, 24, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 16, 16, 16, 16, 16, 16, 19, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 20, 21, 22, 23, 24, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,
17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 25, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 25, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,
17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,
17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 26, 16, 16, 16, 16, 27, 16, 16, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 26, 16, 16, 16, 16, 27, 16, 16, 17, 17, 17, 17, 17,
17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,
17, 28, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 17, 17, 17, 28, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 17, 17,
16, 16, 16, 29, 30, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 29, 30, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 31, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 31, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 32, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 32, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 16, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 248, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 252, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 248, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 252, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 251, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 251, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 15, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 15, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 63, 0, 0, 0, 255, 15, 255, 255, 255, 255, 255, 255, 255, 127, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127, 254, 255, 255, 255, 255, 255, 63, 0, 0, 0, 255, 15, 255, 255, 255, 255, 255, 255, 255, 127, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127, 254, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 224, 255, 255, 255, 255, 63, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 224, 255, 255, 255, 255, 63, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127, 255, 255,
255, 255, 255, 7, 255, 255, 255, 255, 15, 0, 255, 255, 255, 255, 255, 127, 255, 255, 255, 255, 255, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 7, 255, 255, 255, 255, 15, 0, 255, 255, 255, 255, 255, 127, 255, 255, 255, 255, 255, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 31, 255, 255, 255, 255, 255, 255, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 31, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 31, 255, 255, 255, 255, 255, 255, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 31, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 15, 0, 255, 255, 127, 248, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 15, 0, 255, 255, 127, 248,
255, 255, 255, 255, 255, 15, 0, 0, 255, 3, 0, 0, 255, 255, 255, 255, 247, 255, 127, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 254, 255, 255, 255, 255, 255, 15, 0, 0, 255, 3, 0, 0, 255, 255, 255, 255, 247, 255, 127, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 254,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 127, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 127, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 255, 255, 255, 255, 255, 7, 255, 1, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 255, 255, 255, 255, 255, 7, 255, 1, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0 }; 0, 0, 0, 0, 0, 0, 0, 0, 0};
/** Return the terminal display width of a code point: 0, 1 or 2. */ /** Return the terminal display width of a code point: 0, 1 or 2. */
public static int width(int wc) { public static int width(int wc) {
if (wc < 0xff) return (wc + 1 & 0x7f) >= 0x21 ? 1 : (wc != 0) ? 0 : 0; if (wc < 0xff) return (wc + 1 & 0x7f) >= 0x21 ? 1 : (wc != 0) ? 0 : 0;
if ((wc & 0xfffeffff) < 0xfffe) { if ((wc & 0xfffeffff) < 0xfffe) {
if (((table[table[wc >> 8] * 32 + ((wc & 255) >> 3)] >> (wc & 7)) & 1) != 0) return 0; if (((table[table[wc >> 8] * 32 + ((wc & 255) >> 3)] >> (wc & 7)) & 1) != 0) return 0;
if (((wtable[wtable[wc >> 8] * 32 + ((wc & 255) >> 3)] >> (wc & 7)) & 1) != 0) return 2; if (((wtable[wtable[wc >> 8] * 32 + ((wc & 255) >> 3)] >> (wc & 7)) & 1) != 0) return 2;
return 1; return 1;
} }
if ((wc & 0xfffe) == 0xfffe) return 0; if ((wc & 0xfffe) == 0xfffe) return 0;
if (wc - 0x20000 < 0x20000) return 2; if (wc - 0x20000 < 0x20000) return 2;
if (wc == 0xe0001 || wc - 0xe0020 < 0x5f || wc - 0xe0100 < 0xef) return 0; if (wc == 0xe0001 || wc - 0xe0020 < 0x5f || wc - 0xe0100 < 0xef) return 0;
return 1; return 1;
} }
/** The width at an index position in a java char array. */ /** The width at an index position in a java char array. */
public static int width(char[] chars, int index) { public static int width(char[] chars, int index) {
char c = chars[index]; char c = chars[index];
return Character.isHighSurrogate(c) ? width(Character.toCodePoint(c, chars[index + 1])) : width(c); return Character.isHighSurrogate(c) ? width(Character.toCodePoint(c, chars[index + 1])) : width(c);
} }
} }

View File

@@ -8,104 +8,104 @@ import android.view.ScaleGestureDetector;
/** A combination of {@link GestureDetector} and {@link ScaleGestureDetector}. */ /** A combination of {@link GestureDetector} and {@link ScaleGestureDetector}. */
public final class GestureAndScaleRecognizer { public final class GestureAndScaleRecognizer {
public interface Listener { public interface Listener {
boolean onSingleTapUp(MotionEvent e); boolean onSingleTapUp(MotionEvent e);
boolean onDoubleTap(MotionEvent e); boolean onDoubleTap(MotionEvent e);
boolean onScroll(MotionEvent e2, float dx, float dy); boolean onScroll(MotionEvent e2, float dx, float dy);
boolean onFling(MotionEvent e, float velocityX, float velocityY); boolean onFling(MotionEvent e, float velocityX, float velocityY);
boolean onScale(float focusX, float focusY, float scale); boolean onScale(float focusX, float focusY, float scale);
boolean onDown(float x, float y); boolean onDown(float x, float y);
boolean onUp(MotionEvent e); boolean onUp(MotionEvent e);
void onLongPress(MotionEvent e); void onLongPress(MotionEvent e);
} }
private final GestureDetector mGestureDetector; private final GestureDetector mGestureDetector;
private final ScaleGestureDetector mScaleDetector; private final ScaleGestureDetector mScaleDetector;
final Listener mListener; final Listener mListener;
boolean isAfterLongPress; boolean isAfterLongPress;
public GestureAndScaleRecognizer(Context context, Listener listener) { public GestureAndScaleRecognizer(Context context, Listener listener) {
mListener = listener; mListener = listener;
mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() { mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
@Override @Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float dx, float dy) { public boolean onScroll(MotionEvent e1, MotionEvent e2, float dx, float dy) {
return mListener.onScroll(e2, dx, dy); return mListener.onScroll(e2, dx, dy);
} }
@Override @Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return mListener.onFling(e2, velocityX, velocityY); return mListener.onFling(e2, velocityX, velocityY);
} }
@Override @Override
public boolean onDown(MotionEvent e) { public boolean onDown(MotionEvent e) {
return mListener.onDown(e.getX(), e.getY()); return mListener.onDown(e.getX(), e.getY());
} }
@Override @Override
public void onLongPress(MotionEvent e) { public void onLongPress(MotionEvent e) {
mListener.onLongPress(e); mListener.onLongPress(e);
isAfterLongPress = true; isAfterLongPress = true;
} }
}, null, true /* ignoreMultitouch */); }, null, true /* ignoreMultitouch */);
mGestureDetector.setOnDoubleTapListener(new GestureDetector.OnDoubleTapListener() { mGestureDetector.setOnDoubleTapListener(new GestureDetector.OnDoubleTapListener() {
@Override @Override
public boolean onSingleTapConfirmed(MotionEvent e) { public boolean onSingleTapConfirmed(MotionEvent e) {
return mListener.onSingleTapUp(e); return mListener.onSingleTapUp(e);
} }
@Override @Override
public boolean onDoubleTap(MotionEvent e) { public boolean onDoubleTap(MotionEvent e) {
return mListener.onDoubleTap(e); return mListener.onDoubleTap(e);
} }
@Override @Override
public boolean onDoubleTapEvent(MotionEvent e) { public boolean onDoubleTapEvent(MotionEvent e) {
return true; return true;
} }
}); });
mScaleDetector = new ScaleGestureDetector(context, new ScaleGestureDetector.SimpleOnScaleGestureListener() { mScaleDetector = new ScaleGestureDetector(context, new ScaleGestureDetector.SimpleOnScaleGestureListener() {
@Override @Override
public boolean onScaleBegin(ScaleGestureDetector detector) { public boolean onScaleBegin(ScaleGestureDetector detector) {
return true; return true;
} }
@Override @Override
public boolean onScale(ScaleGestureDetector detector) { public boolean onScale(ScaleGestureDetector detector) {
return mListener.onScale(detector.getFocusX(), detector.getFocusY(), detector.getScaleFactor()); return mListener.onScale(detector.getFocusX(), detector.getFocusY(), detector.getScaleFactor());
} }
}); });
} }
public void onTouchEvent(MotionEvent event) { public void onTouchEvent(MotionEvent event) {
mGestureDetector.onTouchEvent(event); mGestureDetector.onTouchEvent(event);
mScaleDetector.onTouchEvent(event); mScaleDetector.onTouchEvent(event);
switch (event.getAction()) { switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_DOWN:
isAfterLongPress = false; isAfterLongPress = false;
break; break;
case MotionEvent.ACTION_UP: case MotionEvent.ACTION_UP:
if (!isAfterLongPress) { if (!isAfterLongPress) {
// This behaviour is desired when in e.g. vim with mouse events, where we do not // This behaviour is desired when in e.g. vim with mouse events, where we do not
// want to move the cursor when lifting finger after a long press. // want to move the cursor when lifting finger after a long press.
mListener.onUp(event); mListener.onUp(event);
} }
break; break;
} }
} }
public boolean isInProgress() { public boolean isInProgress() {
return mScaleDetector.isInProgress(); return mScaleDetector.isInProgress();
} }
} }

View File

@@ -9,20 +9,20 @@ import com.termux.terminal.TerminalSession;
/** /**
* Input and scale listener which may be set on a {@link TerminalView} through * Input and scale listener which may be set on a {@link TerminalView} through
* {@link TerminalView#setOnKeyListener(TerminalKeyListener)}. * {@link TerminalView#setOnKeyListener(TerminalKeyListener)}.
* * <p/>
* TODO: Rename to TerminalViewClient. * TODO: Rename to TerminalViewClient.
*/ */
public interface TerminalKeyListener { public interface TerminalKeyListener {
/** Callback function on scale events according to {@link ScaleGestureDetector#getScaleFactor()}. */ /** Callback function on scale events according to {@link ScaleGestureDetector#getScaleFactor()}. */
float onScale(float scale); float onScale(float scale);
/** On a single tap on the terminal if terminal mouse reporting not enabled. */ /** On a single tap on the terminal if terminal mouse reporting not enabled. */
void onSingleTapUp(MotionEvent e); void onSingleTapUp(MotionEvent e);
boolean shouldBackButtonBeMappedToEscape(); boolean shouldBackButtonBeMappedToEscape();
void copyModeChanged(boolean copyMode); void copyModeChanged(boolean copyMode);
boolean onKeyDown(int keyCode, KeyEvent e, TerminalSession session); boolean onKeyDown(int keyCode, KeyEvent e, TerminalSession session);

View File

@@ -13,223 +13,213 @@ import com.termux.terminal.WcWidth;
/** /**
* Renderer of a {@link TerminalEmulator} into a {@link Canvas}. * Renderer of a {@link TerminalEmulator} into a {@link Canvas}.
* * <p/>
* Saves font metrics, so needs to be recreated each time the typeface or font size changes. * Saves font metrics, so needs to be recreated each time the typeface or font size changes.
*/ */
final class TerminalRenderer { final class TerminalRenderer {
final int mTextSize; final int mTextSize;
final Typeface mTypeface; final Typeface mTypeface;
private final Paint mTextPaint = new Paint(); private final Paint mTextPaint = new Paint();
/** The width of a single mono spaced character obtained by {@link Paint#measureText(String)} on a single 'X'. */ /** The width of a single mono spaced character obtained by {@link Paint#measureText(String)} on a single 'X'. */
final float mFontWidth; final float mFontWidth;
/** The {@link Paint#getFontSpacing()}. See http://www.fampennings.nl/maarten/android/08numgrid/font.png */ /** The {@link Paint#getFontSpacing()}. See http://www.fampennings.nl/maarten/android/08numgrid/font.png */
final int mFontLineSpacing; final int mFontLineSpacing;
/** The {@link Paint#ascent()}. See http://www.fampennings.nl/maarten/android/08numgrid/font.png */ /** The {@link Paint#ascent()}. See http://www.fampennings.nl/maarten/android/08numgrid/font.png */
private final int mFontAscent; private final int mFontAscent;
/** The {@link #mFontLineSpacing} + {@link #mFontAscent}. */ /** The {@link #mFontLineSpacing} + {@link #mFontAscent}. */
final int mFontLineSpacingAndAscent; final int mFontLineSpacingAndAscent;
private final float[] asciiMeasures = new float[127]; private final float[] asciiMeasures = new float[127];
public TerminalRenderer(int textSize, Typeface typeface) { public TerminalRenderer(int textSize, Typeface typeface) {
mTextSize = textSize; mTextSize = textSize;
mTypeface = typeface; mTypeface = typeface;
mTextPaint.setTypeface(typeface); mTextPaint.setTypeface(typeface);
mTextPaint.setAntiAlias(true); mTextPaint.setAntiAlias(true);
mTextPaint.setTextSize(textSize); mTextPaint.setTextSize(textSize);
mFontLineSpacing = (int) Math.ceil(mTextPaint.getFontSpacing()); mFontLineSpacing = (int) Math.ceil(mTextPaint.getFontSpacing());
mFontAscent = (int) Math.ceil(mTextPaint.ascent()); mFontAscent = (int) Math.ceil(mTextPaint.ascent());
mFontLineSpacingAndAscent = mFontLineSpacing + mFontAscent; mFontLineSpacingAndAscent = mFontLineSpacing + mFontAscent;
mFontWidth = mTextPaint.measureText("X"); mFontWidth = mTextPaint.measureText("X");
StringBuilder sb = new StringBuilder(" "); StringBuilder sb = new StringBuilder(" ");
for (int i = 0; i < asciiMeasures.length; i++) { for (int i = 0; i < asciiMeasures.length; i++) {
sb.setCharAt(0, (char) i); sb.setCharAt(0, (char) i);
asciiMeasures[i] = mTextPaint.measureText(sb, 0, 1); asciiMeasures[i] = mTextPaint.measureText(sb, 0, 1);
} }
} }
/** Render the terminal to a canvas with at a specified row scroll, and an optional rectangular selection. */ /** Render the terminal to a canvas with at a specified row scroll, and an optional rectangular selection. */
public final void render(TerminalEmulator mEmulator, Canvas canvas, int topRow, int selectionY1, int selectionY2, int selectionX1, int selectionX2) { public final void render(TerminalEmulator mEmulator, Canvas canvas, int topRow, int selectionY1, int selectionY2, int selectionX1, int selectionX2) {
final boolean reverseVideo = mEmulator.isReverseVideo(); final boolean reverseVideo = mEmulator.isReverseVideo();
final int endRow = topRow + mEmulator.mRows; final int endRow = topRow + mEmulator.mRows;
final int columns = mEmulator.mColumns; final int columns = mEmulator.mColumns;
final int cursorCol = mEmulator.getCursorCol(); final int cursorCol = mEmulator.getCursorCol();
final int cursorRow = mEmulator.getCursorRow(); final int cursorRow = mEmulator.getCursorRow();
final boolean cursorVisible = mEmulator.isShowingCursor(); final boolean cursorVisible = mEmulator.isShowingCursor();
final TerminalBuffer screen = mEmulator.getScreen(); final TerminalBuffer screen = mEmulator.getScreen();
final int[] palette = mEmulator.mColors.mCurrentColors; final int[] palette = mEmulator.mColors.mCurrentColors;
if (reverseVideo) canvas.drawColor(palette[TextStyle.COLOR_INDEX_FOREGROUND], PorterDuff.Mode.SRC); if (reverseVideo)
canvas.drawColor(palette[TextStyle.COLOR_INDEX_FOREGROUND], PorterDuff.Mode.SRC);
float heightOffset = mFontLineSpacingAndAscent; float heightOffset = mFontLineSpacingAndAscent;
for (int row = topRow; row < endRow; row++) { for (int row = topRow; row < endRow; row++) {
heightOffset += mFontLineSpacing; heightOffset += mFontLineSpacing;
final int cursorX = (row == cursorRow && cursorVisible) ? cursorCol : -1; final int cursorX = (row == cursorRow && cursorVisible) ? cursorCol : -1;
int selx1 = -1, selx2 = -1; int selx1 = -1, selx2 = -1;
if (row >= selectionY1 && row <= selectionY2) { if (row >= selectionY1 && row <= selectionY2) {
if (row == selectionY1) selx1 = selectionX1; if (row == selectionY1) selx1 = selectionX1;
selx2 = (row == selectionY2) ? selectionX2 : mEmulator.mColumns; selx2 = (row == selectionY2) ? selectionX2 : mEmulator.mColumns;
} }
TerminalRow lineObject = screen.allocateFullLineIfNecessary(screen.externalToInternalRow(row)); TerminalRow lineObject = screen.allocateFullLineIfNecessary(screen.externalToInternalRow(row));
final char[] line = lineObject.mText; final char[] line = lineObject.mText;
final int charsUsedInLine = lineObject.getSpaceUsed(); final int charsUsedInLine = lineObject.getSpaceUsed();
int lastRunStyle = 0; int lastRunStyle = 0;
boolean lastRunInsideCursor = false; boolean lastRunInsideCursor = false;
int lastRunStartColumn = -1; int lastRunStartColumn = -1;
int lastRunStartIndex = 0; int lastRunStartIndex = 0;
boolean lastRunFontWidthMismatch = false; boolean lastRunFontWidthMismatch = false;
int currentCharIndex = 0; int currentCharIndex = 0;
float measuredWidthForRun = 0.f; float measuredWidthForRun = 0.f;
for (int column = 0; column < columns;) { for (int column = 0; column < columns; ) {
final char charAtIndex = line[currentCharIndex]; final char charAtIndex = line[currentCharIndex];
final boolean charIsHighsurrogate = Character.isHighSurrogate(charAtIndex); final boolean charIsHighsurrogate = Character.isHighSurrogate(charAtIndex);
final int charsForCodePoint = charIsHighsurrogate ? 2 : 1; final int charsForCodePoint = charIsHighsurrogate ? 2 : 1;
final int codePoint = charIsHighsurrogate ? Character.toCodePoint(charAtIndex, line[currentCharIndex + 1]) : charAtIndex; final int codePoint = charIsHighsurrogate ? Character.toCodePoint(charAtIndex, line[currentCharIndex + 1]) : charAtIndex;
final int codePointWcWidth = WcWidth.width(codePoint); final int codePointWcWidth = WcWidth.width(codePoint);
final boolean insideCursor = (column >= selx1 && column <= selx2) || (cursorX == column || (codePointWcWidth == 2 && cursorX == column + 1)); final boolean insideCursor = (column >= selx1 && column <= selx2) || (cursorX == column || (codePointWcWidth == 2 && cursorX == column + 1));
final int style = lineObject.getStyle(column); final int style = lineObject.getStyle(column);
// Check if the measured text width for this code point is not the same as that expected by wcwidth(). // Check if the measured text width for this code point is not the same as that expected by wcwidth().
// This could happen for some fonts which are not truly monospace, or for more exotic characters such as // This could happen for some fonts which are not truly monospace, or for more exotic characters such as
// smileys which android font renders as wide. // smileys which android font renders as wide.
// If this is detected, we draw this code point scaled to match what wcwidth() expects. // If this is detected, we draw this code point scaled to match what wcwidth() expects.
final float measuredCodePointWidth = (codePoint < asciiMeasures.length) ? asciiMeasures[codePoint] : mTextPaint.measureText(line, final float measuredCodePointWidth = (codePoint < asciiMeasures.length) ? asciiMeasures[codePoint] : mTextPaint.measureText(line,
currentCharIndex, charsForCodePoint); currentCharIndex, charsForCodePoint);
final boolean fontWidthMismatch = Math.abs(measuredCodePointWidth / mFontWidth - codePointWcWidth) > 0.01; final boolean fontWidthMismatch = Math.abs(measuredCodePointWidth / mFontWidth - codePointWcWidth) > 0.01;
if (style != lastRunStyle || insideCursor != lastRunInsideCursor || fontWidthMismatch || lastRunFontWidthMismatch) { if (style != lastRunStyle || insideCursor != lastRunInsideCursor || fontWidthMismatch || lastRunFontWidthMismatch) {
if (column == 0) { if (column == 0) {
// Skip first column as there is nothing to draw, just record the current style. // Skip first column as there is nothing to draw, just record the current style.
} else { } else {
final int columnWidthSinceLastRun = column - lastRunStartColumn; final int columnWidthSinceLastRun = column - lastRunStartColumn;
final int charsSinceLastRun = currentCharIndex - lastRunStartIndex; final int charsSinceLastRun = currentCharIndex - lastRunStartIndex;
drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun, lastRunStartIndex, charsSinceLastRun, drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun, lastRunStartIndex, charsSinceLastRun,
measuredWidthForRun, lastRunInsideCursor, lastRunStyle, reverseVideo); measuredWidthForRun, lastRunInsideCursor, lastRunStyle, reverseVideo);
} }
measuredWidthForRun = 0.f; measuredWidthForRun = 0.f;
lastRunStyle = style; lastRunStyle = style;
lastRunInsideCursor = insideCursor; lastRunInsideCursor = insideCursor;
lastRunStartColumn = column; lastRunStartColumn = column;
lastRunStartIndex = currentCharIndex; lastRunStartIndex = currentCharIndex;
lastRunFontWidthMismatch = fontWidthMismatch; lastRunFontWidthMismatch = fontWidthMismatch;
} }
measuredWidthForRun += measuredCodePointWidth; measuredWidthForRun += measuredCodePointWidth;
column += codePointWcWidth; column += codePointWcWidth;
currentCharIndex += charsForCodePoint; currentCharIndex += charsForCodePoint;
while (currentCharIndex < charsUsedInLine && WcWidth.width(line, currentCharIndex) <= 0) { while (currentCharIndex < charsUsedInLine && WcWidth.width(line, currentCharIndex) <= 0) {
// Eat combining chars so that they are treated as part of the last non-combining code point, // Eat combining chars so that they are treated as part of the last non-combining code point,
// instead of e.g. being considered inside the cursor in the next run. // instead of e.g. being considered inside the cursor in the next run.
currentCharIndex += Character.isHighSurrogate(line[currentCharIndex]) ? 2 : 1; currentCharIndex += Character.isHighSurrogate(line[currentCharIndex]) ? 2 : 1;
} }
} }
final int columnWidthSinceLastRun = columns - lastRunStartColumn; final int columnWidthSinceLastRun = columns - lastRunStartColumn;
final int charsSinceLastRun = currentCharIndex - lastRunStartIndex; final int charsSinceLastRun = currentCharIndex - lastRunStartIndex;
drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun, lastRunStartIndex, charsSinceLastRun, drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun, lastRunStartIndex, charsSinceLastRun,
measuredWidthForRun, lastRunInsideCursor, lastRunStyle, reverseVideo); measuredWidthForRun, lastRunInsideCursor, lastRunStyle, reverseVideo);
} }
} }
/** /**
* @param canvas * @param canvas the canvas to render on
* the canvas to render on * @param palette the color palette to look up colors from textStyle
* @param palette * @param y height offset into the canvas where to render the line: line * {@link #mFontLineSpacing}
* the color palette to look up colors from textStyle * @param startColumn the run offset in columns
* @param y * @param runWidthColumns the run width in columns - this is computed from wcwidth() and may not be what the font measures to
* height offset into the canvas where to render the line: line * {@link #mFontLineSpacing} * @param text the java char array to render text from
* @param startColumn * @param startCharIndex index into the text array where to start
* the run offset in columns * @param runWidthChars number of java characters from the text array to render
* @param runWidthColumns * @param cursor true if rendering a cursor or selection
* the run width in columns - this is computed from wcwidth() and may not be what the font measures to * @param textStyle the background, foreground and effect encoded using {@link TextStyle}
* @param text * @param reverseVideo if the screen is rendered with the global reverse video flag set
* the java char array to render text from */
* @param startCharIndex private void drawTextRun(Canvas canvas, char[] text, int[] palette, float y, int startColumn, int runWidthColumns, int startCharIndex, int runWidthChars,
* index into the text array where to start float mes, boolean cursor, int textStyle, boolean reverseVideo) {
* @param runWidthChars int foreColor = TextStyle.decodeForeColor(textStyle);
* number of java characters from the text array to render int backColor = TextStyle.decodeBackColor(textStyle);
* @param cursor final int effect = TextStyle.decodeEffect(textStyle);
* true if rendering a cursor or selection float left = startColumn * mFontWidth;
* @param textStyle float right = left + runWidthColumns * mFontWidth;
* the background, foreground and effect encoded using {@link TextStyle}
* @param reverseVideo
* if the screen is rendered with the global reverse video flag set
*/
private void drawTextRun(Canvas canvas, char[] text, int[] palette, float y, int startColumn, int runWidthColumns, int startCharIndex, int runWidthChars,
float mes, boolean cursor, int textStyle, boolean reverseVideo) {
int foreColor = TextStyle.decodeForeColor(textStyle);
int backColor = TextStyle.decodeBackColor(textStyle);
final int effect = TextStyle.decodeEffect(textStyle);
float left = startColumn * mFontWidth;
float right = left + runWidthColumns * mFontWidth;
mes = mes / mFontWidth; mes = mes / mFontWidth;
boolean savedMatrix = false; boolean savedMatrix = false;
if (Math.abs(mes - runWidthColumns) > 0.01) { if (Math.abs(mes - runWidthColumns) > 0.01) {
canvas.save(); canvas.save();
canvas.scale(runWidthColumns / mes, 1.f); canvas.scale(runWidthColumns / mes, 1.f);
left *= mes / runWidthColumns; left *= mes / runWidthColumns;
right *= mes / runWidthColumns; right *= mes / runWidthColumns;
savedMatrix = true; savedMatrix = true;
} }
// Reverse video here if _one and only one_ of the reverse flags are set: // Reverse video here if _one and only one_ of the reverse flags are set:
boolean reverseVideoHere = reverseVideo ^ (effect & (TextStyle.CHARACTER_ATTRIBUTE_INVERSE)) != 0; boolean reverseVideoHere = reverseVideo ^ (effect & (TextStyle.CHARACTER_ATTRIBUTE_INVERSE)) != 0;
// Switch if _one and only one_ of reverse video and cursor is set: // Switch if _one and only one_ of reverse video and cursor is set:
if (reverseVideoHere ^ cursor) { if (reverseVideoHere ^ cursor) {
int tmp = foreColor; int tmp = foreColor;
foreColor = backColor; foreColor = backColor;
backColor = tmp; backColor = tmp;
} }
if (backColor != TextStyle.COLOR_INDEX_BACKGROUND) { if (backColor != TextStyle.COLOR_INDEX_BACKGROUND) {
// Only draw non-default background. // Only draw non-default background.
mTextPaint.setColor(palette[backColor]); mTextPaint.setColor(palette[backColor]);
canvas.drawRect(left, y - mFontLineSpacingAndAscent + mFontAscent, right, y, mTextPaint); canvas.drawRect(left, y - mFontLineSpacingAndAscent + mFontAscent, right, y, mTextPaint);
} }
if ((effect & TextStyle.CHARACTER_ATTRIBUTE_INVISIBLE) == 0) { if ((effect & TextStyle.CHARACTER_ATTRIBUTE_INVISIBLE) == 0) {
// Treat blink as bold: // Treat blink as bold:
final boolean bold = (effect & (TextStyle.CHARACTER_ATTRIBUTE_BOLD | TextStyle.CHARACTER_ATTRIBUTE_BLINK)) != 0; final boolean bold = (effect & (TextStyle.CHARACTER_ATTRIBUTE_BOLD | TextStyle.CHARACTER_ATTRIBUTE_BLINK)) != 0;
final boolean underline = (effect & TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE) != 0; final boolean underline = (effect & TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE) != 0;
final boolean italic = (effect & TextStyle.CHARACTER_ATTRIBUTE_ITALIC) != 0; final boolean italic = (effect & TextStyle.CHARACTER_ATTRIBUTE_ITALIC) != 0;
final boolean strikeThrough = (effect & TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH) != 0; final boolean strikeThrough = (effect & TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH) != 0;
final boolean dim = (effect & TextStyle.CHARACTER_ATTRIBUTE_DIM) != 0; final boolean dim = (effect & TextStyle.CHARACTER_ATTRIBUTE_DIM) != 0;
// Let bold have bright colors if applicable (one of the first 8): // Let bold have bright colors if applicable (one of the first 8):
final int actualForeColor = foreColor + (bold && foreColor < 8 ? 8 : 0); final int actualForeColor = foreColor + (bold && foreColor < 8 ? 8 : 0);
int foreColorARGB = palette[actualForeColor]; int foreColorARGB = palette[actualForeColor];
if (dim) { if (dim) {
int red = (0xFF & (foreColorARGB >> 16)); int red = (0xFF & (foreColorARGB >> 16));
int green = (0xFF & (foreColorARGB >> 8)); int green = (0xFF & (foreColorARGB >> 8));
int blue = (0xFF & foreColorARGB); int blue = (0xFF & foreColorARGB);
// Dim color handling used by libvte which in turn took it from xterm // Dim color handling used by libvte which in turn took it from xterm
// (https://bug735245.bugzilla-attachments.gnome.org/attachment.cgi?id=284267): // (https://bug735245.bugzilla-attachments.gnome.org/attachment.cgi?id=284267):
red = red * 2 / 3; red = red * 2 / 3;
green = green * 2 / 3; green = green * 2 / 3;
blue = blue * 2 / 3; blue = blue * 2 / 3;
foreColorARGB = 0xFF000000 + (red << 16) + (green << 8) + blue; foreColorARGB = 0xFF000000 + (red << 16) + (green << 8) + blue;
} }
mTextPaint.setFakeBoldText(bold); mTextPaint.setFakeBoldText(bold);
mTextPaint.setUnderlineText(underline); mTextPaint.setUnderlineText(underline);
mTextPaint.setTextSkewX(italic ? -0.35f : 0.f); mTextPaint.setTextSkewX(italic ? -0.35f : 0.f);
mTextPaint.setStrikeThruText(strikeThrough); mTextPaint.setStrikeThruText(strikeThrough);
mTextPaint.setColor(foreColorARGB); mTextPaint.setColor(foreColorARGB);
// The text alignment is the default Paint.Align.LEFT. // The text alignment is the default Paint.Align.LEFT.
canvas.drawText(text, startCharIndex, runWidthChars, left, y - mFontLineSpacingAndAscent, mTextPaint); canvas.drawText(text, startCharIndex, runWidthChars, left, y - mFontLineSpacingAndAscent, mTextPaint);
} }
if (savedMatrix) canvas.restore(); if (savedMatrix) canvas.restore();
} }
} }

File diff suppressed because it is too large Load Diff