Initial commit

This commit is contained in:
Fredrik Fornwall
2015-10-25 15:27:32 +01:00
commit a18ee58f7a
87 changed files with 13851 additions and 0 deletions

41
.gitignore vendored Normal file
View File

@@ -0,0 +1,41 @@
# From https://gist.github.com/iainconnor/8605514
# with the addition of the /captures below.
/captures
# Built application files
/*/build/
# Crashlytics configuations
com_crashlytics_export_strings.xml
# Local configuration file (sdk path, etc)
local.properties
# Gradle generated files
.gradle/
# Signing files
.signing/
# User-specific configurations
.idea/libraries/
.idea/workspace.xml
.idea/tasks.xml
.idea/.name
.idea/compiler.xml
.idea/copyright/profiles_settings.xml
.idea/encodings.xml
.idea/misc.xml
.idea/modules.xml
.idea/scopes/scope_settings.xml
.idea/vcs.xml
*.iml
# OS-specific files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

18
.idea/gradle.xml generated Normal file
View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="1.8" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

12
.idea/runConfigurations.xml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
</set>
</option>
</component>
</project>

34
README.md Normal file
View File

@@ -0,0 +1,34 @@
Termux app
==========
Termux is an Android terminal app and Linux environment.
* [Termux on Google Play](http://play.google.com/store/apps/details?id=com.termux)
* [termux.com](http://termux.com)
* [Termux Help](http://termux.com/help/)
* [Termux app on GitHub](https://github.com/termux/termux-app)
* [Termux packages on GitHub](https://github.com/termux/termux-packages)
* [Termux Google+ community](http://termux.com/community/)
License
=======
Released under the GPLv3 license. Contains code from `Terminal Emulator for Android` which is released under the Apache License.
Building JNI libraries
======================
For ease of use, the JNI libraries are checked into version control. Execute the `build-jnilibs.sh` script to rebuild them.
Terminal resources
==================
* [XTerm control sequences](http://invisible-island.net/xterm/ctlseqs/ctlseqs.html)
* [vt100.net](http://vt100.net/)
* [Terminal codes (ANSI and terminfo equivalents)](http://wiki.bash-hackers.org/scripting/terminalcodes)
Terminal emulators
==================
* VTE (libvte): Terminal emulator widget for GTK+, mainly used in gnome-terminal. [Source](https://github.com/GNOME/vte), [Open Issues](https://bugzilla.gnome.org/buglist.cgi?quicksearch=product%3A%22vte%22+), and [All (including closed) issues](https://bugzilla.gnome.org/buglist.cgi?bug_status=RESOLVED&bug_status=VERIFIED&chfield=resolution&chfieldfrom=-2000d&chfieldvalue=FIXED&product=vte&resolution=FIXED).
* iTerm 2: Mac terminal application. [Source](https://github.com/gnachman/iTerm2), [Issues](https://gitlab.com/gnachman/iterm2/issues) and [Documentation](http://www.iterm2.com/documentation.html) (which includes [iTerm2 proprietary escape codes](http://www.iterm2.com/documentation-escape-codes.html)).
* Konsole: KDE terminal application. [Source](https://projects.kde.org/projects/kde/applications/konsole/repository), in particular [tests](https://projects.kde.org/projects/kde/applications/konsole/repository/revisions/master/show/tests), [Bugs](https://bugs.kde.org/buglist.cgi?bug_severity=critical&bug_severity=grave&bug_severity=major&bug_severity=crash&bug_severity=normal&bug_severity=minor&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&product=konsole) and [Wishes](https://bugs.kde.org/buglist.cgi?bug_severity=wishlist&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&product=konsole).
* hterm: Javascript terminal implementation from chromium. [Source](https://github.com/chromium/hterm), including [tests](https://github.com/chromium/hterm/blob/master/js/hterm_vt_tests.js), and [google group](https://groups.google.com/a/chromium.org/forum/#!forum/chromium-hterm).
* xterm: The grandfather of terminal emulators. [Source](http://invisible-island.net/datafiles/release/xterm.tar.gz).
* Connectbot: Android SSH client. [Source](https://github.com/connectbot/connectbot)
* Android Terminal Emulator: Android terminal app which Termux terminal handling is based on. Inactive. [Source](https://github.com/jackpal/Android-Terminal-Emulator).

41
app/build.gradle Normal file
View File

@@ -0,0 +1,41 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 23
buildToolsVersion "23.0.1"
sourceSets {
main {
jni.srcDirs = []
}
}
defaultConfig {
applicationId "com.termux"
minSdkVersion 21
targetSdkVersion 22
versionCode 16
versionName "0.16"
}
signingConfigs {
release {
storeFile new File(TERMUX_KEYSTORE_FILE)
storePassword TERMUX_KEYSTORE_PASSWORD
keyAlias TERMUX_KEYSTORE_ALIAS
keyPassword TERMUX_KEYSTORE_PASSWORD
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
}
}
dependencies {
testCompile 'junit:junit:4.12'
}

17
app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,17 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in android-sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

View File

@@ -0,0 +1,13 @@
package com.termux;
import android.app.Application;
import android.test.ApplicationTestCase;
/**
* <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
*/
public class ApplicationTest extends ApplicationTestCase<Application> {
public ApplicationTest() {
super(Application.class);
}
}

View File

@@ -0,0 +1,49 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.termux"
android:installLocation="internalOnly"
android:sharedUserId="com.termux"
android:sharedUserLabel="@string/shared_user_label"
android:versionCode="16"
android:versionName="0.16" >
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
<uses-feature android:name="android.software.leanback" android:required="false" />
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="22" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:banner="@drawable/banner"
android:label="@string/application_name"
android:theme="@style/Theme.Termux"
android:supportsRtl="false" >
<activity
android:name="com.termux.app.TermuxActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize|stateAlwaysVisible" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name="com.termux.app.TermuxHelpActivity"
android:exported="false"
android:label="@string/application_help" />
<service
android:name="com.termux.app.TermuxService"
android:exported="false" />
</application>
</manifest>

View File

@@ -0,0 +1,229 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, user-scalable=no" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Termux Help</title>
<style>
html { font-family: 'sans-serif-light', sans-serif; height: 100%; margin: auto; padding: 0; color: black; background-color: white; }
.page { max-width: 820px; margin: auto; padding: 0 1em; }
body { margin-left: auto; margin-right: auto; margin-top: 0; padding: 0; width: 100%; }
p { font-size: 16px; line-height: 1.3em; }
ul.index { padding-left: 0; }
.index li { list-style-type: none; line-height: 1.8em; }
dt { margin-left: 1em; list-style-type: bullet; }
a, a:visited { color: #0000EE }
</style>
</head>
<body>
<div class="page help">
<h1 id="index">Termux Help</h1>
<ul class="index">
<li><a href="#introduction">Introduction</a></li>
<li><a href="#user_interface">User interface</a></li>
<li><a href="#touch_keyboard">Using a touch keyboard</a></li>
<li><a href="#hardware_keyboard">Using a hardware keyboard</a></li>
<li><a href="#package_management">Package management</a></li>
<li><a href="#text_editing">Text editing</a></li>
<li><a href="#using_ssh">Using SSH</a></li>
<li><a href="#interactive_shells">Interactive shells</a></li>
<li><a href="#termux_android">Termux and Android</a></li>
<li><a href="#add_on_api">Add-on: API</a></li>
<li><a href="#add_on_float">Add-on: Float</a></li>
<li><a href="#add_on_styling">Add-on: Styling</a></li>
<li><a href="#add_on_widget">Add-on: Widget</a></li>
<li><a href="#source_and_licenses">Source and licenses</a>
</ul>
<h2 id="introduction">Introduction</h2>
<p>Termux is a terminal emulator for Android combined with a collection of packages for command line software. This help
explains both the terminal interface and the packaging tool available from inside the terminal.</p>
<p>Want to ask a question, report a bug or have an idea for a new package or feature?
Visit the <a href="https://plus.google.com/communities/101692629528551299417">Google+ Termux Community</a>!</p>
<h2 id="user_interface">User interface</h2>
<p>At launch Termux shows a terminal interface, whose text size can be adjusted by pinch zooming or double tapping
and pulling the content towards or from you.</p>
<p>Besides the terminal (with keyboard shortcuts explained below) there are three additional interface elements available:
A <strong>context menu</strong>, <strong>navigation drawer</strong>
and <strong>notification</strong>.</p>
<p>The <strong>context menu</strong> can be shown by long pressing anywhere on the terminal. It provides menu entries for:</p>
<ul>
<li>Selecting and pasting text.</li>
<li>Sharing text from the terminal to other apps (e.g. email or SMS)</li>
<li>Resetting the terminal if it gets stuck.</li>
<li>Switching the terminal to full-screen.</li>
<li>Hangup (exiting the current terminal session).</li>
<li>Styling the terminal by selecting a font and a color scheme.</li>
<li>Showing this help page.</li>
</ul>
<p>The <strong>navigation drawer</strong> is revealed by swiping from the left part of the screen. It has three
elements:</p>
<ul>
<li>A list of sessions. Clicking on a session shows it in the terminal while long pressing allows you to specify a session title.</li>
<li>A button to toggle visibility of the touch keyboard.</li>
<li>A button to create new terminal sessions (long press for creating a named session or a fail-safe one).</li>
</ul>
<p>The <strong>notification</strong>, available when a terminal session is running, is available by pulling down the notification menu.
Pressing the notification leads to the most current terminal session. The notification may also be expanded
(by pinch-zooming or performing a single-finger glide) to expose three actions:</p>
<ul>
<li>Exiting all running terminal sessions.</li>
<li>Use a wake lock to avoid entering sleep mode.</li>
<li>Use a high performance wifi lock to maximize wifi performance.</li>
</ul>
<p>With a wake or wifi lock held the notification and Termux background processes will be available even if no terminal
session is running, which allows server and other background processes to run more reliably.</p>
<h2 id="touch_keyboard">Using a touch keyboard</h2>
<p>Using the Ctrl key is necessary for working with a terminal - but most touch keyboards
does not include one. For that purpose Termux uses the <em>Volume down</em> button to emulate
the Ctrl key. For example, pressing <em>Volume down+L</em> on a touch keyboard sends the same input as
pressing <em>Ctrl+L</em> on a hardware keyboard. The result of using Ctrl in combination
with a key depends on which program is used, but for many command line tools the following
shortcuts works:</p>
<ul>
<li>Ctrl+A → Move cursor to the beginning of line.</li>
<li>Ctrl+C → Abort (send SIGINT to) current process.</li>
<li>Ctrl+D → Logout of a terminal session.</li>
<li>Ctrl+E → Move cursor to the end of line.</li>
<li>Ctrl+K → Delete from cursor to the end of line.</li>
<li>Ctrl+L → Clear the terminal.</li>
<li>Ctrl+Z → Suspend (send SIGTSTP to) current process.</li>
</ul>
<p>The <em>Volume up</em> key also serves as a special key to produce certain input:</p>
<ul>
<li>Volume Up+L → | (the pipe character).</li>
<li>Volume Up+E → Escape key.</li>
<li>Volume Up+T → Tab key.</li>
<li>Volume Up+1 → F1 (and Volume Up+2 → F2, etc).</li>
<li>Volume Up+B → Alt+B, back a word when using readline.</li>
<li>Volume Up+F → Alt+F, forward a word when using readline.</li>
<li>Volume Up+W → Up arrow key.</li>
<li>Volume Up+A → Left arrow key.</li>
<li>Volume Up+S → Down arrow key.</li>
<li>Volume Up+D → Right arrow key.</li>
</ul>
<h2 id="hardware_keyboard">Using a hardware keyboard</h2>
<p>The following shortcuts are available when using Termux with a hardware (e.g. bluetooth) keyboard by combining them with <em>Ctrl+Shift</em>:</p>
<ul>
<li>'C' → Create new session</li>
<li>'R' → Rename current session</li>
<li>Down arrow (or 'N') → Next session</li>
<li>Up arrow (or 'P') → Previous session</li>
<li>Right arrow → Open drawer</li>
<li>Left arrow → Close drawer</li>
<li>'F' → Toggle full screen</li>
<li>'M' → Show menu</li>
<li>'V' → Paste</li>
<li>+/- → Adjust text size</li>
<li>1-9 → Go to numbered session</li>
</ul>
<h2 id="package_management">Package management</h2>
<p>A minimal base system consisting of the Apt package manager and the busybox collection of system utilities
is installed when first starting Termux. Additional packages are available using the apt command:</p>
<dl>
<dt>apt update</dt><dd>Updates the list of available packages. This commands needs to be run initially directly after installation
and regularly afterwards to receive updates.</dd>
<dt>apt search &lt;query&gt;</dt><dd>Search among available packages.</dd>
<dt>apt install &lt;package&gt;</dt><dd>Install a new package.</dd>
<dt>apt upgrade</dt><dd>Upgrade outdated packages. For Apt to know about newer packages you will need to update the package index, so you will normally want to run <em>apt update</em> before upgrading.</dd>
<dt>apt show &lt;package&gt;</dt><dd>Show information about a package.</dd>
<dt>apt list</dt><dd>List all available packages.</dd>
<dt>apt list --installed</dt><dd>List all installed packages.</dd>
<dt>apt remove &lt;package&gt;</dt><dd>Remove an installed package.</dd>
</dl>
<p>Apt as a package manager uses a package format named <em>dpkg</em>. Normally direct use of dpkg is not necessary, but the
following two commands may be of use:</p>
<dl>
<dt>dpkg -L &lt;package&gt;</dt>
<dd>List installed files of a package.</dd>
<dt>dpkg --verify</dt>
<dd>Verify the integrity of installed packages.</dd>
</dl>
<p>View the apt manual page (execute <em>apt install man</em> to install a man page viewer first) for more information.</p>
<h2 id="text_editing">Text editing</h2>
<p>By default the busybox version of <em>vi</em> is available. This is a barebone and somewhat unfriendly editor -
install <a href="http://www.nano-editor.org/dist/v2.2/nano.html">nano</a> for a more straight-forward editor and
<a href="http://vimdoc.sourceforge.net/htmldoc/usr_toc.html">vim</a> for a more powerful one.</p>
<h2 id="using_ssh">Using SSH</h2>
<p>By installing the <strong>openssh</strong> package (by executing <em>apt install openssh</em>) you may SSH into remote systems,
optionally putting private keys or configuration under $HOME/.ssh/.</p>
<p>If you wish to use an SSH agent to avoid entering passwords, the Termux openssh package provides
a wrapper script named <strong>ssha</strong> (note the 'a' at the end) for ssh which:</p>
<ol>
<li>Starts the ssh agent if necessary (or connect to it if already running).</li>
<li>Runs <strong>ssh-add</strong> if necessary.</li>
<li>Runs <strong>ssh</strong> with the provided arguments.</li>
</ol>
<p>This means that the agent will prompt for a key password at first run, but remember the authorization for subsequent ones.</p>
<h2 id="interactive_shells">Interactive shells</h2>
<p>The base system that is installed when first starting Termux uses the <em>bash</em> shell while zsh is available as
an installable alternative:</p>
<ul>
<li>bash - the default shell on most Linux distributions, with resources such as
<a href="http://www.tldp.org/LDP/Bash-Beginners-Guide/html/">Bash Guide for Beginners</a>,
the <a href="https://www.gnu.org/software/bash/manual/bash.html">Bash Reference Manual</a>
or the <a href="http://www.tldp.org/LDP/abs/html/">Advanced Bash-Scripting Guide</a> available.</li>
<li>zsh - a powerful shell with information available at
<a href="http://zsh.sourceforge.net/Guide/zshguide.html">A User's Guide to the Z-Shell</a>, the
<a href="http://zsh.sourceforge.net/Doc/Release/zsh_toc.html">Z Shell Manual</a> or
<a href="http://www.rayninfo.co.uk/tips/zshtips.html">ZSH Tips by ZZapper</a>.
After installing zsh through <em>apt install zsh</em>, execute <em>chsh -s zsh</em> to set it as the default login shell when starting Termux
(and change back with <em>chsh -s bash</em> if necessary).</li>
</ul>
<h2 id="termux_android">Termux and Android</h2>
<p>Termux is designed to cope with the restrictions of running as an ordinary Android app without requiring root, which
leads to several differences between Termux and a traditional desktop system. The file system layout is drastically different:</p>
<ul>
<li>Common folders such as /bin, /usr/, /var and /etc does not exist.</li>
<li>The Android system provides a basic non-standard file system hierarchy, where e.g. /system/bin contains some system binaries.</li>
<li>The user folder $HOME is inside the private file area exposed to Termux as an ordinary Android app.
Uninstalling Termux will cause this file area to be wiped - so save important files outside this area such as in /sdcard
or use a version control system such as <em>git</em>.</li>
<li>Termux installs its packages in a folder exposed through the $PREFIX environment variable (with e.g. binaries in $PREFIX/bin,
and configuration in $PREFIX/etc).</li>
<li>Shared libraries are installed in $PREFIX/lib, which are available from binaries due to Termux setting the $LD_LIBRARY_PATH
environment variable. These may clash with Android system binaries in /system/bin, which may force LD_LIBRARY_PATH to be
cleared before running system binaries.</li>
</ul>
<p>Besides the file system being different, Termux is running as a single-user system without root - each Android app is running as
its own Linux user, so running commands inside Termux may not interfere with other installed applications.</p>
<p>Running as non-root implies that ports below 1024 cannot be bound to. Many packages have been configured to have compatible
default values - the ftpd, httpd, and sshd servers default to 8021, 8080 and 8022, respectively.</p>
<h2 id="add_on_api">Add-on: API</h2>
<p>The API add-on exposes Android system functionality such as SMS messages, GPS location or the Text-to-speech functionality through command line tools.</p>
<ul><li><a href="http://play.google.com/store/apps/details?id=com.termux.api">See more and install from Google Play</a></li></ul>
<h2 id="add_on_float">Add-on: Float</h2>
<p>The Float add-on consists of a floating terminal window visible while running other apps.</p>
<ul><li><a href="http://play.google.com/store/apps/details?id=com.termux.window">See more and install from Google Play</a></li></ul>
<h2 id="add_on_styling">Add-on: Styling</h2>
<p>The Styling add-on provides color schemes and fonts to beabeautify and customize the appearance of the Termux terminal.</p>
<ul><li><a href="http://play.google.com/store/apps/details?id=com.termux.styling">See more and install from Google Play</a></li></ul>
<h2 id="add_on_widget">Add-on: Widget</h2>
<p>The Widget add-on brings a widget to your homescreen, providing links to run scripts in your $HOME/.shortcuts/ folder.</p>
<ul><li><a href="http://play.google.com/store/apps/details?id=com.termux.widget">See more and install from Google Play</a></li></ul>
<h2 id="source_and_licenses">Source and licenses</h2>
<p>Termux uses terminal emulation code from <a href="https://github.com/jackpal/Android-Terminal-Emulator">Terminal Emulator for Android</a>
which is under the <a href="https://raw.githubusercontent.com/jackpal/Android-Terminal-Emulator/master/NOTICE">Apache License, Version 2.0</a>.
Packages available through Termux are distributed under their respective licenses with scripts and patches used to build them
<a href="https://github.com/termux/termux-packages">available on github</a>.</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,43 @@
package com.termux.app;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.util.TypedValue;
import android.view.ViewGroup.LayoutParams;
import android.widget.EditText;
import android.widget.LinearLayout;
final class DialogUtils {
public interface TextSetListener {
void onTextSet(String text);
}
static void textInput(Activity activity, int titleText, int positiveButtonText, String initialText, final TextSetListener onPositive) {
final EditText input = new EditText(activity);
input.setSingleLine();
if (initialText != null) input.setText(initialText);
float dipInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, activity.getResources().getDisplayMetrics());
// https://www.google.com/design/spec/components/dialogs.html#dialogs-specs
int paddingTopAndSides = Math.round(16 * dipInPixels);
int paddingBottom = Math.round(24 * dipInPixels);
LinearLayout layout = new LinearLayout(activity);
layout.setOrientation(LinearLayout.VERTICAL);
layout.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
// layout.setGravity(Gravity.CLIP_VERTICAL);
layout.setPadding(paddingTopAndSides, paddingTopAndSides, paddingTopAndSides, paddingBottom);
layout.addView(input);
new AlertDialog.Builder(activity).setTitle(titleText).setView(layout).setPositiveButton(positiveButtonText, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface d, int whichButton) {
onPositive.onTextSet(input.getText().toString());
}
}).setNegativeButton(android.R.string.cancel, null).show();
input.requestFocus();
}
}

View File

@@ -0,0 +1,68 @@
package com.termux.app;
import android.app.Activity;
import android.graphics.Rect;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.Window;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.FrameLayout.LayoutParams;
/**
* Utility to make the touch keyboard and immersive mode work with full screen activities.
*
* See https://code.google.com/p/android/issues/detail?id=5497
*/
final class FullScreenHelper implements ViewTreeObserver.OnGlobalLayoutListener {
private boolean mEnabled = false;
private final Activity mActivity;
private final Rect mWindowRect = new Rect();
public FullScreenHelper(Activity activity) {
this.mActivity = activity;
}
public void setImmersive(boolean enabled) {
Window win = mActivity.getWindow();
if (enabled == mEnabled) {
if (!enabled) win.setFlags(0, WindowManager.LayoutParams.FLAG_FULLSCREEN);
return;
}
mEnabled = enabled;
final View childViewOfContent = ((FrameLayout) mActivity.findViewById(android.R.id.content)).getChildAt(0);
if (enabled) {
win.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
setImmersiveMode();
childViewOfContent.getViewTreeObserver().addOnGlobalLayoutListener(this);
} else {
win.setFlags(0, WindowManager.LayoutParams.FLAG_FULLSCREEN);
win.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
childViewOfContent.getViewTreeObserver().removeOnGlobalLayoutListener(this);
((LayoutParams) childViewOfContent.getLayoutParams()).height = android.view.ViewGroup.LayoutParams.MATCH_PARENT;
}
}
private void setImmersiveMode() {
mActivity.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
}
@Override
public void onGlobalLayout() {
final View childViewOfContent = ((FrameLayout) mActivity.findViewById(android.R.id.content)).getChildAt(0);
if (mEnabled) setImmersiveMode();
childViewOfContent.getWindowVisibleDisplayFrame(mWindowRect);
int usableHeightNow = Math.min(mWindowRect.height(), childViewOfContent.getRootView().getHeight());
FrameLayout.LayoutParams layout = (LayoutParams) childViewOfContent.getLayoutParams();
if (layout.height != usableHeightNow) {
layout.height = usableHeightNow;
childViewOfContent.requestLayout();
}
}
}

View File

@@ -0,0 +1,752 @@
package com.termux.app;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.termux.R;
import com.termux.drawer.DrawerLayout;
import com.termux.terminal.TerminalSession;
import com.termux.terminal.TerminalSession.SessionChangedCallback;
import com.termux.view.TerminalKeyListener;
import com.termux.view.TerminalView;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnShowListener;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Vibrator;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.StyleSpan;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnKeyListener;
import android.view.View.OnLongClickListener;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.AdapterView.OnItemLongClickListener;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
/**
* A terminal emulator activity.
*
* See
* <ul>
* <li>http://www.mongrel-phones.com.au/default/how_to_make_a_local_service_and_bind_to_it_in_android</li>
* <li>https://code.google.com/p/android/issues/detail?id=6426</li>
* </ul>
* about memory leaks.
*/
public final class TermuxActivity extends Activity implements ServiceConnection {
private static final int CONTEXTMENU_SELECT_ID = 0;
private static final int CONTEXTMENU_PASTE_ID = 3;
private static final int CONTEXTMENU_KILL_PROCESS_ID = 4;
private static final int CONTEXTMENU_RESET_TERMINAL_ID = 5;
private static final int CONTEXTMENU_STYLING_ID = 6;
private static final int CONTEXTMENU_TOGGLE_FULLSCREEN_ID = 7;
private static final int CONTEXTMENU_HELP_ID = 8;
private static final int MAX_SESSIONS = 8;
private static final String RELOAD_STYLE_ACTION = "com.termux.app.reload_style";
/** The main view of the activity showing the terminal. */
TerminalView mTerminalView;
final FullScreenHelper mFullScreenHelper = new FullScreenHelper(this);
TermuxPreferences mSettings;
/**
* The connection to the {@link TermuxService}. Requested in {@link #onCreate(Bundle)} with a call to
* {@link #bindService(Intent, ServiceConnection, int)}, and obtained and stored in
* {@link #onServiceConnected(ComponentName, IBinder)}.
*/
TermuxService mTermService;
/** Initialized in {@link #onServiceConnected(ComponentName, IBinder)}. */
ArrayAdapter<TerminalSession> mListViewAdapter;
/** The last toast shown, used cancel current toast before showing new in {@link #showToast(String, boolean)}. */
Toast mLastToast;
/**
* If between onResume() and onStop(). Note that only one session is in the foreground of the terminal view at the
* time, so if the session causing a change is not in the foreground it should probably be treated as background.
*/
boolean mIsVisible;
private final BroadcastReceiver mBroadcastReceiever = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (mIsVisible) {
String whatToReload = intent.getStringExtra(RELOAD_STYLE_ACTION);
if (whatToReload == null || "colors".equals(whatToReload)) mTerminalView.checkForColors();
if (whatToReload == null || "font".equals(whatToReload)) mTerminalView.checkForTypeface();
}
}
};
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
// Prevent overdraw:
getWindow().getDecorView().setBackground(null);
setContentView(R.layout.drawer_layout);
mTerminalView = (TerminalView) findViewById(R.id.terminal_view);
mSettings = new TermuxPreferences(this);
mTerminalView.setTextSize(mSettings.getFontSize());
mFullScreenHelper.setImmersive(mSettings.isFullScreen());
mTerminalView.requestFocus();
OnKeyListener keyListener = new OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (event.getAction() != KeyEvent.ACTION_DOWN) return false;
final TerminalSession currentSession = getCurrentTermSession();
if (keyCode == KeyEvent.KEYCODE_ENTER && !currentSession.isRunning()) {
// Return pressed with finished session - remove it.
currentSession.finishIfRunning();
int index = mTermService.removeTermSession(currentSession);
mListViewAdapter.notifyDataSetChanged();
if (mTermService.getSessions().isEmpty()) {
// There are no sessions to show, so finish the activity.
finish();
} else {
if (index >= mTermService.getSessions().size()) {
index = mTermService.getSessions().size() - 1;
}
switchToSession(mTermService.getSessions().get(index));
}
return true;
} else if (!(event.isCtrlPressed() && event.isShiftPressed())) {
// Only hook shortcuts with Ctrl+Shift down.
return false;
}
// Get the unmodified code point:
int unicodeChar = event.getUnicodeChar(0);
if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN || unicodeChar == 'n'/* next */) {
int index = mTermService.getSessions().indexOf(currentSession);
if (++index >= mTermService.getSessions().size()) index = 0;
switchToSession(mTermService.getSessions().get(index));
} else if (keyCode == KeyEvent.KEYCODE_DPAD_UP || unicodeChar == 'p' /* previous */) {
int index = mTermService.getSessions().indexOf(currentSession);
if (--index < 0) index = mTermService.getSessions().size() - 1;
switchToSession(mTermService.getSessions().get(index));
} else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
getDrawer().openDrawer(Gravity.START);
} else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
getDrawer().closeDrawers();
} else if (unicodeChar == 'f'/* full screen */) {
toggleImmersive();
} else if (unicodeChar == 'm'/* menu */) {
mTerminalView.showContextMenu();
} else if (unicodeChar == 'r'/* rename */) {
renameSession(currentSession);
} else if (unicodeChar == 'c'/* create */) {
addNewSession(false, null);
} else if (unicodeChar == 'u' /* urls */) {
showUrlSelection();
} else if (unicodeChar == 'v') {
doPaste();
} else if (unicodeChar == '+' || event.getUnicodeChar(KeyEvent.META_SHIFT_ON) == '+') {
// We also check for the shifted char here since shift may be required to produce '+',
// see https://github.com/termux/termux-api/issues/2
changeFontSize(true);
} else if (unicodeChar == '-') {
changeFontSize(false);
} else if (unicodeChar >= '1' && unicodeChar <= '9') {
int num = unicodeChar - '1';
if (mTermService.getSessions().size() > num) switchToSession(mTermService.getSessions().get(num));
}
return true;
}
};
mTerminalView.setOnKeyListener(keyListener);
findViewById(R.id.left_drawer_list).setOnKeyListener(keyListener);
mTerminalView.setOnKeyListener(new TerminalKeyListener() {
@Override
public float onScale(float scale) {
if (scale < 0.9f || scale > 1.1f) {
boolean increase = scale > 1.f;
changeFontSize(increase);
return 1.0f;
}
return scale;
}
@Override
public void onLongPress(MotionEvent event) {
mTerminalView.showContextMenu();
}
@Override
public void onSingleTapUp(MotionEvent e) {
// Toggle keyboard visibility if tapping with a finger:
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, 0);
}
});
findViewById(R.id.new_session_button).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
addNewSession(false, null);
}
});
findViewById(R.id.new_session_button).setOnLongClickListener(new OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
Resources res = getResources();
new AlertDialog.Builder(TermuxActivity.this).setTitle(R.string.new_session)
.setItems(new String[] { res.getString(R.string.new_session_normal_unnamed), res.getString(R.string.new_session_normal_named),
res.getString(R.string.new_session_failsafe) }, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case 0:
addNewSession(false, null);
break;
case 1:
DialogUtils.textInput(TermuxActivity.this, R.string.session_new_named_title, R.string.session_new_named_positive_button, null,
new DialogUtils.TextSetListener() {
@Override
public void onTextSet(String text) {
addNewSession(false, text);
}
});
break;
case 2:
addNewSession(true, null);
break;
}
}
}).show();
return true;
}
});
findViewById(R.id.toggle_keyboard_button).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, 0);
getDrawer().closeDrawers();
}
});
registerForContextMenu(mTerminalView);
Intent serviceIntent = new Intent(this, TermuxService.class);
// Start the service and make it run regardless of who is bound to it:
startService(serviceIntent);
if (!bindService(serviceIntent, this, 0)) throw new RuntimeException("bindService() failed");
mTerminalView.checkForTypeface();
mTerminalView.checkForColors();
}
/**
* Part of the {@link ServiceConnection} interface. The service is bound with
* {@link #bindService(Intent, ServiceConnection, int)} in {@link #onCreate(Bundle)} which will cause a call to this
* callback method.
*/
@Override
public void onServiceConnected(ComponentName componentName, IBinder service) {
mTermService = ((TermuxService.LocalBinder) service).service;
mTermService.mSessionChangeCallback = new SessionChangedCallback() {
@Override
public void onTextChanged(TerminalSession changedSession) {
if (!mIsVisible) return;
if (getCurrentTermSession() == changedSession) mTerminalView.onScreenUpdated();
}
@Override
public void onTitleChanged(TerminalSession updatedSession) {
if (!mIsVisible) return;
if (updatedSession != getCurrentTermSession()) {
// Only show toast for other sessions than the current one, since the user
// probably consciously caused the title change to change in the current session
// and don't want an annoying toast for that.
showToast(toToastTitle(updatedSession), false);
}
mListViewAdapter.notifyDataSetChanged();
}
@Override
public void onSessionFinished(final TerminalSession finishedSession) {
if (mTermService.mWantsToStop) {
// The service wants to stop as soon as possible.
finish();
return;
}
if (mIsVisible && finishedSession != getCurrentTermSession()) {
// Show toast for non-current sessions that exit.
int indexOfSession = mTermService.getSessions().indexOf(finishedSession);
// Verify that session was not removed before we got told about it finishing:
if (indexOfSession >= 0) showToast(toToastTitle(finishedSession) + " - exited", true);
}
mListViewAdapter.notifyDataSetChanged();
}
@Override
public void onClipboardText(TerminalSession session, String text) {
if (!mIsVisible) return;
showToast("Clipboard set:\n\"" + text + "\"", true);
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
clipboard.setPrimaryClip(new ClipData(null, new String[] { "text/plain" }, new ClipData.Item(text)));
}
@Override
public void onBell(TerminalSession session) {
if (mIsVisible) ((Vibrator) getSystemService(VIBRATOR_SERVICE)).vibrate(50);
}
};
ListView listView = (ListView) findViewById(R.id.left_drawer_list);
mListViewAdapter = new ArrayAdapter<TerminalSession>(getApplicationContext(), R.layout.line_in_drawer, mTermService.getSessions()) {
final StyleSpan boldSpan = new StyleSpan(Typeface.BOLD);
final StyleSpan italicSpan = new StyleSpan(Typeface.ITALIC);
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View row = convertView;
if (row == null) {
LayoutInflater inflater = getLayoutInflater();
row = inflater.inflate(R.layout.line_in_drawer, parent, false);
}
TerminalSession sessionAtRow = getItem(position);
boolean sessionRunning = sessionAtRow.isRunning();
TextView firstLineView = (TextView) row.findViewById(R.id.row_line);
String name = sessionAtRow.mSessionName;
String sessionTitle = sessionAtRow.getTitle();
String numberPart = "[" + (position + 1) + "] ";
String sessionNamePart = (TextUtils.isEmpty(name) ? "" : name);
String sessionTitlePart = (TextUtils.isEmpty(sessionTitle) ? "" : ((sessionNamePart.isEmpty() ? "" : "\n") + sessionTitle));
String text = numberPart + sessionNamePart + sessionTitlePart;
SpannableString styledText = new SpannableString(text);
styledText.setSpan(boldSpan, 0, numberPart.length() + sessionNamePart.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
styledText.setSpan(italicSpan, numberPart.length() + sessionNamePart.length(), text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
firstLineView.setText(styledText);
if (sessionRunning) {
firstLineView.setPaintFlags(firstLineView.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG);
} else {
firstLineView.setPaintFlags(firstLineView.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
}
int color = sessionRunning || sessionAtRow.getExitStatus() == 0 ? Color.BLACK : Color.RED;
firstLineView.setTextColor(color);
return row;
}
};
listView.setAdapter(mListViewAdapter);
listView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
TerminalSession clickedSession = mListViewAdapter.getItem(position);
switchToSession(clickedSession);
getDrawer().closeDrawers();
}
});
listView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
@Override
public boolean onItemLongClick(AdapterView<?> parent, View view, final int position, long id) {
final TerminalSession selectedSession = mListViewAdapter.getItem(position);
renameSession(selectedSession);
return true;
}
});
if (mTermService.getSessions().isEmpty()) {
if (mIsVisible) {
TermuxInstaller.setupIfNeeded(TermuxActivity.this, new Runnable() {
@Override
public void run() {
if (TermuxPreferences.isShowWelcomeDialog(TermuxActivity.this)) {
new AlertDialog.Builder(TermuxActivity.this).setTitle(R.string.welcome_dialog_title).setMessage(R.string.welcome_dialog_body)
.setCancelable(false).setPositiveButton(android.R.string.ok, null)
.setNegativeButton(R.string.welcome_dialog_dont_show_again_button, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
TermuxPreferences.disableWelcomeDialog(TermuxActivity.this);
dialog.dismiss();
}
}).show();
}
addNewSession(false, null);
}
});
} else {
// The service connected while not in foreground - just bail out.
finish();
}
} else {
switchToSession(getStoredCurrentSessionOrLast());
}
}
@SuppressLint("InflateParams")
void renameSession(final TerminalSession sessionToRename) {
DialogUtils.textInput(this, R.string.session_rename_title, R.string.session_rename_positive_button, sessionToRename.mSessionName,
new DialogUtils.TextSetListener() {
@Override
public void onTextSet(String text) {
sessionToRename.mSessionName = text;
}
});
}
@Override
public void onServiceDisconnected(ComponentName name) {
if (mTermService != null) {
// Respect being stopped from the TermuxService notification action.
finish();
}
}
TerminalSession getCurrentTermSession() {
return mTerminalView.getCurrentSession();
}
@Override
public void onStart() {
super.onStart();
mIsVisible = true;
if (mTermService != null) {
// The service has connected, but data may have changed since we were last in the foreground.
switchToSession(getStoredCurrentSessionOrLast());
mListViewAdapter.notifyDataSetChanged();
}
registerReceiver(mBroadcastReceiever, new IntentFilter(RELOAD_STYLE_ACTION));
}
@Override
protected void onStop() {
super.onStop();
mIsVisible = false;
TerminalSession currentSession = getCurrentTermSession();
if (currentSession != null) TermuxPreferences.storeCurrentSession(this, currentSession);
unregisterReceiver(mBroadcastReceiever);
getDrawer().closeDrawers();
}
@Override
public void onBackPressed() {
if (getDrawer().isDrawerOpen(Gravity.START))
getDrawer().closeDrawers();
else
finish();
}
@Override
public void onDestroy() {
super.onDestroy();
if (mTermService != null) {
// Do not leave service with references to activity.
mTermService.mSessionChangeCallback = null;
mTermService = null;
}
unbindService(this);
}
DrawerLayout getDrawer() {
return (DrawerLayout) findViewById(R.id.drawer_layout);
}
void addNewSession(boolean failSafe, String sessionName) {
if (mTermService.getSessions().size() >= MAX_SESSIONS) {
new AlertDialog.Builder(this).setTitle(R.string.max_terminals_reached_title).setMessage(R.string.max_terminals_reached_message)
.setPositiveButton(android.R.string.ok, null).show();
} else {
String executablePath = (failSafe ? "/system/bin/sh" : null);
TerminalSession newSession = mTermService.createTermSession(executablePath, null, null, failSafe);
if (sessionName != null) {
newSession.mSessionName = sessionName;
}
switchToSession(newSession);
getDrawer().closeDrawers();
}
}
/** Try switching to session and note about it, but do nothing if already displaying the session. */
void switchToSession(TerminalSession session) {
if (mTerminalView.attachSession(session)) noteSessionInfo();
}
String toToastTitle(TerminalSession session) {
final int indexOfSession = mTermService.getSessions().indexOf(session);
StringBuilder toastTitle = new StringBuilder("[" + (indexOfSession + 1) + "]");
if (!TextUtils.isEmpty(session.mSessionName)) {
toastTitle.append(" ").append(session.mSessionName);
}
String title = session.getTitle();
if (!TextUtils.isEmpty(title)) {
// Space to "[${NR}] or newline after session name:
toastTitle.append(session.mSessionName == null ? " " : "\n");
toastTitle.append(title);
}
return toastTitle.toString();
}
void noteSessionInfo() {
if (!mIsVisible) return;
TerminalSession session = getCurrentTermSession();
final int indexOfSession = mTermService.getSessions().indexOf(session);
showToast(toToastTitle(session), false);
mListViewAdapter.notifyDataSetChanged();
final ListView lv = ((ListView) findViewById(R.id.left_drawer_list));
lv.setItemChecked(indexOfSession, true);
lv.smoothScrollToPosition(indexOfSession);
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
TerminalSession currentSession = getCurrentTermSession();
if (currentSession == null) return;
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
menu.add(Menu.NONE, CONTEXTMENU_PASTE_ID, Menu.NONE, R.string.paste_text).setEnabled(clipboard.hasPrimaryClip());
menu.add(Menu.NONE, CONTEXTMENU_SELECT_ID, Menu.NONE, R.string.select);
menu.add(Menu.NONE, CONTEXTMENU_RESET_TERMINAL_ID, Menu.NONE, R.string.reset_terminal);
menu.add(Menu.NONE, CONTEXTMENU_KILL_PROCESS_ID, Menu.NONE, R.string.kill_process).setEnabled(currentSession.isRunning());
menu.add(Menu.NONE, CONTEXTMENU_TOGGLE_FULLSCREEN_ID, Menu.NONE, R.string.toggle_fullscreen).setCheckable(true).setChecked(mSettings.isFullScreen());
menu.add(Menu.NONE, CONTEXTMENU_STYLING_ID, Menu.NONE, R.string.style_terminal);
menu.add(Menu.NONE, CONTEXTMENU_HELP_ID, Menu.NONE, R.string.help);
}
/** Hook system menu to show context menu instead. */
@Override
public boolean onCreateOptionsMenu(Menu menu) {
mTerminalView.showContextMenu();
return false;
}
void showUrlSelection() {
String text = getCurrentTermSession().getEmulator().getScreen().getTranscriptText();
// Pattern for recognizing a URL, based off RFC 3986
// http://stackoverflow.com/questions/5713558/detect-and-extract-url-from-a-string
final Pattern urlPattern = Pattern.compile(
"(?:^|[\\W])((ht|f)tp(s?):\\/\\/|www\\.)" + "(([\\w\\-]+\\.){1,}?([\\w\\-.~]+\\/?)*" + "[\\p{Alnum}.,%_=?&#\\-+()\\[\\]\\*$~@!:/{};']*)",
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
LinkedHashSet<CharSequence> urlSet = new LinkedHashSet<>();
Matcher matcher = urlPattern.matcher(text);
while (matcher.find()) {
int matchStart = matcher.start(1);
int matchEnd = matcher.end();
String url = text.substring(matchStart, matchEnd);
urlSet.add(url);
}
if (urlSet.isEmpty()) {
new AlertDialog.Builder(this).setMessage(R.string.select_url_no_found).show();
return;
}
final CharSequence[] urls = urlSet.toArray(new CharSequence[urlSet.size()]);
Collections.reverse(Arrays.asList(urls)); // Latest first.
// Click to copy url to clipboard:
final AlertDialog dialog = new AlertDialog.Builder(TermuxActivity.this).setItems(urls, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface di, int which) {
String url = (String) urls[which];
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
clipboard.setPrimaryClip(new ClipData(null, new String[] { "text/plain" }, new ClipData.Item(url)));
Toast.makeText(TermuxActivity.this, R.string.select_url_copied_to_clipboard, Toast.LENGTH_LONG).show();
}
}).setTitle(R.string.select_url_dialog_title).create();
// Long press to open URL:
dialog.setOnShowListener(new OnShowListener() {
@Override
public void onShow(DialogInterface di) {
ListView lv = dialog.getListView(); // this is a ListView with your "buds" in it
lv.setOnItemLongClickListener(new OnItemLongClickListener() {
@Override
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
dialog.dismiss();
String url = (String) urls[position];
startActivity(Intent.createChooser(new Intent(Intent.ACTION_VIEW, Uri.parse(url)), null));
return true;
}
});
}
});
dialog.show();
}
@Override
public boolean onContextItemSelected(MenuItem item) {
switch (item.getItemId()) {
case CONTEXTMENU_SELECT_ID:
CharSequence[] items = new CharSequence[] { getString(R.string.select_text), getString(R.string.select_url),
getString(R.string.select_all_and_share) };
new AlertDialog.Builder(this).setItems(items, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case 0:
mTerminalView.toggleSelectingText();
break;
case 1:
showUrlSelection();
break;
case 2:
TerminalSession session = getCurrentTermSession();
if (session != null) {
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TEXT, session.getEmulator().getScreen().getTranscriptText().trim());
intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.share_transcript_title));
startActivity(Intent.createChooser(intent, getString(R.string.share_transcript_chooser_title)));
}
break;
}
dialog.dismiss();
}
}).show();
return true;
case CONTEXTMENU_PASTE_ID:
doPaste();
return true;
case CONTEXTMENU_KILL_PROCESS_ID:
final AlertDialog.Builder b = new AlertDialog.Builder(this);
b.setIcon(android.R.drawable.ic_dialog_alert);
b.setMessage(R.string.confirm_kill_process);
b.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
dialog.dismiss();
getCurrentTermSession().finishIfRunning();
}
});
b.setNegativeButton(android.R.string.no, null);
b.show();
return true;
case CONTEXTMENU_RESET_TERMINAL_ID: {
TerminalSession session = getCurrentTermSession();
if (session != null) {
session.reset();
showToast(getResources().getString(R.string.reset_toast_notification), true);
}
return true;
}
case CONTEXTMENU_STYLING_ID: {
Intent stylingIntent = new Intent();
stylingIntent.setClassName("com.termux.styling", "com.termux.styling.TermuxStyleActivity");
try {
startActivity(stylingIntent);
} catch (ActivityNotFoundException e) {
new AlertDialog.Builder(this).setMessage(R.string.styling_not_installed)
.setPositiveButton(R.string.styling_install, new android.content.DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://play.google.com/store/apps/details?id=com.termux.styling")));
}
}).setNegativeButton(android.R.string.cancel, null).show();
}
}
return true;
case CONTEXTMENU_TOGGLE_FULLSCREEN_ID:
toggleImmersive();
return true;
case CONTEXTMENU_HELP_ID:
startActivity(new Intent(this, TermuxHelpActivity.class));
return true;
default:
return super.onContextItemSelected(item);
}
}
void toggleImmersive() {
boolean newValue = !mSettings.isFullScreen();
mSettings.setFullScreen(this, newValue);
mFullScreenHelper.setImmersive(newValue);
}
void changeFontSize(boolean increase) {
mSettings.changeFontSize(this, increase);
mTerminalView.setTextSize(mSettings.getFontSize());
}
void doPaste() {
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clipData = clipboard.getPrimaryClip();
if (clipData == null) return;
CharSequence paste = clipData.getItemAt(0).coerceToText(this);
if (!TextUtils.isEmpty(paste)) getCurrentTermSession().getEmulator().paste(paste.toString());
}
/** The current session as stored or the last one if that does not exist. */
public TerminalSession getStoredCurrentSessionOrLast() {
TerminalSession stored = TermuxPreferences.getCurrentSession(this);
if (stored != null) return stored;
int numberOfSessions = mTermService.getSessions().size();
if (numberOfSessions == 0) return null;
return mTermService.getSessions().get(numberOfSessions - 1);
}
/** Show a toast and dismiss the last one if still visible. */
void showToast(String text, boolean longDuration) {
if (mLastToast != null) mLastToast.cancel();
mLastToast = Toast.makeText(TermuxActivity.this, text, longDuration ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT);
mLastToast.setGravity(Gravity.TOP, 0, 0);
mLastToast.show();
}
}

View File

@@ -0,0 +1,46 @@
package com.termux.app;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.webkit.WebView;
import android.webkit.WebViewClient;
/** Basic embedded browser for viewing the bundled help page. */
public final class TermuxHelpActivity extends Activity {
private WebView mWebView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mWebView = new WebView(this);
setContentView(mWebView);
mWebView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
try {
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
} catch (ActivityNotFoundException e) {
// TODO: Android TV does not have a system browser - but needs better method of getting back
// than navigating deep here.
return false;
}
return true;
}
});
mWebView.loadUrl("file:///android_asset/help.html");
}
@Override
public void onBackPressed() {
if (mWebView.canGoBack()) {
mWebView.goBack();
} else {
super.onBackPressed();
}
}
}

View File

@@ -0,0 +1,193 @@
package com.termux.app;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.DialogInterface.OnDismissListener;
import android.system.Os;
import android.util.Log;
import android.util.Pair;
import com.termux.R;
import com.termux.terminal.EmulatorDebug;
/**
* Install the Termux bootstrap packages if necessary by following the below steps:
*
* (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.
*
* (2) A progress dialog is shown with "Installing..." message and a spinner.
*
* (3) A staging folder, $STAGING_PREFIX, is {@link #deleteFolder(File)} if left over from broken installation below.
*
* (4) The architecture is determined and an appropriate bootstrap zip url is determined in {@link #determineZipUrl()}.
*
* (5) The zip, containing entries relative to the $PREFIX, is is downloaded and extracted by a zip input stream
* continously encountering zip file entries:
*
* (5.1) If the zip entry encountered is SYMLINKS.txt, go through it and remember all symlinks to setup.
*
* (5.2) For every other zip entry, extract it into $STAGING_PREFIX and set execute permissions if necessary.
*/
final class TermuxInstaller {
/** Performs setup if necessary. */
static void setupIfNeeded(final Activity activity, final Runnable whenDone) {
// Termux can only be run as the primary user (device owner) since only that
// account has the expected file system paths. Verify that:
android.os.UserManager um = (android.os.UserManager) activity.getSystemService(Context.USER_SERVICE);
boolean isPrimaryUser = um.getSerialNumberForUser(android.os.Process.myUserHandle()) == 0;
if (!isPrimaryUser) {
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_not_primary_user_message)
.setOnDismissListener(new OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog) {
System.exit(0);
}
}).setPositiveButton(android.R.string.ok, null).show();
return;
}
final File PREFIX_FILE = new File(TermuxService.PREFIX_PATH);
if (PREFIX_FILE.isDirectory()) {
whenDone.run();
return;
}
final ProgressDialog progress = ProgressDialog.show(activity, null, activity.getString(R.string.bootstrap_installer_body), true, false);
new Thread() {
@Override
public void run() {
try {
final String STAGING_PREFIX_PATH = TermuxService.FILES_PATH + "/usr-staging";
final File STAGING_PREFIX_FILE = new File(STAGING_PREFIX_PATH);
if (STAGING_PREFIX_FILE.exists()) {
deleteFolder(STAGING_PREFIX_FILE);
}
final byte[] buffer = new byte[8096];
final List<Pair<String, String>> symlinks = new ArrayList<>(50);
final URL zipUrl = determineZipUrl();
try (ZipInputStream zipInput = new ZipInputStream(zipUrl.openStream())) {
ZipEntry zipEntry;
while ((zipEntry = zipInput.getNextEntry()) != null) {
if (zipEntry.getName().equals("SYMLINKS.txt")) {
BufferedReader symlinksReader = new BufferedReader(new InputStreamReader(zipInput));
String line;
while ((line = symlinksReader.readLine()) != null) {
String[] parts = line.split("");
if (parts.length != 2) throw new RuntimeException("Malformed symlink line: " + line);
String oldPath = parts[0];
String newPath = STAGING_PREFIX_PATH + "/" + parts[1];
symlinks.add(Pair.create(oldPath, newPath));
}
} else {
String zipEntryName = zipEntry.getName();
File targetFile = new File(STAGING_PREFIX_PATH, zipEntryName);
if (zipEntry.isDirectory()) {
if (!targetFile.mkdirs()) throw new RuntimeException("Failed to create directory: " + targetFile.getAbsolutePath());
} else {
try (FileOutputStream outStream = new FileOutputStream(targetFile)) {
int readBytes;
while ((readBytes = zipInput.read(buffer)) != -1)
outStream.write(buffer, 0, readBytes);
}
if (zipEntryName.startsWith("bin/") || zipEntryName.startsWith("libexec") || zipEntryName.startsWith("lib/apt/methods")) {
Os.chmod(targetFile.getAbsolutePath(), 0700);
}
}
}
}
}
if (symlinks.isEmpty()) throw new RuntimeException("No SYMLINKS.txt encountered");
for (Pair<String, String> symlink : symlinks) {
Os.symlink(symlink.first, symlink.second);
}
if (!STAGING_PREFIX_FILE.renameTo(PREFIX_FILE)) {
throw new RuntimeException("Unable to rename staging folder");
}
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
whenDone.run();
}
});
} catch (final Exception e) {
Log.e(EmulatorDebug.LOG_TAG, "Bootstrap error", e);
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_body)
.setNegativeButton(R.string.bootstrap_error_abort, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
activity.finish();
}
}).setPositiveButton(R.string.bootstrap_error_try_again, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
TermuxInstaller.setupIfNeeded(activity, whenDone);
}
}).show();
}
});
} finally {
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
progress.dismiss();
}
});
}
}
}.start();
}
/** Get bootstrap zip url for this systems cpu architecture. */
static URL determineZipUrl() throws MalformedURLException {
String arch = System.getProperty("os.arch");
if (arch.startsWith("arm") || arch.equals("aarch64")) {
// Handle different arm variants such as armv7l:
arch = "arm";
} else if (arch.equals("x86_64")) {
arch = "i686";
}
return new URL("http://apt.termux.com/bootstrap/bootstrap-" + arch + ".zip");
}
/** Delete a folder and all its content or throw. */
static void deleteFolder(File fileOrDirectory) {
File[] children = fileOrDirectory.listFiles();
if (children != null) {
for (File child : children) {
deleteFolder(child);
}
}
if (!fileOrDirectory.delete()) {
throw new RuntimeException("Unable to delete " + (fileOrDirectory.isDirectory() ? "directory " : "file ") + fileOrDirectory.getAbsolutePath());
}
}
}

View File

@@ -0,0 +1,89 @@
package com.termux.app;
import com.termux.terminal.TerminalSession;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.util.TypedValue;
final class TermuxPreferences {
private final int MIN_FONTSIZE;
private static final int MAX_FONTSIZE = 256;
private static final String FULLSCREEN_KEY = "fullscreen";
private static final String FONTSIZE_KEY = "fontsize";
private static final String CURRENT_SESSION_KEY = "current_session";
private static final String SHOW_WELCOME_DIALOG_KEY = "intro_dialog";
private boolean mFullScreen;
private int mFontSize;
TermuxPreferences(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
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
// to prevent invisible text due to zoom be mistake:
MIN_FONTSIZE = (int) (4f * dipInPixels);
mFullScreen = prefs.getBoolean(FULLSCREEN_KEY, false);
// http://www.google.com/design/spec/style/typography.html#typography-line-height
int defaultFontSize = Math.round(12 * dipInPixels);
// Make it divisible by 2 since that is the minimal adjustment step:
if (defaultFontSize % 2 == 1) defaultFontSize--;
try {
mFontSize = Integer.parseInt(prefs.getString(FONTSIZE_KEY, Integer.toString(defaultFontSize)));
} catch (NumberFormatException | ClassCastException e) {
mFontSize = defaultFontSize;
}
mFontSize = Math.max(MIN_FONTSIZE, Math.min(mFontSize, MAX_FONTSIZE));
}
boolean isFullScreen() {
return mFullScreen;
}
void setFullScreen(Context context, boolean newValue) {
mFullScreen = newValue;
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
prefs.edit().putBoolean(FULLSCREEN_KEY, newValue).apply();
}
int getFontSize() {
return mFontSize;
}
void changeFontSize(Context context, boolean increase) {
mFontSize += (increase ? 1 : -1) * 2;
mFontSize = Math.max(MIN_FONTSIZE, Math.min(mFontSize, MAX_FONTSIZE));
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
prefs.edit().putString(FONTSIZE_KEY, Integer.toString(mFontSize)).apply();
}
static void storeCurrentSession(Context context, TerminalSession session) {
PreferenceManager.getDefaultSharedPreferences(context).edit().putString(TermuxPreferences.CURRENT_SESSION_KEY, session.mHandle).commit();
}
static TerminalSession getCurrentSession(TermuxActivity context) {
String sessionHandle = PreferenceManager.getDefaultSharedPreferences(context).getString(TermuxPreferences.CURRENT_SESSION_KEY, "");
for (int i = 0, len = context.mTermService.getSessions().size(); i < len; i++) {
TerminalSession session = context.mTermService.getSessions().get(i);
if (session.mHandle.equals(sessionHandle)) return session;
}
return null;
}
public static boolean isShowWelcomeDialog(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SHOW_WELCOME_DIALOG_KEY, true);
}
public static void disableWelcomeDialog(Context context) {
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(SHOW_WELCOME_DIALOG_KEY, false).apply();
}
}

View File

@@ -0,0 +1,346 @@
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.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.net.Uri;
import android.net.wifi.WifiManager;
import android.os.Binder;
import android.os.IBinder;
import android.os.PowerManager;
import android.util.Log;
import android.widget.ArrayAdapter;
/**
* 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
* 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.
*
* 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)}.
*
* Optionally may hold a wake and a wifi lock, in which case that is shown in the notification - see
* {@link #buildNotification()}.
*/
public final class TermuxService extends Service implements SessionChangedCallback {
/** Note that this is a symlink on the Android M preview. */
@SuppressLint("SdCardPath")
public static final String FILES_PATH = "/data/data/com.termux/files";
public static final String PREFIX_PATH = FILES_PATH + "/usr";
public static final String HOME_PATH = FILES_PATH + "/home";
private static final int NOTIFICATION_ID = 1337;
/** Intent action to stop the service. */
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. */
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. */
private static final String ACTION_LOCK_WIFI = "com.termux.service_toggle_wifi_lock";
/** Intent action to launch a new terminal session. Executed from TermuxWidgetProvider. */
private static final String ACTION_EXECUTE = "com.termux.service_execute";
/** This service is only bound from inside the same process and never uses IPC. */
class LocalBinder extends Binder {
public final TermuxService service = TermuxService.this;
}
private final IBinder mBinder = new LocalBinder();
/**
* The terminal sessions which this service manages.
*
* 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()} }.
*/
final List<TerminalSession> mTerminalSessions = new ArrayList<>();
/** Note that the service may often outlive the activity, so need to clear this reference. */
SessionChangedCallback mSessionChangeCallback;
private PowerManager.WakeLock mWakeLock;
private WifiManager.WifiLock mWifiLock;
/** If the user has executed the {@link #ACTION_STOP_SERVICE} intent. */
boolean mWantsToStop = false;
@SuppressLint("Wakelock")
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
String action = intent.getAction();
if (ACTION_STOP_SERVICE.equals(action)) {
mWantsToStop = true;
for (int i = 0; i < mTerminalSessions.size(); i++)
mTerminalSessions.get(i).finishIfRunning();
stopSelf();
} else if (ACTION_LOCK_WAKE.equals(action)) {
if (mWakeLock == null) {
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, EmulatorDebug.LOG_TAG);
mWakeLock.acquire();
} else {
mWakeLock.release();
mWakeLock = null;
}
updateNotification();
} else if (ACTION_LOCK_WIFI.equals(action)) {
if (mWifiLock == null) {
WifiManager wm = (WifiManager) getSystemService(Context.WIFI_SERVICE);
mWifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, EmulatorDebug.LOG_TAG);
mWifiLock.acquire();
} else {
mWifiLock.release();
mWifiLock = null;
}
updateNotification();
} else if (ACTION_EXECUTE.equals(action)) {
Uri executableUri = intent.getData();
String executablePath = (executableUri == null ? null : executableUri.getPath());
String[] arguments = (executableUri == null ? null : intent.getStringArrayExtra("com.termux.execute.arguments"));
String cwd = intent.getStringExtra("com.termux.execute.cwd");
TerminalSession newSession = createTermSession(executablePath, arguments, cwd, false);
// Transform executable path to session name, e.g. "/bin/do-something.sh" => "do something.sh".
if (executablePath != null) {
int lastSlash = executablePath.lastIndexOf('/');
String name = (lastSlash == -1) ? executablePath : executablePath.substring(lastSlash + 1);
name = name.replace('-', ' ');
newSession.mSessionName = name;
}
// Make the newly created session the current one to be displayed:
TermuxPreferences.storeCurrentSession(this, newSession);
// Launch the main Termux app, which will now show to current session:
startActivity(new Intent(this, TermuxActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
} else if (action != null) {
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
// start of {@link Term):
return Service.START_NOT_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
@Override
public void onCreate() {
startForeground(NOTIFICATION_ID, buildNotification());
}
/** Update the shown foreground service notification after making any changes that affect it. */
private void updateNotification() {
if (mWakeLock == null && mWifiLock == null && getSessions().isEmpty()) {
// Exit if we are updating after the user disabled all locks with no sessions.
stopSelf();
} else {
((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).notify(NOTIFICATION_ID, buildNotification());
}
}
private Notification buildNotification() {
Intent notifyIntent = new Intent(this, TermuxActivity.class);
// 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":
notifyIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notifyIntent, 0);
int sessionCount = mTerminalSessions.size();
String contentText = sessionCount + " terminal session" + (sessionCount == 1 ? "" : "s");
boolean wakeLockHeld = mWakeLock != null;
boolean wifiLockHeld = mWifiLock != null;
if (wakeLockHeld && wifiLockHeld) {
contentText += " (wake&wifi lock held)";
} else if (wakeLockHeld) {
contentText += " (wake lock held)";
} else if (wifiLockHeld) {
contentText += " (wifi lock held)";
}
Notification.Builder builder = new Notification.Builder(this);
builder.setContentTitle(getText(R.string.application_name));
builder.setContentText(contentText);
builder.setSmallIcon(R.drawable.ic_service_notification);
builder.setContentIntent(pendingIntent);
builder.setOngoing(true);
// 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:
builder.setPriority((wakeLockHeld || wifiLockHeld) ? Notification.PRIORITY_HIGH : Notification.PRIORITY_MIN);
// No need to show a timestamp:
builder.setShowWhen(false);
// Background color for small notification icon:
builder.setColor(0xFF000000);
Resources res = getResources();
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));
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),
PendingIntent.getService(this, 0, toggleWakeLockIntent, 0));
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),
PendingIntent.getService(this, 0, toggleWifiLockIntent, 0));
return builder.build();
}
@Override
public void onDestroy() {
if (mWakeLock != null) mWakeLock.release();
if (mWifiLock != null) mWifiLock.release();
stopForeground(true);
for (int i = 0; i < mTerminalSessions.size(); i++)
mTerminalSessions.get(i).finishIfRunning();
mTerminalSessions.clear();
}
public List<TerminalSession> getSessions() {
return mTerminalSessions;
}
TerminalSession createTermSession(String executablePath, String[] arguments, String cwd, boolean failSafe) {
new File(HOME_PATH).mkdirs();
if (cwd == null) cwd = HOME_PATH;
final String termEnv = "TERM=xterm-256color";
final String homeEnv = "HOME=" + HOME_PATH;
final String prefixEnv = "PREFIX=" + PREFIX_PATH;
final String androidRootEnv = "ANDROID_ROOT=" + System.getenv("ANDROID_ROOT");
final String androidDataEnv = "ANDROID_DATA=" + System.getenv("ANDROID_DATA");
String[] env;
if (failSafe) {
env = new String[] { termEnv, homeEnv, prefixEnv, androidRootEnv, androidDataEnv };
} else {
final String ps1Env = "PS1=$ ";
final String ldEnv = "LD_LIBRARY_PATH=" + PREFIX_PATH + "/lib";
final String langEnv = "LANG=en_US.UTF-8";
final String pathEnv = "PATH=" + PREFIX_PATH + "/bin:" + PREFIX_PATH + "/bin/applets:" + System.getenv("PATH");
final String pwdEnv = "PWD=" + cwd;
env = new String[] { termEnv, homeEnv, prefixEnv, ps1Env, ldEnv, langEnv, pathEnv, pwdEnv, androidRootEnv, androidDataEnv };
}
String shellName;
if (executablePath == null) {
File shell = new File(HOME_PATH, ".termux/shell");
if (shell.exists()) {
try {
File canonicalFile = shell.getCanonicalFile();
if (canonicalFile.isFile() && canonicalFile.canExecute()) {
executablePath = canonicalFile.getName().equals("busybox") ? (PREFIX_PATH + "/bin/ash") : canonicalFile.getAbsolutePath();
} else {
Log.w(EmulatorDebug.LOG_TAG, "$HOME/.termux/shell points to non-executable shell: " + canonicalFile.getAbsolutePath());
}
} catch (IOException e) {
Log.e(EmulatorDebug.LOG_TAG, "Error checking $HOME/.termux/shell", e);
}
}
if (executablePath == null) {
// Try bash, zsh and ash in that order:
for (String shellBinary : new String[] { "bash", "zsh", "ash" }) {
File shellFile = new File(PREFIX_PATH + "/bin/" + shellBinary);
if (shellFile.canExecute()) {
executablePath = shellFile.getAbsolutePath();
break;
}
}
}
if (executablePath == null) {
// Fall back to system shell as last resort:
executablePath = "/system/bin/sh";
}
String[] parts = executablePath.split("/");
shellName = "-" + parts[parts.length - 1];
} else {
int lastSlashIndex = executablePath.lastIndexOf('/');
shellName = lastSlashIndex == -1 ? executablePath : executablePath.substring(lastSlashIndex + 1);
}
String[] args;
if (arguments == null) {
args = new String[] { shellName };
} else {
args = new String[arguments.length + 1];
args[0] = shellName;
System.arraycopy(arguments, 0, args, 1, arguments.length);
}
TerminalSession session = new TerminalSession(executablePath, cwd, args, env, this);
mTerminalSessions.add(session);
updateNotification();
return session;
}
public int removeTermSession(TerminalSession sessionToRemove) {
int indexOfRemoved = mTerminalSessions.indexOf(sessionToRemove);
mTerminalSessions.remove(indexOfRemoved);
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
// holding wake lock since there may be daemon processes (e.g. sshd) running.
stopSelf();
} else {
updateNotification();
}
return indexOfRemoved;
}
@Override
public void onTitleChanged(TerminalSession changedSession) {
if (mSessionChangeCallback != null) mSessionChangeCallback.onTitleChanged(changedSession);
}
@Override
public void onSessionFinished(final TerminalSession finishedSession) {
if (mSessionChangeCallback != null) mSessionChangeCallback.onSessionFinished(finishedSession);
}
@Override
public void onTextChanged(TerminalSession changedSession) {
if (mSessionChangeCallback != null) mSessionChangeCallback.onTextChanged(changedSession);
}
@Override
public void onClipboardText(TerminalSession session, String text) {
if (mSessionChangeCallback != null) mSessionChangeCallback.onClipboardText(session, text);
}
@Override
public void onBell(TerminalSession session) {
if (mSessionChangeCallback != null) mSessionChangeCallback.onBell(session);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,88 @@
package com.termux.drawer;
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
/**
* Provides functionality for DrawerLayout unique to API 21
*/
@SuppressLint("RtlHardcoded")
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
class DrawerLayoutCompatApi21 {
private static final int[] THEME_ATTRS = { android.R.attr.colorPrimaryDark };
public static void configureApplyInsets(DrawerLayout drawerLayout) {
drawerLayout.setOnApplyWindowInsetsListener(new InsetsListener());
drawerLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
}
public static void dispatchChildInsets(View child, Object insets, int gravity) {
WindowInsets wi = (WindowInsets) insets;
if (gravity == Gravity.LEFT) {
wi = wi.replaceSystemWindowInsets(wi.getSystemWindowInsetLeft(), wi.getSystemWindowInsetTop(), 0, wi.getSystemWindowInsetBottom());
} else if (gravity == Gravity.RIGHT) {
wi = wi.replaceSystemWindowInsets(0, wi.getSystemWindowInsetTop(), wi.getSystemWindowInsetRight(), wi.getSystemWindowInsetBottom());
}
child.dispatchApplyWindowInsets(wi);
}
public static void applyMarginInsets(ViewGroup.MarginLayoutParams lp, Object insets, int gravity) {
WindowInsets wi = (WindowInsets) insets;
if (gravity == Gravity.LEFT) {
wi = wi.replaceSystemWindowInsets(wi.getSystemWindowInsetLeft(), wi.getSystemWindowInsetTop(), 0, wi.getSystemWindowInsetBottom());
} else if (gravity == Gravity.RIGHT) {
wi = wi.replaceSystemWindowInsets(0, wi.getSystemWindowInsetTop(), wi.getSystemWindowInsetRight(), wi.getSystemWindowInsetBottom());
}
lp.leftMargin = wi.getSystemWindowInsetLeft();
lp.topMargin = wi.getSystemWindowInsetTop();
lp.rightMargin = wi.getSystemWindowInsetRight();
lp.bottomMargin = wi.getSystemWindowInsetBottom();
}
public static int getTopInset(Object insets) {
return insets != null ? ((WindowInsets) insets).getSystemWindowInsetTop() : 0;
}
public static Drawable getDefaultStatusBarBackground(Context context) {
final TypedArray a = context.obtainStyledAttributes(THEME_ATTRS);
try {
return a.getDrawable(0);
} finally {
a.recycle();
}
}
static class InsetsListener implements View.OnApplyWindowInsetsListener {
@Override
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
final DrawerLayout drawerLayout = (DrawerLayout) v;
drawerLayout.setChildInsets(insets, insets.getSystemWindowInsetTop() > 0);
return insets.consumeSystemWindowInsets();
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,10 @@
/**
* Extraction (and some minor cleanup to get rid of warnings) of DrawerLayout from the
* <a href="http://developer.android.com/tools/support-library/index.html">Android Support Library</a>.
*
* Source at:
* https://android.googlesource.com/platform/frameworks/support/+/refs/heads/master/v4/java/android/support/v4/widget/DrawerLayout.java
* https://android.googlesource.com/platform/frameworks/support/+/refs/heads/master/v4/java/android/support/v4/widget/ViewDragHelper.java
*/
package com.termux.drawer;

View File

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

View File

@@ -0,0 +1,10 @@
package com.termux.terminal;
import android.util.Log;
public final class EmulatorDebug {
/** The tag to use with {@link Log}. */
public static final String LOG_TAG = "termux";
}

View File

@@ -0,0 +1,55 @@
package com.termux.terminal;
/**
* Native methods for creating and managing pseudoterminal subprocesses. C code is in jni/termux.c.
*/
final class JNI {
static {
System.loadLibrary("termux");
}
/**
* Create a subprocess. Differs from {@link ProcessBuilder} in that a pseudoterminal is used to communicate with the
* subprocess.
*
* Callers are responsible for calling {@link #close(int)} on the returned file descriptor.
*
* @param cmd
* The command to execute
* @param cwd
* The current working directory for the executed command
* @param args
* An array of arguments to the command
* @param envVars
* An array of strings of the form "VAR=value" to be added to the environment of the process
* @param processId
* A one-element array to which the process ID of the started process will be written.
* @return the file descriptor resulting from opening /dev/ptmx master device. The sub process will have opened the
* slave device counterpart (/dev/pts/$N) and have it as stdint, stdout and stderr.
*/
public static native int createSubprocess(String cmd, String cwd, String[] args, String[] envVars, int[] processId);
/** Set the window size for a given pty, which allows connected programs to learn how large their screen is. */
public static native void setPtyWindowSize(int fd, int rows, int cols);
/**
* Causes the calling thread to wait for the process associated with the receiver to finish executing.
*
* @return if >= 0, the exit status of the process. If < 0, the signal causing the process to stop negated.
*/
public static native int waitFor(int processId);
/**
* Send SIGHUP to a process group.
*
* There exists a kill(2) system call wrapper in {@link android.os.Process#sendSignal(int, int)}, but that makes a
* "if (pid > 0)" check so cannot be used for sending to a process group:
* https://android.googlesource.com/platform/frameworks/base/+/donut-release/core/jni/android_util_Process.cpp
*/
public static native void hangupProcessGroup(int processId);
/** Close a file descriptor through the close(2) system call. */
public static native void close(int fileDescriptor);
}

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,55 @@
package com.termux.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}.
*
* 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).
*/
public final class TextStyle {
public final static int CHARACTER_ATTRIBUTE_BOLD = 1;
public final static int CHARACTER_ATTRIBUTE_ITALIC = 1 << 1;
public final static int CHARACTER_ATTRIBUTE_UNDERLINE = 1 << 2;
public final static int CHARACTER_ATTRIBUTE_BLINK = 1 << 3;
public final static int CHARACTER_ATTRIBUTE_INVERSE = 1 << 4;
public final static int CHARACTER_ATTRIBUTE_INVISIBLE = 1 << 5;
public final static int CHARACTER_ATTRIBUTE_STRIKETHROUGH = 1 << 6;
/**
* The selective erase control functions (DECSED and DECSEL) can only erase characters defined as erasable.
*
* This bit is set if DECSCA (Select Character Protection Attribute) has been used to define the characters that
* come after it as erasable from the screen.
*/
public final static int CHARACTER_ATTRIBUTE_PROTECTED = 1 << 7;
/** Dim colors. Also known as faint or half intensity. */
public final static int CHARACTER_ATTRIBUTE_DIM = 1 << 8;
public final static int COLOR_INDEX_FOREGROUND = 256;
public final static int COLOR_INDEX_BACKGROUND = 257;
public final static int COLOR_INDEX_CURSOR = 258;
/** The 256 standard color entries and the three special (foreground, background and cursor) ones. */
public final static int NUM_INDEXED_COLORS = 259;
/** Normal foreground and background colors and no effects. */
final static int NORMAL = encode(COLOR_INDEX_FOREGROUND, COLOR_INDEX_BACKGROUND, 0);
static int encode(int foreColor, int backColor, int effect) {
return ((effect & 0b111111111) << 18) | ((foreColor & 0b111111111) << 9) | (backColor & 0b111111111);
}
public static int decodeForeColor(int encodedColor) {
return (encodedColor >> 9) & 0b111111111;
}
public static int decodeBackColor(int encodedColor) {
return encodedColor & 0b111111111;
}
public static int decodeEffect(int encodedColor) {
return (encodedColor >> 18) & 0b111111111;
}
}

View File

@@ -0,0 +1,108 @@
package com.termux.terminal;
/**
* wcwidth() implementation from http://git.musl-libc.org/cgit/musl/tree/src/ctype
*
* Modified to return 0 instead of -1.
*/
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,
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, 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,
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, 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, 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, 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, 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,
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,
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,
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, 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,
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,
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,
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,
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,
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,
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, 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,
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, 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, 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, };
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,
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, 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, 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, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16, 16, 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, 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,
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,
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, 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, 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, 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,
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, 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 };
/** Return the terminal display width of a code point: 0, 1 or 2. */
public static int width(int wc) {
if (wc < 0xff) return (wc + 1 & 0x7f) >= 0x21 ? 1 : (wc != 0) ? 0 : 0;
if ((wc & 0xfffeffff) < 0xfffe) {
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;
return 1;
}
if ((wc & 0xfffe) == 0xfffe) return 0;
if (wc - 0x20000 < 0x20000) return 2;
if (wc == 0xe0001 || wc - 0xe0020 < 0x5f || wc - 0xe0100 < 0xef) return 0;
return 1;
}
/** The width at an index position in a java char array. */
public static int width(char[] chars, int index) {
char c = chars[index];
return Character.isHighSurrogate(c) ? width(Character.toCodePoint(c, chars[index + 1])) : width(c);
}
}

View File

@@ -0,0 +1,100 @@
package com.termux.view;
import android.content.Context;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
/** A combination of {@link GestureDetector} and {@link ScaleGestureDetector}. */
public class GestureAndScaleRecognizer {
public interface Listener {
boolean onSingleTapUp(MotionEvent e);
boolean onDoubleTap(MotionEvent e);
boolean onScroll(MotionEvent e2, float dx, float dy);
boolean onFling(MotionEvent e, float velocityX, float velocityY);
boolean onScale(float focusX, float focusY, float scale);
boolean onDown(float x, float y);
boolean onUp(MotionEvent e);
void onLongPress(MotionEvent e);
}
private final GestureDetector mGestureDetector;
private final ScaleGestureDetector mScaleDetector;
final Listener mListener;
public GestureAndScaleRecognizer(Context context, Listener listener) {
mListener = listener;
mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float dx, float dy) {
return mListener.onScroll(e2, dx, dy);
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return mListener.onFling(e2, velocityX, velocityY);
}
@Override
public boolean onDown(MotionEvent e) {
return mListener.onDown(e.getX(), e.getY());
}
@Override
public void onLongPress(MotionEvent e) {
mListener.onLongPress(e);
}
}, null, true /* ignoreMultitouch */);
mGestureDetector.setOnDoubleTapListener(new GestureDetector.OnDoubleTapListener() {
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
return mListener.onSingleTapUp(e);
}
@Override
public boolean onDoubleTap(MotionEvent e) {
return mListener.onDoubleTap(e);
}
@Override
public boolean onDoubleTapEvent(MotionEvent e) {
return true;
}
});
mScaleDetector = new ScaleGestureDetector(context, new ScaleGestureDetector.SimpleOnScaleGestureListener() {
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
return true;
}
@Override
public boolean onScale(ScaleGestureDetector detector) {
return mListener.onScale(detector.getFocusX(), detector.getFocusY(), detector.getScaleFactor());
}
});
}
public void onTouchEvent(MotionEvent event) {
mGestureDetector.onTouchEvent(event);
mScaleDetector.onTouchEvent(event);
if (event.getAction() == MotionEvent.ACTION_UP) {
mListener.onUp(event);
}
}
public boolean isInProgress() {
return mScaleDetector.isInProgress();
}
}

View File

@@ -0,0 +1,20 @@
package com.termux.view;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
/**
* Input and scale listener which may be set on a {@link TerminalView} through
* {@link TerminalView#setOnKeyListener(TerminalKeyListener)}.
*/
public interface TerminalKeyListener {
/** Callback function on scale events according to {@link ScaleGestureDetector#getScaleFactor()}. */
float onScale(float scale);
void onLongPress(MotionEvent e);
/** On a single tap on the terminal if terminal mouse reporting not enabled. */
void onSingleTapUp(MotionEvent e);
}

View File

@@ -0,0 +1,232 @@
package com.termux.view;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.Typeface;
import com.termux.terminal.TerminalBuffer;
import com.termux.terminal.TerminalEmulator;
import com.termux.terminal.TerminalRow;
import com.termux.terminal.TextStyle;
import com.termux.terminal.WcWidth;
/**
* Renderer of a {@link TerminalEmulator} into a {@link Canvas}.
*
* Saves font metrics, so needs to be recreated each time the typeface or font size changes.
*/
final class TerminalRenderer {
final int mTextSize;
final Typeface mTypeface;
private final Paint mTextPaint = new Paint();
/** The width of a single mono spaced character obtained by {@link Paint#measureText(String)} on a single 'X'. */
final float mFontWidth;
/** The {@link Paint#getFontSpacing()}. See http://www.fampennings.nl/maarten/android/08numgrid/font.png */
final int mFontLineSpacing;
/** The {@link Paint#ascent()}. See http://www.fampennings.nl/maarten/android/08numgrid/font.png */
private final int mFontAscent;
/** The {@link #mFontLineSpacing} + {@link #mFontAscent}. */
final int mFontLineSpacingAndAscent;
private final float[] asciiMeasures = new float[127];
public TerminalRenderer(int textSize, Typeface typeface) {
mTextSize = textSize;
mTypeface = typeface;
mTextPaint.setTypeface(typeface);
mTextPaint.setAntiAlias(true);
mTextPaint.setTextSize(textSize);
mFontLineSpacing = (int) Math.ceil(mTextPaint.getFontSpacing());
mFontAscent = (int) Math.ceil(mTextPaint.ascent());
mFontLineSpacingAndAscent = mFontLineSpacing + mFontAscent;
mFontWidth = mTextPaint.measureText("X");
StringBuilder sb = new StringBuilder(" ");
for (int i = 0; i < asciiMeasures.length; i++) {
sb.setCharAt(0, (char) i);
asciiMeasures[i] = mTextPaint.measureText(sb, 0, 1);
}
}
/** 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) {
final boolean reverseVideo = mEmulator.isReverseVideo();
final int endRow = topRow + mEmulator.mRows;
final int columns = mEmulator.mColumns;
final int cursorCol = mEmulator.getCursorCol();
final int cursorRow = mEmulator.getCursorRow();
final boolean cursorVisible = mEmulator.isShowingCursor();
final TerminalBuffer screen = mEmulator.getScreen();
final int[] palette = mEmulator.mColors.mCurrentColors;
int fillColor = palette[reverseVideo ? TextStyle.COLOR_INDEX_FOREGROUND : TextStyle.COLOR_INDEX_BACKGROUND];
canvas.drawColor(fillColor, PorterDuff.Mode.SRC);
float heightOffset = mFontLineSpacingAndAscent;
for (int row = topRow; row < endRow; row++) {
heightOffset += mFontLineSpacing;
final int cursorX = (row == cursorRow && cursorVisible) ? cursorCol : -1;
int selx1 = -1, selx2 = -1;
if (row >= selectionY1 && row <= selectionY2) {
if (row == selectionY1) selx1 = selectionX1;
selx2 = (row == selectionY2) ? selectionX2 : mEmulator.mColumns;
}
TerminalRow lineObject = screen.allocateFullLineIfNecessary(screen.externalToInternalRow(row));
final char[] line = lineObject.mText;
int lastRunStyle = 0;
boolean lastRunInsideCursor = false;
int lastRunStartColumn = -1;
int lastRunStartIndex = 0;
boolean lastRunFontWidthMismatch = false;
int currentCharIndex = 0;
float measuredWidthForRun = 0.f;
for (int column = 0; column < columns;) {
final char charAtIndex = line[currentCharIndex];
final boolean charIsHighsurrogate = Character.isHighSurrogate(charAtIndex);
final int charsForCodePoint = charIsHighsurrogate ? 2 : 1;
final int codePoint = charIsHighsurrogate ? Character.toCodePoint(charAtIndex, line[currentCharIndex + 1]) : charAtIndex;
final int codePointWcWidth = WcWidth.width(codePoint);
final boolean insideCursor = (column >= selx1 && column <= selx2) || (cursorX == column || (codePointWcWidth == 2 && cursorX == column + 1));
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().
// 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.
// 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,
currentCharIndex, charsForCodePoint);
final boolean fontWidthMismatch = Math.abs(measuredCodePointWidth / mFontWidth - codePointWcWidth) > 0.01;
if (style != lastRunStyle || insideCursor != lastRunInsideCursor || fontWidthMismatch || lastRunFontWidthMismatch) {
if (column == 0) {
// Skip first column as there is nothing to draw, just record the current style.
} else {
final int columnWidthSinceLastRun = column - lastRunStartColumn;
final int charsSinceLastRun = currentCharIndex - lastRunStartIndex;
drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun, lastRunStartIndex, charsSinceLastRun,
measuredWidthForRun, lastRunInsideCursor, lastRunStyle, reverseVideo);
}
measuredWidthForRun = 0.f;
lastRunStyle = style;
lastRunInsideCursor = insideCursor;
lastRunStartColumn = column;
lastRunStartIndex = currentCharIndex;
lastRunFontWidthMismatch = fontWidthMismatch;
}
measuredWidthForRun += measuredCodePointWidth;
column += codePointWcWidth;
currentCharIndex += charsForCodePoint;
while (WcWidth.width(line, currentCharIndex) <= 0) {
// 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.
currentCharIndex += Character.isHighSurrogate(line[currentCharIndex]) ? 2 : 1;
}
}
final int columnWidthSinceLastRun = columns - lastRunStartColumn;
final int charsSinceLastRun = currentCharIndex - lastRunStartIndex;
drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun, lastRunStartIndex, charsSinceLastRun,
measuredWidthForRun, lastRunInsideCursor, lastRunStyle, reverseVideo);
}
}
/**
* @param canvas
* the canvas to render on
* @param palette
* the color palette to look up colors from textStyle
* @param y
* height offset into the canvas where to render the line: line * {@link #mFontLineSpacing}
* @param startColumn
* the run offset in columns
* @param runWidthColumns
* the run width in columns - this is computed from wcwidth() and may not be what the font measures to
* @param text
* the java char array to render text from
* @param startCharIndex
* index into the text array where to start
* @param runWidthChars
* number of java characters from the text array to render
* @param cursor
* true if rendering a cursor or selection
* @param textStyle
* 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;
boolean savedMatrix = false;
if (Math.abs(mes - runWidthColumns) > 0.01) {
canvas.save();
canvas.scale(runWidthColumns / mes, 1.f);
left *= mes / runWidthColumns;
right *= mes / runWidthColumns;
savedMatrix = true;
}
// Reverse video here if _one and only one_ of the reverse flags are set:
boolean reverseVideoHere = reverseVideo ^ (effect & (TextStyle.CHARACTER_ATTRIBUTE_INVERSE)) != 0;
// Switch if _one and only one_ of reverse video and cursor is set:
if (reverseVideoHere ^ cursor) {
int tmp = foreColor;
foreColor = backColor;
backColor = tmp;
}
if (backColor != TextStyle.COLOR_INDEX_BACKGROUND) {
// Only draw non-default background.
mTextPaint.setColor(palette[backColor]);
canvas.drawRect(left, y - mFontLineSpacingAndAscent + mFontAscent, right, y, mTextPaint);
}
if ((effect & TextStyle.CHARACTER_ATTRIBUTE_INVISIBLE) == 0) {
// Treat blink as bold:
final boolean bold = (effect & (TextStyle.CHARACTER_ATTRIBUTE_BOLD | TextStyle.CHARACTER_ATTRIBUTE_BLINK)) != 0;
final boolean underline = (effect & TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE) != 0;
final boolean italic = (effect & TextStyle.CHARACTER_ATTRIBUTE_ITALIC) != 0;
final boolean strikeThrough = (effect & TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH) != 0;
final boolean dim = (effect & TextStyle.CHARACTER_ATTRIBUTE_DIM) != 0;
int foreColorARGB = palette[foreColor];
if (dim) {
int red = (0xFF & (foreColorARGB >> 16));
int green = (0xFF & (foreColorARGB >> 8));
int blue = (0xFF & foreColorARGB);
// Dim color handling used by libvte which in turn took it from xterm
// (https://bug735245.bugzilla-attachments.gnome.org/attachment.cgi?id=284267):
red = red * 2 / 3;
green = green * 2 / 3;
blue = blue * 2 / 3;
foreColorARGB = 0xFF000000 + (red << 16) + (green << 8) + blue;
}
mTextPaint.setFakeBoldText(bold);
mTextPaint.setUnderlineText(underline);
mTextPaint.setTextSkewX(italic ? -0.35f : 0.f);
mTextPaint.setStrikeThruText(strikeThrough);
mTextPaint.setColor(foreColorARGB);
// The text alignment is the default Paint.Align.LEFT.
canvas.drawText(text, startCharIndex, runWidthChars, left, y - mFontLineSpacingAndAscent, mTextPaint);
}
if (savedMatrix) canvas.restore();
}
}

View File

@@ -0,0 +1,826 @@
package com.termux.view;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.Properties;
import com.termux.terminal.EmulatorDebug;
import com.termux.terminal.KeyHandler;
import com.termux.terminal.TerminalColors;
import com.termux.terminal.TerminalEmulator;
import com.termux.terminal.TerminalSession;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Typeface;
import android.text.InputType;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.HapticFeedbackConstants;
import android.view.InputDevice;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.widget.Scroller;
/** View displaying and interacting with a {@link TerminalSession}. */
public final class TerminalView extends View {
/** Log view key and IME events. */
private static final boolean LOG_KEY_EVENTS = false;
/** The currently displayed terminal session, whose emulator is {@link #mEmulator}. */
TerminalSession mTermSession;
/** Our terminal emulator whose session is {@link #mTermSession}. */
TerminalEmulator mEmulator;
TerminalRenderer mRenderer;
TerminalKeyListener mOnKeyListener;
/** The top row of text to display. Ranges from -activeTranscriptRows to 0. */
int mTopRow;
/** Keeping track of the special keys acting as Ctrl and Fn for the soft keyboard and other hardware keys. */
boolean mVirtualControlKeyDown, mVirtualFnKeyDown;
boolean mIsSelectingText = false;
int mSelXAnchor = -1, mSelYAnchor = -1;
int mSelX1 = -1, mSelX2 = -1, mSelY1 = -1, mSelY2 = -1;
float mScaleFactor = 1.f;
final GestureAndScaleRecognizer mGestureRecognizer;
/** Keep track of where mouse touch event started which we report as mouse scroll. */
private int mMouseScrollStartX = -1, mMouseScrollStartY = -1;
/** Keep track of the time when a touch event leading to sending mouse scroll events started. */
private long mMouseStartDownTime = -1;
final Scroller mScroller;
/** What was left in from scrolling movement. */
float mScrollRemainder;
/** If non-zero, this is the last unicode code point received if that was a combining character. */
int mCombiningAccent;
public TerminalView(Context context, AttributeSet attributes) { // NO_UCD (unused code)
super(context, attributes);
mGestureRecognizer = new GestureAndScaleRecognizer(context, new GestureAndScaleRecognizer.Listener() {
@Override
public boolean onUp(MotionEvent e) {
mScrollRemainder = 0.0f;
if (mEmulator != null && mEmulator.isMouseTrackingActive()) {
// Quick event processing when mouse tracking is active - do not wait for check of double tapping
// for zooming.
sendMouseEventCode(e, TerminalEmulator.MOUSE_LEFT_BUTTON, true);
sendMouseEventCode(e, TerminalEmulator.MOUSE_LEFT_BUTTON, false);
return true;
}
return false;
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
if (mEmulator == null) return true;
requestFocus();
if (!mEmulator.isMouseTrackingActive()) {
if (!e.isFromSource(InputDevice.SOURCE_MOUSE)) {
mOnKeyListener.onSingleTapUp(e);
return true;
}
}
return false;
}
@Override
public boolean onScroll(MotionEvent e2, float distanceX, float distanceY) {
if (mEmulator == null) return true;
if (mEmulator.isMouseTrackingActive() && e2.isFromSource(InputDevice.SOURCE_MOUSE)) {
// If moving with mouse pointer while pressing button, report that instead of scroll.
// This means that we never report moving with button press-events for touch input,
// since we cannot just start sending these events without a starting press event,
// which we do not do for touch input, only mouse in onTouchEvent().
sendMouseEventCode(e2, TerminalEmulator.MOUSE_LEFT_BUTTON_MOVED, true);
} else {
distanceY += mScrollRemainder;
int deltaRows = (int) (distanceY / mRenderer.mFontLineSpacing);
mScrollRemainder = distanceY - deltaRows * mRenderer.mFontLineSpacing;
doScroll(e2, deltaRows);
}
return true;
}
@Override
public boolean onScale(float focusX, float focusY, float scale) {
mScaleFactor *= scale;
mScaleFactor = mOnKeyListener.onScale(mScaleFactor);
return true;
}
@Override
public boolean onFling(final MotionEvent e2, float velocityX, float velocityY) {
if (mEmulator == null) return true;
// Do not start scrolling until last fling has been taken care of:
if (!mScroller.isFinished()) return true;
final boolean mouseTrackingAtStartOfFling = mEmulator.isMouseTrackingActive();
float SCALE = 0.25f;
if (mouseTrackingAtStartOfFling) {
mScroller.fling(0, 0, 0, -(int) (velocityY * SCALE), 0, 0, -mEmulator.mRows / 2, mEmulator.mRows / 2);
} else {
mScroller.fling(0, mTopRow, 0, -(int) (velocityY * SCALE), 0, 0, -mEmulator.getScreen().getActiveTranscriptRows(), 0);
}
post(new Runnable() {
private int mLastY = 0;
@Override
public void run() {
if (mouseTrackingAtStartOfFling != mEmulator.isMouseTrackingActive()) {
mScroller.abortAnimation();
return;
}
if (mScroller.isFinished()) return;
boolean more = mScroller.computeScrollOffset();
int newY = mScroller.getCurrY();
int diff = mouseTrackingAtStartOfFling ? (newY - mLastY) : (newY - mTopRow);
doScroll(e2, diff);
mLastY = newY;
if (more) post(this);
}
});
return true;
}
@Override
public boolean onDown(float x, float y) {
return false;
}
@Override
public boolean onDoubleTap(MotionEvent e) {
// Do not treat is as a single confirmed tap - it may be followed by zoom.
return false;
}
@Override
public void onLongPress(MotionEvent e) {
if (mEmulator != null && !mGestureRecognizer.isInProgress()) {
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
mOnKeyListener.onLongPress(e);
}
}
});
mScroller = new Scroller(context);
}
/**
* @param onKeyListener
* Listener for all kinds of key events, both hardware and IME (which makes it different from that
* available with {@link View#setOnKeyListener(OnKeyListener)}.
*/
public void setOnKeyListener(TerminalKeyListener onKeyListener) {
this.mOnKeyListener = onKeyListener;
}
/**
* Attach a {@link TerminalSession} to this view.
*
* @param session
* The {@link TerminalSession} this view will be displaying.
*/
public boolean attachSession(TerminalSession session) {
if (session == mTermSession) return false;
mTopRow = 0;
mTermSession = session;
mEmulator = null;
mCombiningAccent = 0;
updateSize();
// Wait with enabling the scrollbar until we have a terminal to get scroll position from.
setVerticalScrollBarEnabled(true);
return true;
}
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
// Make the IME run in a limited "generate key events" mode.
//
// If using just "TYPE_NULL", there is a problem with the "Google Pinyin Input" being in
// word mode when used with the "En" tab available when the "Show English keyboard" option
// is enabled - see https://github.com/termux/termux-packages/issues/25.
//
// Adding TYPE_TEXT_FLAG_NO_SUGGESTIONS fixes Pinyin Input, put causes Swype to be put in
// word mode... Using TYPE_TEXT_VARIATION_VISIBLE_PASSWORD fixes that.
//
// So a bit messy. If this gets too messy it's perhaps best resolved by reverting back to just
// "TYPE_NULL" and let the Pinyin Input english keyboard be in word mode.
outAttrs.inputType = InputType.TYPE_NULL | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD;
// Let part of the application show behind when in landscape:
outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_FULLSCREEN;
return new BaseInputConnection(this, true) {
@Override
public boolean beginBatchEdit() {
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "IME: beginBatchEdit()");
return true;
}
@Override
public boolean clearMetaKeyStates(int states) {
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "IME: clearMetaKeyStates(" + states + ")");
return true;
}
@Override
public boolean endBatchEdit() {
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "IME: endBatchEdit()");
return false;
}
@Override
public boolean finishComposingText() {
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "IME: finishComposingText()");
return true;
}
@Override
public int getCursorCapsMode(int reqModes) {
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "IME: getCursorCapsMode(" + reqModes + ")");
int mode = 0;
if ((reqModes & TextUtils.CAP_MODE_CHARACTERS) != 0) {
mode |= TextUtils.CAP_MODE_CHARACTERS;
}
return mode;
}
@Override
public CharSequence getTextAfterCursor(int n, int flags) {
return "";
}
@Override
public CharSequence getTextBeforeCursor(int n, int flags) {
return "";
}
@Override
public boolean commitText(CharSequence text, int newCursorPosition) {
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "IME: commitText(\"" + text + "\", " + newCursorPosition + ")");
if (mEmulator == null) return true;
final int textLengthInChars = text.length();
for (int i = 0; i < textLengthInChars; i++) {
char firstChar = text.charAt(i);
int codePoint;
if (Character.isHighSurrogate(firstChar)) {
if (++i < textLengthInChars) {
codePoint = Character.toCodePoint(firstChar, text.charAt(i));
} else {
// At end of string, with no low surrogate following the high:
codePoint = TerminalEmulator.UNICODE_REPLACEMENT_CHAR;
}
} else {
codePoint = firstChar;
}
inputCodePoint(codePoint, false, false);
}
return true;
}
@Override
public boolean deleteSurroundingText(int leftLength, int rightLength) {
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "IME: deleteSurroundingText(" + leftLength + ", " + rightLength + ")");
// Swype keyboard sometimes(?) sends this on backspace:
if (leftLength == 0 && rightLength == 0) leftLength = 1;
for (int i = 0; i < leftLength; i++)
sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));
return true;
}
};
}
@Override
protected int computeVerticalScrollRange() {
return mEmulator == null ? 1 : mEmulator.getScreen().getActiveRows();
}
@Override
protected int computeVerticalScrollExtent() {
return mEmulator == null ? 1 : mEmulator.mRows;
}
@Override
protected int computeVerticalScrollOffset() {
return mEmulator == null ? 1 : mEmulator.getScreen().getActiveRows() + mTopRow - mEmulator.mRows;
}
public void onScreenUpdated() {
if (mEmulator == null) return;
if (mIsSelectingText) {
int rowShift = mEmulator.getScrollCounter();
mSelY1 -= rowShift;
mSelY2 -= rowShift;
mSelYAnchor -= rowShift;
}
mEmulator.clearScrollCounter();
if (mTopRow != 0) {
// Scroll down if not already there.
mTopRow = 0;
scrollTo(0, 0);
}
invalidate();
}
/**
* Sets the text size, which in turn sets the number of rows and columns.
*
* @param textSize
* the new font size, in density-independent pixels.
*/
public void setTextSize(int textSize) {
mRenderer = new TerminalRenderer(textSize, mRenderer == null ? Typeface.MONOSPACE : mRenderer.mTypeface);
updateSize();
}
@Override
public boolean onCheckIsTextEditor() {
return true;
}
@Override
public boolean isOpaque() {
return true;
}
/** Send a single mouse event code to the terminal. */
void sendMouseEventCode(MotionEvent e, int button, boolean pressed) {
int x = (int) (e.getX() / mRenderer.mFontWidth) + 1;
int y = (int) ((e.getY() - mRenderer.mFontLineSpacingAndAscent) / mRenderer.mFontLineSpacing) + 1;
if (pressed && (button == TerminalEmulator.MOUSE_WHEELDOWN_BUTTON || button == TerminalEmulator.MOUSE_WHEELUP_BUTTON)) {
if (mMouseStartDownTime == e.getDownTime()) {
x = mMouseScrollStartX;
y = mMouseScrollStartY;
} else {
mMouseStartDownTime = e.getDownTime();
mMouseScrollStartX = x;
mMouseScrollStartY = y;
}
}
mEmulator.sendMouseEvent(button, x, y, pressed);
}
/** Perform a scroll, either from dragging the screen or by scrolling a mouse wheel. */
void doScroll(MotionEvent event, int rowsDown) {
boolean up = rowsDown < 0;
int amount = Math.abs(rowsDown);
for (int i = 0; i < amount; i++) {
if (mEmulator.isMouseTrackingActive()) {
sendMouseEventCode(event, up ? TerminalEmulator.MOUSE_WHEELUP_BUTTON : TerminalEmulator.MOUSE_WHEELDOWN_BUTTON, true);
} else if (mEmulator.isAlternateBufferActive()) {
// Send up and down key events for scrolling, which is what some terminals do to make scroll work in
// e.g. less, which shifts to the alt screen without mouse handling.
handleKeyCode(up ? KeyEvent.KEYCODE_DPAD_UP : KeyEvent.KEYCODE_DPAD_DOWN, 0);
} else {
mTopRow = Math.min(0, Math.max(-(mEmulator.getScreen().getActiveTranscriptRows()), mTopRow + (up ? -1 : 1)));
if (!awakenScrollBars()) invalidate();
}
}
}
/** Overriding {@link View#onGenericMotionEvent(MotionEvent)}. */
@Override
public boolean onGenericMotionEvent(MotionEvent event) {
if (mEmulator != null && event.isFromSource(InputDevice.SOURCE_MOUSE) && event.getAction() == MotionEvent.ACTION_SCROLL) {
// Handle mouse wheel scrolling.
boolean up = event.getAxisValue(MotionEvent.AXIS_VSCROLL) > 0.0f;
doScroll(event, up ? -3 : 3);
return true;
}
return false;
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (mEmulator == null) return true;
final boolean eventFromMouse = ev.isFromSource(InputDevice.SOURCE_MOUSE);
final int action = ev.getAction();
if (eventFromMouse) {
if ((ev.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) {
if (action == MotionEvent.ACTION_DOWN) showContextMenu();
return true;
} else if (mEmulator.isMouseTrackingActive() && (ev.getAction() == MotionEvent.ACTION_DOWN || ev.getAction() == MotionEvent.ACTION_UP)) {
sendMouseEventCode(ev, TerminalEmulator.MOUSE_LEFT_BUTTON, ev.getAction() == MotionEvent.ACTION_DOWN);
return true;
} else if (!mEmulator.isMouseTrackingActive() && action == MotionEvent.ACTION_DOWN) {
// Start text selection with mouse. Note that the check against MotionEvent.ACTION_DOWN is
// important, since we otherwise would pick up secondary mouse button up actions.
mIsSelectingText = true;
}
} else if (!mIsSelectingText) {
mGestureRecognizer.onTouchEvent(ev);
return true;
}
if (mIsSelectingText) {
int cx = (int) (ev.getX() / mRenderer.mFontWidth);
// Offset for finger:
final int SELECT_TEXT_OFFSET_Y = eventFromMouse ? 0 : -40;
int cy = (int) ((ev.getY() + SELECT_TEXT_OFFSET_Y) / mRenderer.mFontLineSpacing) + mTopRow;
switch (action) {
case MotionEvent.ACTION_DOWN:
mSelXAnchor = cx;
mSelYAnchor = cy;
mSelX1 = cx;
mSelY1 = cy;
mSelX2 = mSelX1;
mSelY2 = mSelY1;
invalidate();
break;
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_UP:
boolean touchBeforeAnchor = (cy < mSelYAnchor || (cy == mSelYAnchor && cx < mSelXAnchor));
int minx = touchBeforeAnchor ? cx : mSelXAnchor;
int maxx = !touchBeforeAnchor ? cx : mSelXAnchor;
int miny = touchBeforeAnchor ? cy : mSelYAnchor;
int maxy = !touchBeforeAnchor ? cy : mSelYAnchor;
mSelX1 = minx;
mSelY1 = miny;
mSelX2 = maxx;
mSelY2 = maxy;
if (action == MotionEvent.ACTION_UP) {
String selectedText = mEmulator.getSelectedText(mSelX1, mSelY1, mSelX2, mSelY2).trim();
mTermSession.clipboardText(selectedText);
toggleSelectingText();
}
invalidate();
break;
default:
toggleSelectingText();
invalidate();
break;
}
return true;
}
return false;
}
@Override
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "onKeyPreIme(keyCode=" + keyCode + ", event=" + event + ")");
if (keyCode == KeyEvent.KEYCODE_ESCAPE || keyCode == KeyEvent.KEYCODE_BACK) {
// Handle the escape key ourselves to avoid the system from treating it as back key
// and e.g. close keyboard.
switch (event.getAction()) {
case KeyEvent.ACTION_DOWN:
return onKeyDown(keyCode, event);
case KeyEvent.ACTION_UP:
return onKeyUp(keyCode, event);
}
}
return super.onKeyPreIme(keyCode, event);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "onKeyDown(keyCode=" + keyCode + ", isSystem()=" + event.isSystem() + ", event=" + event + ")");
if (mEmulator == null) return true;
int metaState = event.getMetaState();
boolean controlDownFromEvent = event.isCtrlPressed();
boolean leftAltDownFromEvent = (metaState & KeyEvent.META_ALT_LEFT_ON) != 0;
boolean rightAltDownFromEvent = (metaState & KeyEvent.META_ALT_RIGHT_ON) != 0;
if (handleVirtualKeys(keyCode, event, true)) {
invalidate();
return true;
} else if (event.isSystem() && keyCode != KeyEvent.KEYCODE_BACK) {
return super.onKeyDown(keyCode, event);
}
int keyMod = 0;
if (controlDownFromEvent) keyMod |= KeyHandler.KEYMOD_CTRL;
if (event.isAltPressed()) keyMod |= KeyHandler.KEYMOD_ALT;
if (event.isShiftPressed()) keyMod |= KeyHandler.KEYMOD_SHIFT;
if (handleKeyCode(keyCode, keyMod)) {
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "handleKeyCode() took key event");
return true;
}
// Clear Ctrl since we handle that ourselves:
int bitsToClear = KeyEvent.META_CTRL_MASK;
if (rightAltDownFromEvent) {
// Let right Alt/Alt Gr be used to compose characters.
} else {
// Use left alt to send to terminal (e.g. Left Alt+B to jump back a word), so remove:
bitsToClear |= KeyEvent.META_ALT_ON | KeyEvent.META_ALT_LEFT_ON;
}
int effectiveMetaState = event.getMetaState() & ~bitsToClear;
int result = event.getUnicodeChar(effectiveMetaState);
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "KeyEvent#getUnicodeChar(" + effectiveMetaState + ") returned: " + result);
if (result == 0) {
return true;
}
int oldCombiningAccent = mCombiningAccent;
if ((result & KeyCharacterMap.COMBINING_ACCENT) != 0) {
// If entered combining accent previously, write it out:
if (mCombiningAccent != 0) inputCodePoint(mCombiningAccent, controlDownFromEvent, leftAltDownFromEvent);
mCombiningAccent = result & KeyCharacterMap.COMBINING_ACCENT_MASK;
} else {
if (mCombiningAccent != 0) {
int combinedChar = KeyCharacterMap.getDeadChar(mCombiningAccent, result);
if (combinedChar > 0) result = combinedChar;
mCombiningAccent = 0;
}
inputCodePoint(result, controlDownFromEvent, leftAltDownFromEvent);
}
if (mCombiningAccent != oldCombiningAccent) invalidate();
return true;
}
void inputCodePoint(int codePoint, boolean controlDownFromEvent, boolean leftAltDownFromEvent) {
if (LOG_KEY_EVENTS) {
Log.i(EmulatorDebug.LOG_TAG, "inputCodePoint(codePoint=" + codePoint + ", controlDownFromEvent=" + controlDownFromEvent + ", leftAltDownFromEvent="
+ leftAltDownFromEvent + ")");
}
int resultingKeyCode = -1; // Set if virtual key causes this to be translated to key event.
if (controlDownFromEvent || mVirtualControlKeyDown) {
if (codePoint >= 'a' && codePoint <= 'z') {
codePoint = codePoint - 'a' + 1;
} else if (codePoint >= 'A' && codePoint <= 'Z') {
codePoint = codePoint - 'A' + 1;
} else if (codePoint == ' ' || codePoint == '2') {
codePoint = 0;
} else if (codePoint == '[' || codePoint == '3') {
codePoint = 27; // ^[ (Esc)
} else if (codePoint == '\\' || codePoint == '4') {
codePoint = 28;
} else if (codePoint == ']' || codePoint == '5') {
codePoint = 29;
} else if (codePoint == '^' || codePoint == '6') {
codePoint = 30; // control-^
} else if (codePoint == '_' || codePoint == '7') {
codePoint = 31;
} else if (codePoint == '8') {
codePoint = 127; // DEL
} else if (codePoint == '9') {
resultingKeyCode = KeyEvent.KEYCODE_F11;
} else if (codePoint == '0') {
resultingKeyCode = KeyEvent.KEYCODE_F12;
}
} else if (mVirtualFnKeyDown) {
if (codePoint == 'w' || codePoint == 'W') {
resultingKeyCode = KeyEvent.KEYCODE_DPAD_UP;
} else if (codePoint == 'a' || codePoint == 'A') {
resultingKeyCode = KeyEvent.KEYCODE_DPAD_LEFT;
} else if (codePoint == 's' || codePoint == 'S') {
resultingKeyCode = KeyEvent.KEYCODE_DPAD_DOWN;
} else if (codePoint == 'd' || codePoint == 'D') {
resultingKeyCode = KeyEvent.KEYCODE_DPAD_RIGHT;
} else if (codePoint == 'p' || codePoint == 'P') {
resultingKeyCode = KeyEvent.KEYCODE_PAGE_UP;
} else if (codePoint == 'n' || codePoint == 'N') {
resultingKeyCode = KeyEvent.KEYCODE_PAGE_DOWN;
} else if (codePoint == 't' || codePoint == 'T') {
resultingKeyCode = KeyEvent.KEYCODE_TAB;
} else if (codePoint == 'l' || codePoint == 'L') {
codePoint = '|';
} else if (codePoint == 'u' || codePoint == 'U') {
codePoint = '_';
} else if (codePoint == 'e' || codePoint == 'E') {
codePoint = 27; // ^[ (Esc)
} else if (codePoint == '.') {
codePoint = 28; // ^\
} else if (codePoint > '0' && codePoint <= '9') {
// F1-F9
resultingKeyCode = (codePoint - '1') + KeyEvent.KEYCODE_F1;
} else if (codePoint == '0') {
resultingKeyCode = KeyEvent.KEYCODE_F10;
} else if (codePoint == 'i' || codePoint == 'I') {
resultingKeyCode = KeyEvent.KEYCODE_INSERT;
} else if (codePoint == 'x' || codePoint == 'X') {
resultingKeyCode = KeyEvent.KEYCODE_FORWARD_DEL;
} else if (codePoint == 'h' || codePoint == 'H') {
resultingKeyCode = KeyEvent.KEYCODE_MOVE_HOME;
} else if (codePoint == 'f' || codePoint == 'F') {
// As left alt+f, jumping forward in readline:
codePoint = 'f';
leftAltDownFromEvent = true;
} else if (codePoint == 'b' || codePoint == 'B') {
// As left alt+b, jumping forward in readline:
codePoint = 'b';
leftAltDownFromEvent = true;
}
}
if (codePoint > -1) {
if (resultingKeyCode > -1) {
handleKeyCode(resultingKeyCode, 0);
} else {
// The below two workarounds are needed on at least Logitech Keyboard k810 on Samsung Galaxy Tab Pro
// (Android 4.4) with the stock Samsung Keyboard. They should be harmless when not used since the need
// to input the original characters instead of the new ones using the keyboard should be low.
// Rewrite U+02DC 'SMALL TILDE' to U+007E 'TILDE' for ~ to work in shells:
if (codePoint == 0x02DC) codePoint = 0x07E;
// Rewrite U+02CB 'MODIFIER LETTER GRAVE ACCENT' to U+0060 'GRAVE ACCENT' for ` (backticks) to work:
if (codePoint == 0x02CB) codePoint = 0x60;
// If left alt, send escape before the code point to make e.g. Alt+B and Alt+F work in readline:
mTermSession.writeCodePoint(leftAltDownFromEvent, codePoint);
}
}
}
/** Input the specified keyCode if applicable and return if the input was consumed. */
public boolean handleKeyCode(int keyCode, int keyMod) {
TerminalEmulator term = mTermSession.getEmulator();
String code = KeyHandler.getCode(keyCode, keyMod, term.isCursorKeysApplicationMode(), term.isKeypadApplicationMode());
if (code == null) return false;
mTermSession.write(code);
return true;
}
/**
* Called when a key is released in the view.
*
* @param keyCode
* The keycode of the key which was released.
* @param event
* A {@link KeyEvent} describing the event.
* @return Whether the event was handled.
*/
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "onKeyUp(keyCode=" + keyCode + ", event=" + event + ")");
if (mEmulator == null) return true;
if (handleVirtualKeys(keyCode, event, false)) {
invalidate();
return true;
} else if (event.isSystem()) {
// Let system key events through.
return super.onKeyUp(keyCode, event);
}
return true;
}
/** Handle dedicated volume buttons as virtual keys if applicable. */
private boolean handleVirtualKeys(int keyCode, KeyEvent event, boolean down) {
InputDevice inputDevice = event.getDevice();
if (inputDevice != null && inputDevice.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC) {
// Do not steal dedicated buttons from a full external keyboard.
return false;
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "handleVirtualKeys(down=" + down + ") taking ctrl event");
mVirtualControlKeyDown = down;
return true;
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "handleVirtualKeys(down=" + down + ") taking Fn event");
mVirtualFnKeyDown = down;
return true;
}
return false;
}
public void checkForTypeface() {
new Thread() {
@Override
public void run() {
try {
File fontFile = new File(getContext().getFilesDir().getPath() + "/home/.termux/font.ttf");
final Typeface newTypeface = fontFile.exists() ? Typeface.createFromFile(fontFile) : Typeface.MONOSPACE;
if (newTypeface != mRenderer.mTypeface) {
((Activity) getContext()).runOnUiThread(new Runnable() {
@Override
public void run() {
try {
mRenderer = new TerminalRenderer(mRenderer.mTextSize, newTypeface);
updateSize();
invalidate();
} catch (Exception e) {
Log.e(EmulatorDebug.LOG_TAG, "Error loading font", e);
}
}
});
}
} catch (Exception e) {
Log.e(EmulatorDebug.LOG_TAG, "Error loading font", e);
}
}
}.start();
}
public void checkForColors() {
new Thread() {
@Override
public void run() {
try {
File colorsFile = new File(getContext().getFilesDir().getPath() + "/home/.termux/colors.properties");
final Properties props = colorsFile.isFile() ? new Properties() : null;
if (props != null) {
try (InputStream in = new FileInputStream(colorsFile)) {
props.load(in);
}
}
((Activity) getContext()).runOnUiThread(new Runnable() {
@Override
public void run() {
try {
if (props == null) {
TerminalColors.COLOR_SCHEME.reset();
} else {
TerminalColors.COLOR_SCHEME.updateWith(props);
}
if (mEmulator != null) mEmulator.mColors.reset();
invalidate();
} catch (Exception e) {
Log.e(EmulatorDebug.LOG_TAG, "Setting colors failed: " + e.getMessage());
}
}
});
} catch (Exception e) {
Log.e(EmulatorDebug.LOG_TAG, "Failed colors handling", e);
}
}
}.start();
}
/**
* This is called during layout when the size of this view has changed. If you were just added to the view
* hierarchy, you're called with the old values of 0.
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
updateSize();
}
/** Check if the terminal size in rows and columns should be updated. */
public void updateSize() {
int viewWidth = getWidth();
int viewHeight = getHeight();
if (viewWidth == 0 || viewHeight == 0 || mTermSession == null) return;
// Set to 80 and 24 if you want to enable vttest.
int newColumns = Math.max(8, (int) (viewWidth / mRenderer.mFontWidth));
int newRows = Math.max(8, (viewHeight - mRenderer.mFontLineSpacingAndAscent) / mRenderer.mFontLineSpacing);
if (mEmulator == null || (newColumns != mEmulator.mColumns || newRows != mEmulator.mRows)) {
mTermSession.updateSize(newColumns, newRows);
mEmulator = mTermSession.getEmulator();
mTopRow = 0;
scrollTo(0, 0);
invalidate();
}
}
@Override
protected void onDraw(Canvas canvas) {
if (mEmulator == null) {
canvas.drawColor(0XFF000000);
} else {
mRenderer.render(mEmulator, canvas, mTopRow, mSelY1, mSelY2, mSelX1, mSelX2);
}
}
/** Toggle text selection mode in the view. */
public void toggleSelectingText() {
mIsSelectingText = !mIsSelectingText;
if (!mIsSelectingText) mSelX1 = mSelY1 = mSelX2 = mSelY2 = -1;
}
public TerminalSession getCurrentSession() {
return mTermSession;
}
}

View File

@@ -0,0 +1,5 @@
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE:= libtermux
LOCAL_SRC_FILES:= termux.c
include $(BUILD_SHARED_LIBRARY)

View File

@@ -0,0 +1,5 @@
APP_ABI := armeabi-v7a x86
APP_PLATFORM := android-21
NDK_TOOLCHAIN_VERSION := 4.9
APP_CFLAGS := -std=c11 -Wall -Wextra -Os -fno-stack-protector
APP_LDFLAGS = -nostdlib -Wl,--gc-sections

203
app/src/main/jni/termux.c Normal file
View File

@@ -0,0 +1,203 @@
#include <dirent.h>
#include <fcntl.h>
#include <jni.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <termios.h>
#include <unistd.h>
#define TERMUX_UNUSED(x) x __attribute__((__unused__))
#ifdef __APPLE__
# define LACKS_PTSNAME_R
#endif
static int throw_runtime_exception(JNIEnv* env, char const* message)
{
jclass exClass = (*env)->FindClass(env, "java/lang/RuntimeException");
(*env)->ThrowNew(env, exClass, message);
return -1;
}
static int create_subprocess(JNIEnv* env, char const* cmd, char const* cwd, char* const argv[], char** envp, int* pProcessId)
{
int ptm = open("/dev/ptmx", O_RDWR | O_CLOEXEC);
if (ptm < 0) return throw_runtime_exception(env, "Cannot open /dev/ptmx");
#ifdef LACKS_PTSNAME_R
char* devname;
#else
char devname[64];
#endif
if (grantpt(ptm) || unlockpt(ptm) ||
#ifdef LACKS_PTSNAME_R
(devname = ptsname(ptm)) == NULL
#else
ptsname_r(ptm, devname, sizeof(devname))
#endif
) {
return throw_runtime_exception(env, "Cannot grantpt()/unlockpt()/ptsname_r() on /dev/ptmx");
}
// Enable UTF-8 mode and disable flow control to prevent Ctrl+S from locking up the display.
struct termios tios;
tcgetattr(ptm, &tios);
tios.c_iflag |= IUTF8;
tios.c_iflag &= ~(IXON | IXOFF);
tcsetattr(ptm, TCSANOW, &tios);
/** Set initial winsize (better too small than too large). */
struct winsize sz = { .ws_row = 20, .ws_col = 20 };
ioctl(ptm, TIOCSWINSZ, &sz);
pid_t pid = fork();
if (pid < 0) {
return throw_runtime_exception(env, "Fork failed");
} else if (pid > 0) {
*pProcessId = (int) pid;
return ptm;
} else {
// Clear signals which the Android java process may have blocked:
sigset_t signals_to_unblock;
sigfillset(&signals_to_unblock);
sigprocmask(SIG_UNBLOCK, &signals_to_unblock, 0);
close(ptm);
setsid();
int pts = open(devname, O_RDWR);
if (pts < 0) exit(-1);
dup2(pts, 0);
dup2(pts, 1);
dup2(pts, 2);
DIR* self_dir = opendir("/proc/self/fd");
if (self_dir != NULL) {
int self_dir_fd = dirfd(self_dir);
struct dirent* entry;
while ((entry = readdir(self_dir)) != NULL) {
int fd = atoi(entry->d_name);
if(fd > 2 && fd != self_dir_fd) close(fd);
}
closedir(self_dir);
}
clearenv();
if (envp) for (; *envp; ++envp) putenv(*envp);
if (chdir(cwd) != 0) {
char* error_message;
// No need to free asprintf()-allocated memory since doing execvp() or exit() below.
if (asprintf(&error_message, "chdir(\"%s\")", cwd) == -1) error_message = "chdir()";
perror(error_message);
fflush(stderr);
}
execvp(cmd, argv);
// Show terminal output about failing exec() call:
char* error_message;
if (asprintf(&error_message, "exec(\"%s\")", cmd) == -1) error_message = "exec()";
perror(error_message);
_exit(1);
}
}
JNIEXPORT jint JNICALL Java_com_termux_terminal_JNI_createSubprocess(JNIEnv* env, jclass TERMUX_UNUSED(clazz), jstring cmd, jstring cwd, jobjectArray args, jobjectArray envVars, jintArray processIdArray)
{
jsize size = args ? (*env)->GetArrayLength(env, args) : 0;
char** argv = NULL;
if (size > 0) {
argv = (char**) malloc((size + 1) * sizeof(char*));
if (!argv) return throw_runtime_exception(env, "Couldn't allocate argv array");
for (int i = 0; i < size; ++i) {
jstring arg_java_string = (jstring) (*env)->GetObjectArrayElement(env, args, i);
char const* arg_utf8 = (*env)->GetStringUTFChars(env, arg_java_string, NULL);
if (!arg_utf8) return throw_runtime_exception(env, "GetStringUTFChars() failed for argv");
argv[i] = strdup(arg_utf8);
(*env)->ReleaseStringUTFChars(env, arg_java_string, arg_utf8);
}
argv[size] = NULL;
}
size = envVars ? (*env)->GetArrayLength(env, envVars) : 0;
char** envp = NULL;
if (size > 0) {
envp = (char**) malloc((size + 1) * sizeof(char *));
if (!envp) return throw_runtime_exception(env, "malloc() for envp array failed");
for (int i = 0; i < size; ++i) {
jstring env_java_string = (jstring) (*env)->GetObjectArrayElement(env, envVars, i);
char const* env_utf8 = (*env)->GetStringUTFChars(env, env_java_string, 0);
if (!env_utf8) return throw_runtime_exception(env, "GetStringUTFChars() failed for env");
envp[i] = strdup(env_utf8);
(*env)->ReleaseStringUTFChars(env, env_java_string, env_utf8);
}
envp[size] = NULL;
}
int procId = 0;
char const* cmd_cwd = (*env)->GetStringUTFChars(env, cwd, NULL);
char const* cmd_utf8 = (*env)->GetStringUTFChars(env, cmd, NULL);
int ptm = create_subprocess(env, cmd_utf8, cmd_cwd, argv, envp, &procId);
(*env)->ReleaseStringUTFChars(env, cmd, cmd_utf8);
(*env)->ReleaseStringUTFChars(env, cmd, cmd_cwd);
if (argv) {
for (char** tmp = argv; *tmp; ++tmp) free(*tmp);
free(argv);
}
if (envp) {
for (char** tmp = envp; *tmp; ++tmp) free(*tmp);
free(envp);
}
int* pProcId = (int*) (*env)->GetPrimitiveArrayCritical(env, processIdArray, NULL);
if (!pProcId) return throw_runtime_exception(env, "JNI call GetPrimitiveArrayCritical(processIdArray, &isCopy) failed");
*pProcId = procId;
(*env)->ReleasePrimitiveArrayCritical(env, processIdArray, pProcId, 0);
return ptm;
}
JNIEXPORT void JNICALL Java_com_termux_terminal_JNI_setPtyWindowSize(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint fd, jint rows, jint cols)
{
struct winsize sz = { .ws_row = rows, .ws_col = cols };
ioctl(fd, TIOCSWINSZ, &sz);
}
JNIEXPORT void JNICALL Java_com_termux_terminal_JNI_setPtyUTF8Mode(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint fd)
{
struct termios tios;
tcgetattr(fd, &tios);
if ((tios.c_iflag & IUTF8) == 0) {
tios.c_iflag |= IUTF8;
tcsetattr(fd, TCSANOW, &tios);
}
}
JNIEXPORT int JNICALL Java_com_termux_terminal_JNI_waitFor(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint pid)
{
int status;
waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
return WEXITSTATUS(status);
} else if (WIFSIGNALED(status)) {
return -WTERMSIG(status);
} else {
// Should never happen - waitpid(2) says "One of the first three macros will evaluate to a non-zero (true) value".
return 0;
}
}
JNIEXPORT void JNICALL Java_com_termux_terminal_JNI_hangupProcessGroup(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint procId)
{
killpg(procId, SIGHUP);
}
JNIEXPORT void JNICALL Java_com_termux_terminal_JNI_close(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint fileDescriptor)
{
close(fileDescriptor);
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 786 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 779 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 983 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="#E0E0E0" />
</shape>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8" ?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_activated="true" android:drawable="@drawable/current_session"/>
<item android:state_activated="false" android:drawable="@drawable/session_ripple"/>
</selector>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@android:color/darker_gray" >
<item>
<color android:color="@android:color/white" />
</item>
</ripple>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
<solid android:color="#66FFFFFF" />
<size android:width="4dp" />
<!--
<gradient
android:angle="45"
android:centerColor="#66ff5c33"
android:endColor="#66FF3401"
android:startColor="#66FF3401" />
<corners android:radius="8dp" />
<padding
android:left="0.5dp"
android:right="0.5dp" />
-->
</shape>

View File

@@ -0,0 +1,58 @@
<com.termux.drawer.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<com.termux.view.TerminalView
android:id="@+id/terminal_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:focusableInTouchMode="true"
android:scrollbarThumbVertical="@drawable/terminal_scroll_shape"
android:scrollbars="vertical" />
<LinearLayout
android:id="@+id/left_drawer"
android:layout_width="240dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:background="@android:color/white"
android:choiceMode="singleChoice"
android:divider="@android:color/transparent"
android:dividerHeight="0dp"
android:orientation="vertical" >
<ListView
android:id="@+id/left_drawer_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_gravity="top"
android:layout_weight="1"
android:choiceMode="singleChoice"
android:longClickable="true" />
<LinearLayout
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<Button
android:id="@+id/toggle_keyboard_button"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/toggle_soft_keyboard" />
<Button
android:id="@+id/new_session_button"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/new_session" />
</LinearLayout>
</LinearLayout>
</com.termux.drawer.DrawerLayout>

View File

@@ -0,0 +1,9 @@
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/row_line"
android:layout_width="fill_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:background="@drawable/selected_session_background"
android:ellipsize="marquee"
android:gravity="start|center_vertical"
android:padding="6dip"
android:textSize="14sp" />

View File

@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="application_name">Termux</string>
<string name="application_help">Termux help</string>
<string name="shared_user_label">Termux user</string>
<string name="new_session">New session</string>
<string name="new_session_normal_unnamed">Normal - unnamed</string>
<string name="new_session_normal_named">Normal - named</string>
<string name="new_session_failsafe">Failsafe</string>
<string name="toggle_soft_keyboard">Keyboard</string>
<string name="reset_terminal">Reset</string>
<string name="style_terminal">Style</string>
<string name="toggle_fullscreen">Fullscreen</string>
<string name="share_transcript">Share</string>
<string name="share_transcript_title">Terminal transcript</string>
<string name="help">Help</string>
<string name="welcome_dialog_title">Welcome to Termux</string>
<string name="welcome_dialog_body">Long press anywhere on the terminal for a context menu where Help is available.\n\nExecute \'apt update\' to update the packages list before installing packages.</string>
<string name="welcome_dialog_dont_show_again_button">Do not show again</string>
<string name="bootstrap_installer_body">Installing…</string>
<string name="bootstrap_error_title">Unable to install</string>
<string name="bootstrap_error_body">Termux was unable to install the bootstrap packages.\n\nCheck your network connection and try again.</string>
<string name="bootstrap_error_abort">Abort</string>
<string name="bootstrap_error_try_again">Try again</string>
<string name="bootstrap_error_not_primary_user_title">Unable to install</string>
<string name="bootstrap_error_not_primary_user_message">Termux can only be installed on the primary user account.</string>
<string name="max_terminals_reached_title">Max terminals reached</string>
<string name="max_terminals_reached_message">Close down existing ones before creating new.</string>
<string name="reset_toast_notification">Terminal reset.</string>
<string name="select">Select…</string>
<string name="select_text">Select text</string>
<string name="select_url">Select URL</string>
<string name="select_url_dialog_title">Click URL to copy or long press to open</string>
<string name="select_all_and_share">Select all text and share</string>
<string name="select_url_no_found">No URL found in the terminal.</string>
<string name="select_url_copied_to_clipboard">URL copied to clipboard</string>
<string name="share_transcript_chooser_title">Send text to:</string>
<string name="paste_text">Paste</string>
<string name="kill_process">Hangup</string>
<string name="confirm_kill_process">Close this process?</string>
<string name="session_rename_title">Set session name</string>
<string name="session_rename_positive_button">Set</string>
<string name="session_new_named_title">New named session</string>
<string name="session_new_named_positive_button">Create</string>
<string name="styling_not_installed">The Termux:Style add-on is not installed.</string>
<string name="styling_install">Install</string>
<string name="notification_action_exit">Exit</string>
<string name="notification_action_wakelock">Wake</string>
<string name="notification_action_wifilock">Wifi</string>
</resources>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<!-- See https://developer.android.com/training/material/theme.html for how to customize the Material theme. -->
<!-- NOTE: Cannot use "Light." since it hides the terminal scrollbar on the default black background. -->
<style name="Theme.Termux" parent="@android:style/Theme.Material.Light.NoActionBar">
<item name="android:statusBarColor">#000000</item>
<item name="android:windowBackground">@android:color/black</item>
<!-- Seen in buttons on left drawer: -->
<item name="android:colorAccent">#212121</item>
<item name="android:alertDialogTheme">@style/TermuxAlertDialogStyle</item>
</style>
<style name="TermuxAlertDialogStyle" parent="@android:style/Theme.Material.Light.Dialog.Alert">
<!-- Seen in buttons on alert dialog: -->
<item name="android:colorAccent">#212121</item>
</style>
</resources>

View File

@@ -0,0 +1,54 @@
package com.termux.terminal;
import junit.framework.TestCase;
public class ByteQueueTest extends TestCase {
private static void assertArrayEquals(byte[] expected, byte[] actual) {
if (expected.length != actual.length) {
fail("Difference array length");
}
for (int i = 0; i < expected.length; i++) {
if (expected[i] != actual[i]) {
fail("Inequals at index=" + i + ", expected=" + (int) expected[i] + ", actual=" + (int) actual[i]);
}
}
}
public void testCompleteWrites() throws Exception {
ByteQueue q = new ByteQueue(10);
assertEquals(true, q.write(new byte[] { 1, 2, 3 }, 0, 3));
byte[] arr = new byte[10];
assertEquals(3, q.read(arr, true));
assertArrayEquals(new byte[] { 1, 2, 3 }, new byte[] { arr[0], arr[1], arr[2] });
assertEquals(true, q.write(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }, 0, 10));
assertEquals(10, q.read(arr, true));
assertArrayEquals(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }, arr);
}
public void testQueueWraparound() throws Exception {
ByteQueue q = new ByteQueue(10);
byte[] origArray = new byte[] { 1, 2, 3, 4, 5, 6 };
byte[] readArray = new byte[origArray.length];
for (int i = 0; i < 20; i++) {
q.write(origArray, 0, origArray.length);
assertEquals(origArray.length, q.read(readArray, true));
assertArrayEquals(origArray, readArray);
}
}
public void testWriteNotesClosing() throws Exception {
ByteQueue q = new ByteQueue(10);
q.close();
assertEquals(false, q.write(new byte[] { 1, 2, 3 }, 0, 3));
}
public void testReadNonBlocking() throws Exception {
ByteQueue q = new ByteQueue(10);
assertEquals(0, q.read(new byte[128], false));
}
}

View File

@@ -0,0 +1,23 @@
package com.termux.terminal;
/** "\033[" is the Control Sequence Introducer char sequence (CSI). */
public class ControlSequenceIntroducerTest extends TerminalTestCase {
/** CSI Ps P Scroll down Ps lines (default = 1) (SD). */
public void testCsiT() {
withTerminalSized(4, 6).enterString("1\r\n2\r\n3\r\nhi\033[2Tyo\r\nA\r\nB").assertLinesAre(" ", " ", "1 ", "2 yo", "A ",
"Bi ");
// Default value (1):
withTerminalSized(4, 6).enterString("1\r\n2\r\n3\r\nhi\033[Tyo\r\nA\r\nB").assertLinesAre(" ", "1 ", "2 ", "3 yo", "Ai ",
"B ");
}
/** CSI Ps S Scroll up Ps lines (default = 1) (SU). */
public void testCsiS() {
// The behaviour here is a bit inconsistent between terminals - this is how the OS X Terminal.app does it:
withTerminalSized(3, 4).enterString("1\r\n2\r\n3\r\nhi\033[2Sy").assertLinesAre("3 ", "hi ", " ", " y");
// Default value (1):
withTerminalSized(3, 4).enterString("1\r\n2\r\n3\r\nhi\033[Sy").assertLinesAre("2 ", "3 ", "hi ", " y");
}
}

View File

@@ -0,0 +1,175 @@
package com.termux.terminal;
import junit.framework.Assert;
public class CursorAndScreenTest extends TerminalTestCase {
public void testDeleteLinesKeepsStyles() {
int cols = 5, rows = 5;
withTerminalSized(cols, rows);
for (int row = 0; row < 5; row++) {
for (int col = 0; col < 5; col++) {
// Foreground color to col, background to row:
enterString("\033[38;5;" + col + "m");
enterString("\033[48;5;" + row + "m");
enterString(Character.toString((char) ('A' + col + row * 5)));
}
}
assertLinesAre("ABCDE", "FGHIJ", "KLMNO", "PQRST", "UVWXY");
for (int row = 0; row < 5; row++) {
for (int col = 0; col < 5; col++) {
int s = getStyleAt(row, col);
Assert.assertEquals(col, TextStyle.decodeForeColor(s));
Assert.assertEquals(row, TextStyle.decodeBackColor(s));
}
}
// "${CSI}H" - place cursor at 1,1, then "${CSI}2M" to delete two lines.
enterString("\033[H\033[2M");
assertLinesAre("KLMNO", "PQRST", "UVWXY", " ", " ");
for (int row = 0; row < 3; row++) {
for (int col = 0; col < 5; col++) {
int s = getStyleAt(row, col);
Assert.assertEquals(col, TextStyle.decodeForeColor(s));
Assert.assertEquals(row + 2, TextStyle.decodeBackColor(s));
}
}
// Set default fg and background for the new blank lines:
enterString("\033[38;5;98m");
enterString("\033[48;5;99m");
// "${CSI}B" to go down one line, then "${CSI}2L" to insert two lines:
enterString("\033[B\033[2L");
assertLinesAre("KLMNO", " ", " ", "PQRST", "UVWXY");
for (int row = 0; row < 5; row++) {
for (int col = 0; col < 5; col++) {
int wantedForeground = (row == 1 || row == 2) ? 98 : col;
int wantedBackground = (row == 1 || row == 2) ? 99 : (row == 0 ? 2 : row);
int s = getStyleAt(row, col);
Assert.assertEquals(wantedForeground, TextStyle.decodeForeColor(s));
Assert.assertEquals(wantedBackground, TextStyle.decodeBackColor(s));
}
}
}
public void testDeleteCharacters() {
withTerminalSized(5, 2).enterString("枝ce").assertLinesAre("枝ce ", " ");
withTerminalSized(5, 2).enterString("a枝ce").assertLinesAre("a枝ce", " ");
withTerminalSized(5, 2).enterString("nice").enterString("\033[G\033[P").assertLinesAre("ice ", " ");
withTerminalSized(5, 2).enterString("nice").enterString("\033[G\033[2P").assertLinesAre("ce ", " ");
withTerminalSized(5, 2).enterString("nice").enterString("\033[2G\033[2P").assertLinesAre("ne ", " ");
// "${CSI}${n}P, the delete characters (DCH) sequence should cap characters to delete.
withTerminalSized(5, 2).enterString("nice").enterString("\033[G\033[99P").assertLinesAre(" ", " ");
// With combining char U+0302.
withTerminalSized(5, 2).enterString("n\u0302ice").enterString("\033[G\033[2P").assertLinesAre("ce ", " ");
withTerminalSized(5, 2).enterString("n\u0302ice").enterString("\033[G\033[P").assertLinesAre("ice ", " ");
withTerminalSized(5, 2).enterString("n\u0302ice").enterString("\033[2G\033[2P").assertLinesAre("n\u0302e ", " ");
// With wide 枝 char, checking that putting char at part replaces other with whitespace:
withTerminalSized(5, 2).enterString("枝ce").enterString("\033[Ga").assertLinesAre("a ce ", " ");
withTerminalSized(5, 2).enterString("枝ce").enterString("\033[2Ga").assertLinesAre(" ace ", " ");
// With wide 枝 char, deleting either part replaces other with whitespace:
withTerminalSized(5, 2).enterString("枝ce").enterString("\033[G\033[P").assertLinesAre(" ce ", " ");
withTerminalSized(5, 2).enterString("枝ce").enterString("\033[2G\033[P").assertLinesAre(" ce ", " ");
withTerminalSized(5, 2).enterString("枝ce").enterString("\033[2G\033[2P").assertLinesAre(" e ", " ");
withTerminalSized(5, 2).enterString("枝ce").enterString("\033[G\033[2P").assertLinesAre("ce ", " ");
withTerminalSized(5, 2).enterString("a枝ce").enterString("\033[G\033[P").assertLinesAre("枝ce ", " ");
}
public void testInsertMode() {
// "${CSI}4h" enables insert mode.
withTerminalSized(5, 2).enterString("nice").enterString("\033[G\033[4hA").assertLinesAre("Anice", " ");
withTerminalSized(5, 2).enterString("nice").enterString("\033[2G\033[4hA").assertLinesAre("nAice", " ");
withTerminalSized(5, 2).enterString("nice").enterString("\033[G\033[4hABC").assertLinesAre("ABCni", " ");
// With combining char U+0302.
withTerminalSized(5, 2).enterString("n\u0302ice").enterString("\033[G\033[4hA").assertLinesAre("An\u0302ice", " ");
withTerminalSized(5, 2).enterString("n\u0302ice").enterString("\033[G\033[4hAB").assertLinesAre("ABn\u0302ic", " ");
withTerminalSized(5, 2).enterString("n\u0302ic\u0302e").enterString("\033[2G\033[4hA").assertLinesAre("n\u0302Aic\u0302e", " ");
// ... but without insert mode, combining char should be overwritten:
withTerminalSized(5, 2).enterString("n\u0302ice").enterString("\033[GA").assertLinesAre("Aice ", " ");
// ... also with two combining:
withTerminalSized(5, 2).enterString("n\u0302\u0302i\u0302ce").enterString("\033[GA").assertLinesAre("Ai\u0302ce ", " ");
// ... and in last column:
withTerminalSized(5, 2).enterString("n\u0302\u0302ice!\u0302").enterString("\033[5GA").assertLinesAre("n\u0302\u0302iceA", " ");
withTerminalSized(5, 2).enterString("nic\u0302e!\u0302").enterString("\033[4G枝").assertLinesAre("nic\u0302枝", " ");
withTerminalSized(5, 2).enterString("nic枝\u0302").enterString("\033[3GA").assertLinesAre("niA枝\u0302", " ");
withTerminalSized(5, 2).enterString("nic枝\u0302").enterString("\033[3GA").assertLinesAre("niA枝\u0302", " ");
// With wide 枝 char.
withTerminalSized(5, 2).enterString("nice").enterString("\033[G\033[4h枝").assertLinesAre("枝nic", " ");
withTerminalSized(5, 2).enterString("nice").enterString("\033[2G\033[4h枝").assertLinesAre("n枝ic", " ");
withTerminalSized(5, 2).enterString("n枝ce").enterString("\033[G\033[4ha").assertLinesAre("an枝c", " ");
}
/** HPA—Horizontal Position Absolute (http://www.vt100.net/docs/vt510-rm/HPA) */
public void testCursorHorizontalPositionAbsolute() {
withTerminalSized(4, 4).enterString("ABC\033[`").assertCursorAt(0, 0);
enterString("\033[1`").assertCursorAt(0, 0).enterString("\033[2`").assertCursorAt(0, 1);
enterString("\r\n\033[3`").assertCursorAt(1, 2).enterString("\033[22`").assertCursorAt(1, 3);
// Enable and configure right and left margins, first without origin mode:
enterString("\033[?69h\033[2;3s\033[`").assertCursorAt(0, 0).enterString("\033[22`").assertCursorAt(0, 3);
// .. now with origin mode:
enterString("\033[?6h\033[`").assertCursorAt(0, 1).enterString("\033[22`").assertCursorAt(0, 2);
}
public void testCursorForward() {
// "${CSI}${N:=1}C" moves cursor forward N columns:
withTerminalSized(6, 2).enterString("A\033[CB\033[2CC").assertLinesAre("A B C", " ");
// If an attempt is made to move the cursor to the right of the right margin, the cursor stops at the right margin:
withTerminalSized(6, 2).enterString("A\033[44CB").assertLinesAre("A B", " ");
// Enable right margin and verify that CUF ends at the set right margin:
withTerminalSized(6, 2).enterString("\033[?69h\033[1;3s\033[44CAB").assertLinesAre(" A ", "B ");
}
public void testCursorBack() {
// "${CSI}${N:=1}D" moves cursor back N columns:
withTerminalSized(3, 2).enterString("A\033[DB").assertLinesAre("B ", " ");
withTerminalSized(3, 2).enterString("AB\033[2DC").assertLinesAre("CB ", " ");
// If an attempt is made to move the cursor to the left of the left margin, the cursor stops at the left margin:
withTerminalSized(3, 2).enterString("AB\033[44DC").assertLinesAre("CB ", " ");
// Enable left margin and verify that CUB ends at the set left margin:
withTerminalSized(6, 2).enterString("ABCD\033[?69h\033[2;6s\033[44DE").assertLinesAre("AECD ", " ");
}
public void testCursorUp() {
// "${CSI}${N:=1}A" moves cursor up N rows:
withTerminalSized(3, 3).enterString("ABCDEFG\033[AH").assertLinesAre("ABC", "DHF", "G ");
withTerminalSized(3, 3).enterString("ABCDEFG\033[2AH").assertLinesAre("AHC", "DEF", "G ");
// If an attempt is made to move the cursor above the top margin, the cursor stops at the top margin:
withTerminalSized(3, 3).enterString("ABCDEFG\033[44AH").assertLinesAre("AHC", "DEF", "G ");
// Set top margin and validate that cursor does not go above it:
withTerminalSized(3, 3).enterString("\033[2rABCDEFG\033[44AH").assertLinesAre("ABC", "DHF", "G ");
}
public void testCursorDown() {
// "${CSI}${N:=1}B" moves cursor down N rows:
withTerminalSized(3, 3).enterString("AB\033[BC").assertLinesAre("AB ", " C", " ");
withTerminalSized(3, 3).enterString("AB\033[2BC").assertLinesAre("AB ", " ", " C");
// If an attempt is made to move the cursor below the bottom margin, the cursor stops at the bottom margin:
withTerminalSized(3, 3).enterString("AB\033[44BC").assertLinesAre("AB ", " ", " C");
// Set bottom margin and validate that cursor does not go above it:
withTerminalSized(3, 3).enterString("\033[1;2rAB\033[44BC").assertLinesAre("AB ", " C", " ");
}
public void testReportCursorPosition() {
withTerminalSized(10, 10);
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10; j++) {
enterString("\033[" + (i + 1) + ";" + (j + 1) + "H"); // CUP cursor position.
assertCursorAt(i, j);
// Device Status Report (DSR):
assertEnteringStringGivesResponse("\033[6n", "\033[" + (i + 1) + ";" + (j + 1) + "R");
// DECXCPR — Extended Cursor Position. Note that http://www.vt100.net/docs/vt510-rm/DECXCPR says
// the response is "${CSI}${LINE};${COLUMN};${PAGE}R" while xterm (http://invisible-island.net/xterm/ctlseqs/ctlseqs.html)
// drops the question mark. Expect xterm behaviour here.
assertEnteringStringGivesResponse("\033[?6n", "\033[?" + (i + 1) + ";" + (j + 1) + ";1R");
}
}
}
public void testHorizontalTabColorsBackground() {
withTerminalSized(10, 3).enterString("\033[48;5;15m").enterString("\t");
assertCursorAt(0, 8);
for (int i = 0; i < 10; i++) {
int expectedColor = i < 8 ? 15 : TextStyle.COLOR_INDEX_BACKGROUND;
assertEquals(expectedColor, TextStyle.decodeBackColor(getStyleAt(0, i)));
}
}
}

View File

@@ -0,0 +1,62 @@
package com.termux.terminal;
/**
* <pre>
* "CSI ? Pm h", DEC Private Mode Set (DECSET)
* </pre>
*
* and
*
* <pre>
* "CSI ? Pm l", DEC Private Mode Reset (DECRST)
* </pre>
*
* controls various aspects of the terminal
*/
public class DecSetTest extends TerminalTestCase {
/** DECSET 25, DECTCEM, controls visibility of the cursor. */
public void testShowHideCursor() {
withTerminalSized(3, 3);
assertTrue("Initially the cursor should be visible", mTerminal.isShowingCursor());
enterString("\033[?25l"); // Hide Cursor (DECTCEM).
assertFalse(mTerminal.isShowingCursor());
enterString("\033[?25h"); // Show Cursor (DECTCEM).
assertTrue(mTerminal.isShowingCursor());
enterString("\033[?25l"); // Hide Cursor (DECTCEM), again.
assertFalse(mTerminal.isShowingCursor());
mTerminal.reset();
assertTrue("Resetting the terminal should show the cursor", mTerminal.isShowingCursor());
}
/** DECSET 2004, controls bracketed paste mode. */
public void testBracketedPasteMode() {
withTerminalSized(3, 3);
mTerminal.paste("a");
assertEquals("Pasting 'a' should output 'a' when bracketed paste mode is disabled", "a", mOutput.getOutputAndClear());
enterString("\033[?2004h"); // Enable bracketed paste mode.
mTerminal.paste("a");
assertEquals("Pasting when in bracketed paste mode should be bracketed", "\033[200~a\033[201~", mOutput.getOutputAndClear());
enterString("\033[?2004l"); // Disable bracketed paste mode.
mTerminal.paste("a");
assertEquals("Pasting 'a' should output 'a' when bracketed paste mode is disabled", "a", mOutput.getOutputAndClear());
enterString("\033[?2004h"); // Enable bracketed paste mode, again.
mTerminal.paste("a");
assertEquals("Pasting when in bracketed paste mode again should be bracketed", "\033[200~a\033[201~", mOutput.getOutputAndClear());
mTerminal.paste("\033ab\033cd\033");
assertEquals("Pasting an escape character should not input it", "\033[200~abcd\033[201~", mOutput.getOutputAndClear());
mTerminal.paste("\u0081ab\u0081cd\u009F");
assertEquals("Pasting C1 control codes should not input it", "\033[200~abcd\033[201~", mOutput.getOutputAndClear());
mTerminal.reset();
mTerminal.paste("a");
assertEquals("Terminal reset() should disable bracketed paste mode", "a", mOutput.getOutputAndClear());
}
}

View File

@@ -0,0 +1,53 @@
package com.termux.terminal;
/**
* "\033P" is a device control string.
*/
public class DeviceControlStringTest extends TerminalTestCase {
private static String hexEncode(String s) {
StringBuilder result = new StringBuilder();
for (int i = 0; i < s.length(); i++)
result.append(String.format("%02X", (int) s.charAt(i)));
return result.toString();
}
private void assertCapabilityResponse(String cap, String expectedResponse) {
String input = "\033P+q" + hexEncode(cap) + "\033\\";
assertEnteringStringGivesResponse(input, "\033P1+r" + hexEncode(cap) + "=" + hexEncode(expectedResponse) + "\033\\");
}
public void testReportColorsAndName() {
// Request Termcap/Terminfo String. The string following the "q" is a list of names encoded in
// hexadecimal (2 digits per character) separated by ; which correspond to termcap or terminfo key
// names.
// Two special features are also recognized, which are not key names: Co for termcap colors (or colors
// for terminfo colors), and TN for termcap name (or name for terminfo name).
// xterm responds with DCS 1 + r P t ST for valid requests, adding to P t an = , and the value of the
// corresponding string that xterm would send, or DCS 0 + r P t ST for invalid requests. The strings are
// encoded in hexadecimal (2 digits per character).
withTerminalSized(3, 3).enterString("A");
assertCapabilityResponse("Co", "256");
assertCapabilityResponse("colors", "256");
assertCapabilityResponse("TN", "xterm");
assertCapabilityResponse("name", "xterm");
enterString("B").assertLinesAre("AB ", " ", " ");
}
public void testReportKeys() {
withTerminalSized(3, 3);
assertCapabilityResponse("kB", "\033[Z");
}
public void testReallyLongDeviceControlString() {
withTerminalSized(3, 3).enterString("\033P");
for (int i = 0; i < 10000; i++) {
enterString("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
}
// The terminal should ignore the overlong DCS sequence and continue printing "aaa." and fill at least the first two lines with
// them:
assertLineIs(0, "aaa");
assertLineIs(1, "aaa");
}
}

View File

@@ -0,0 +1,33 @@
package com.termux.terminal;
public class HistoryTest extends TerminalTestCase {
public void testHistory() {
final int rows = 3;
final int cols = 3;
withTerminalSized(cols, rows).enterString("111222333444555666777888999");
assertCursorAt(2, 2);
assertLinesAre("777", "888", "999");
assertHistoryStartsWith("666", "555");
mTerminal.resize(cols, 2);
assertHistoryStartsWith("777", "666", "555");
mTerminal.resize(cols, 3);
assertHistoryStartsWith("666", "555");
}
public void testHistoryWithScrollRegion() {
// "CSI P_s ; P_s r" - set Scrolling Region [top;bottom] (default = full size of window) (DECSTBM).
withTerminalSized(3, 4).enterString("111222333444");
assertLinesAre("111", "222", "333", "444");
enterString("\033[2;3r");
// NOTE: "DECSTBM moves the cursor to column 1, line 1 of the page."
assertCursorAt(0, 0);
enterString("\nCDEFGH").assertLinesAre("111", "CDE", "FGH", "444");
enterString("IJK").assertLinesAre("111", "FGH", "IJK", "444").assertHistoryStartsWith("CDE");
enterString("LMN").assertLinesAre("111", "IJK", "LMN", "444").assertHistoryStartsWith("FGH", "CDE");
}
}

View File

@@ -0,0 +1,171 @@
package com.termux.terminal;
import android.view.KeyEvent;
import junit.framework.TestCase;
public class KeyHandlerTest extends TestCase {
private static String stringToHex(String s) {
if (s == null) return null;
StringBuilder buffer = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
if (buffer.length() > 0) {
buffer.append(" ");
}
buffer.append("0x" + Integer.toHexString(s.charAt(i)));
}
return buffer.toString();
}
private static void assertKeysEquals(String expected, String actual) {
if (!expected.equals(actual)) {
assertEquals(stringToHex(expected), stringToHex(actual));
}
}
/** See http://pubs.opengroup.org/onlinepubs/7990989799/xcurses/terminfo.html */
public void testTermCaps() {
// Backspace.
assertKeysEquals("\u007f", KeyHandler.getCodeFromTermcap("kb", false, false));
// Back tab.
assertKeysEquals("\033[Z", KeyHandler.getCodeFromTermcap("kB", false, false));
// Arrow keys (up/down/right/left):
assertKeysEquals("\033[A", KeyHandler.getCodeFromTermcap("ku", false, false));
assertKeysEquals("\033[B", KeyHandler.getCodeFromTermcap("kd", false, false));
assertKeysEquals("\033[C", KeyHandler.getCodeFromTermcap("kr", false, false));
assertKeysEquals("\033[D", KeyHandler.getCodeFromTermcap("kl", false, false));
// .. shifted:
assertKeysEquals("\033[1;2A", KeyHandler.getCodeFromTermcap("kUP", false, false));
assertKeysEquals("\033[1;2B", KeyHandler.getCodeFromTermcap("kDN", false, false));
assertKeysEquals("\033[1;2C", KeyHandler.getCodeFromTermcap("%i", false, false));
assertKeysEquals("\033[1;2D", KeyHandler.getCodeFromTermcap("#4", false, false));
// Home/end keys:
assertKeysEquals("\033[H", KeyHandler.getCodeFromTermcap("kh", false, false));
assertKeysEquals("\033[F", KeyHandler.getCodeFromTermcap("@7", false, false));
// ... shifted:
assertKeysEquals("\033[1;2H", KeyHandler.getCodeFromTermcap("#2", false, false));
assertKeysEquals("\033[1;2F", KeyHandler.getCodeFromTermcap("*7", false, false));
// The traditional keyboard keypad:
// [Insert] [Home] [Page Up ]
// [Delete] [End] [Page Down]
//
// Termcap names (with xterm response in parenthesis):
// K1=Upper left of keypad (xterm sends same "<ESC>[H" = Home).
// K2=Center of keypad (xterm sends invalid response).
// K3=Upper right of keypad (xterm sends "<ESC>[5~" = Page Up).
// K4=Lower left of keypad (xterm sends "<ESC>[F" = End key).
// K5=Lower right of keypad (xterm sends "<ESC>[6~" = Page Down).
//
// vim/neovim (runtime/doc/term.txt):
// t_K1 <kHome> keypad home key
// t_K3 <kPageUp> keypad page-up key
// t_K4 <kEnd> keypad end key
// t_K5 <kPageDown> keypad page-down key
//
assertKeysEquals("\033[H", KeyHandler.getCodeFromTermcap("K1", false, false));
assertKeysEquals("\033OH", KeyHandler.getCodeFromTermcap("K1", true, false));
assertKeysEquals("\033[5~", KeyHandler.getCodeFromTermcap("K3", false, false));
assertKeysEquals("\033[F", KeyHandler.getCodeFromTermcap("K4", false, false));
assertKeysEquals("\033OF", KeyHandler.getCodeFromTermcap("K4", true, false));
assertKeysEquals("\033[6~", KeyHandler.getCodeFromTermcap("K5", false, false));
// Function keys F1-F12:
assertKeysEquals("\033OP", KeyHandler.getCodeFromTermcap("k1", false, false));
assertKeysEquals("\033OQ", KeyHandler.getCodeFromTermcap("k2", false, false));
assertKeysEquals("\033OR", KeyHandler.getCodeFromTermcap("k3", false, false));
assertKeysEquals("\033OS", KeyHandler.getCodeFromTermcap("k4", false, false));
assertKeysEquals("\033[15~", KeyHandler.getCodeFromTermcap("k5", false, false));
assertKeysEquals("\033[17~", KeyHandler.getCodeFromTermcap("k6", false, false));
assertKeysEquals("\033[18~", KeyHandler.getCodeFromTermcap("k7", false, false));
assertKeysEquals("\033[19~", KeyHandler.getCodeFromTermcap("k8", false, false));
assertKeysEquals("\033[20~", KeyHandler.getCodeFromTermcap("k9", false, false));
assertKeysEquals("\033[21~", KeyHandler.getCodeFromTermcap("k;", false, false));
assertKeysEquals("\033[23~", KeyHandler.getCodeFromTermcap("F1", false, false));
assertKeysEquals("\033[24~", KeyHandler.getCodeFromTermcap("F2", false, false));
// Function keys F13-F24 (same as shifted F1-F12):
assertKeysEquals("\033[1;2P", KeyHandler.getCodeFromTermcap("F3", false, false));
assertKeysEquals("\033[1;2Q", KeyHandler.getCodeFromTermcap("F4", false, false));
assertKeysEquals("\033[1;2R", KeyHandler.getCodeFromTermcap("F5", false, false));
assertKeysEquals("\033[1;2S", KeyHandler.getCodeFromTermcap("F6", false, false));
assertKeysEquals("\033[15;2~", KeyHandler.getCodeFromTermcap("F7", false, false));
assertKeysEquals("\033[17;2~", KeyHandler.getCodeFromTermcap("F8", false, false));
assertKeysEquals("\033[18;2~", KeyHandler.getCodeFromTermcap("F9", false, false));
assertKeysEquals("\033[19;2~", KeyHandler.getCodeFromTermcap("FA", false, false));
assertKeysEquals("\033[20;2~", KeyHandler.getCodeFromTermcap("FB", false, false));
assertKeysEquals("\033[21;2~", KeyHandler.getCodeFromTermcap("FC", false, false));
assertKeysEquals("\033[23;2~", KeyHandler.getCodeFromTermcap("FD", false, false));
assertKeysEquals("\033[24;2~", KeyHandler.getCodeFromTermcap("FE", false, false));
}
public void testKeyCodes() {
// Return sends carriage return (\r), which normally gets translated by the device driver to newline (\n) unless the ICRNL termios
// flag has been set.
assertKeysEquals("\r", KeyHandler.getCode(KeyEvent.KEYCODE_ENTER, 0, false, false));
// Backspace.
assertKeysEquals("\u007f", KeyHandler.getCode(KeyEvent.KEYCODE_DEL, 0, false, false));
// Back tab.
assertKeysEquals("\033[Z", KeyHandler.getCode(KeyEvent.KEYCODE_TAB, KeyHandler.KEYMOD_SHIFT, false, false));
// Arrow keys (up/down/right/left):
assertKeysEquals("\033[A", KeyHandler.getCode(KeyEvent.KEYCODE_DPAD_UP, 0, false, false));
assertKeysEquals("\033[B", KeyHandler.getCode(KeyEvent.KEYCODE_DPAD_DOWN, 0, false, false));
assertKeysEquals("\033[C", KeyHandler.getCode(KeyEvent.KEYCODE_DPAD_RIGHT, 0, false, false));
assertKeysEquals("\033[D", KeyHandler.getCode(KeyEvent.KEYCODE_DPAD_LEFT, 0, false, false));
// .. shifted:
assertKeysEquals("\033[1;2A", KeyHandler.getCode(KeyEvent.KEYCODE_DPAD_UP, KeyHandler.KEYMOD_SHIFT, false, false));
assertKeysEquals("\033[1;2B", KeyHandler.getCode(KeyEvent.KEYCODE_DPAD_DOWN, KeyHandler.KEYMOD_SHIFT, false, false));
assertKeysEquals("\033[1;2C", KeyHandler.getCode(KeyEvent.KEYCODE_DPAD_RIGHT, KeyHandler.KEYMOD_SHIFT, false, false));
assertKeysEquals("\033[1;2D", KeyHandler.getCode(KeyEvent.KEYCODE_DPAD_LEFT, KeyHandler.KEYMOD_SHIFT, false, false));
// .. ctrl:ed:
assertKeysEquals("\033[1;5A", KeyHandler.getCode(KeyEvent.KEYCODE_DPAD_UP, KeyHandler.KEYMOD_CTRL, false, false));
assertKeysEquals("\033[1;5B", KeyHandler.getCode(KeyEvent.KEYCODE_DPAD_DOWN, KeyHandler.KEYMOD_CTRL, false, false));
assertKeysEquals("\033[1;5C", KeyHandler.getCode(KeyEvent.KEYCODE_DPAD_RIGHT, KeyHandler.KEYMOD_CTRL, false, false));
assertKeysEquals("\033[1;5D", KeyHandler.getCode(KeyEvent.KEYCODE_DPAD_LEFT, KeyHandler.KEYMOD_CTRL, false, false));
// .. ctrl:ed and shifted:
int mod = KeyHandler.KEYMOD_CTRL | KeyHandler.KEYMOD_SHIFT;
assertKeysEquals("\033[1;6A", KeyHandler.getCode(KeyEvent.KEYCODE_DPAD_UP, mod, false, false));
assertKeysEquals("\033[1;6B", KeyHandler.getCode(KeyEvent.KEYCODE_DPAD_DOWN, mod, false, false));
assertKeysEquals("\033[1;6C", KeyHandler.getCode(KeyEvent.KEYCODE_DPAD_RIGHT, mod, false, false));
assertKeysEquals("\033[1;6D", KeyHandler.getCode(KeyEvent.KEYCODE_DPAD_LEFT, mod, false, false));
// Home/end keys:
assertKeysEquals("\033[H", KeyHandler.getCode(KeyEvent.KEYCODE_HOME, 0, false, false));
assertKeysEquals("\033[F", KeyHandler.getCode(KeyEvent.KEYCODE_MOVE_END, 0, false, false));
// ... shifted:
assertKeysEquals("\033[1;2H", KeyHandler.getCode(KeyEvent.KEYCODE_HOME, KeyHandler.KEYMOD_SHIFT, false, false));
assertKeysEquals("\033[1;2F", KeyHandler.getCode(KeyEvent.KEYCODE_MOVE_END, KeyHandler.KEYMOD_SHIFT, false, false));
// Function keys F1-F12:
assertKeysEquals("\033OP", KeyHandler.getCode(KeyEvent.KEYCODE_F1, 0, false, false));
assertKeysEquals("\033OQ", KeyHandler.getCode(KeyEvent.KEYCODE_F2, 0, false, false));
assertKeysEquals("\033OR", KeyHandler.getCode(KeyEvent.KEYCODE_F3, 0, false, false));
assertKeysEquals("\033OS", KeyHandler.getCode(KeyEvent.KEYCODE_F4, 0, false, false));
assertKeysEquals("\033[15~", KeyHandler.getCode(KeyEvent.KEYCODE_F5, 0, false, false));
assertKeysEquals("\033[17~", KeyHandler.getCode(KeyEvent.KEYCODE_F6, 0, false, false));
assertKeysEquals("\033[18~", KeyHandler.getCode(KeyEvent.KEYCODE_F7, 0, false, false));
assertKeysEquals("\033[19~", KeyHandler.getCode(KeyEvent.KEYCODE_F8, 0, false, false));
assertKeysEquals("\033[20~", KeyHandler.getCode(KeyEvent.KEYCODE_F9, 0, false, false));
assertKeysEquals("\033[21~", KeyHandler.getCode(KeyEvent.KEYCODE_F10, 0, false, false));
assertKeysEquals("\033[23~", KeyHandler.getCode(KeyEvent.KEYCODE_F11, 0, false, false));
assertKeysEquals("\033[24~", KeyHandler.getCode(KeyEvent.KEYCODE_F12, 0, false, false));
// Function keys F13-F24 (same as shifted F1-F12):
assertKeysEquals("\033[1;2P", KeyHandler.getCode(KeyEvent.KEYCODE_F1, KeyHandler.KEYMOD_SHIFT, false, false));
assertKeysEquals("\033[1;2Q", KeyHandler.getCode(KeyEvent.KEYCODE_F2, KeyHandler.KEYMOD_SHIFT, false, false));
assertKeysEquals("\033[1;2R", KeyHandler.getCode(KeyEvent.KEYCODE_F3, KeyHandler.KEYMOD_SHIFT, false, false));
assertKeysEquals("\033[1;2S", KeyHandler.getCode(KeyEvent.KEYCODE_F4, KeyHandler.KEYMOD_SHIFT, false, false));
assertKeysEquals("\033[15;2~", KeyHandler.getCode(KeyEvent.KEYCODE_F5, KeyHandler.KEYMOD_SHIFT, false, false));
assertKeysEquals("\033[17;2~", KeyHandler.getCode(KeyEvent.KEYCODE_F6, KeyHandler.KEYMOD_SHIFT, false, false));
assertKeysEquals("\033[18;2~", KeyHandler.getCode(KeyEvent.KEYCODE_F7, KeyHandler.KEYMOD_SHIFT, false, false));
assertKeysEquals("\033[19;2~", KeyHandler.getCode(KeyEvent.KEYCODE_F8, KeyHandler.KEYMOD_SHIFT, false, false));
assertKeysEquals("\033[20;2~", KeyHandler.getCode(KeyEvent.KEYCODE_F9, KeyHandler.KEYMOD_SHIFT, false, false));
assertKeysEquals("\033[21;2~", KeyHandler.getCode(KeyEvent.KEYCODE_F10, KeyHandler.KEYMOD_SHIFT, false, false));
assertKeysEquals("\033[23;2~", KeyHandler.getCode(KeyEvent.KEYCODE_F11, KeyHandler.KEYMOD_SHIFT, false, false));
assertKeysEquals("\033[24;2~", KeyHandler.getCode(KeyEvent.KEYCODE_F12, KeyHandler.KEYMOD_SHIFT, false, false));
}
}

View File

@@ -0,0 +1,196 @@
package com.termux.terminal;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import android.util.Base64;
/** "ESC ]" is the Operating System Command. */
public class OperatingSystemControlTest extends TerminalTestCase {
public void testSetTitle() throws Exception {
List<ChangedTitle> expectedTitleChanges = new ArrayList<>();
withTerminalSized(10, 10);
enterString("\033]0;Hello, world\007");
assertEquals("Hello, world", mTerminal.getTitle());
expectedTitleChanges.add(new ChangedTitle((String) null, "Hello, world"));
assertEquals(expectedTitleChanges, mOutput.titleChanges);
enterString("\033]0;Goodbye, world\007");
assertEquals("Goodbye, world", mTerminal.getTitle());
expectedTitleChanges.add(new ChangedTitle("Hello, world", "Goodbye, world"));
assertEquals(expectedTitleChanges, mOutput.titleChanges);
enterString("\033]0;Goodbye, \u00F1 world\007");
assertEquals("Goodbye, \uu00F1 world", mTerminal.getTitle());
expectedTitleChanges.add(new ChangedTitle("Goodbye, world", "Goodbye, \uu00F1 world"));
assertEquals(expectedTitleChanges, mOutput.titleChanges);
// 2 should work as well (0 sets both title and icon).
enterString("\033]2;Updated\007");
assertEquals("Updated", mTerminal.getTitle());
expectedTitleChanges.add(new ChangedTitle("Goodbye, \uu00F1 world", "Updated"));
assertEquals(expectedTitleChanges, mOutput.titleChanges);
enterString("\033[22;0t");
enterString("\033]0;FIRST\007");
expectedTitleChanges.add(new ChangedTitle("Updated", "FIRST"));
assertEquals("FIRST", mTerminal.getTitle());
assertEquals(expectedTitleChanges, mOutput.titleChanges);
enterString("\033[22;0t");
enterString("\033]0;SECOND\007");
assertEquals("SECOND", mTerminal.getTitle());
expectedTitleChanges.add(new ChangedTitle("FIRST", "SECOND"));
assertEquals(expectedTitleChanges, mOutput.titleChanges);
enterString("\033[23;0t");
assertEquals("FIRST", mTerminal.getTitle());
expectedTitleChanges.add(new ChangedTitle("SECOND", "FIRST"));
assertEquals(expectedTitleChanges, mOutput.titleChanges);
enterString("\033[23;0t");
expectedTitleChanges.add(new ChangedTitle("FIRST", "Updated"));
assertEquals(expectedTitleChanges, mOutput.titleChanges);
enterString("\033[22;0t");
enterString("\033[22;0t");
enterString("\033[22;0t");
// Popping to same title should not cause changes.
enterString("\033[23;0t");
enterString("\033[23;0t");
enterString("\033[23;0t");
assertEquals(expectedTitleChanges, mOutput.titleChanges);
}
public void testTitleStack() throws Exception {
// echo -ne '\e]0;BEFORE\007' # set title
// echo -ne '\e[22t' # push to stack
// echo -ne '\e]0;AFTER\007' # set new title
// echo -ne '\e[23t' # retrieve from stack
withTerminalSized(10, 10);
enterString("\033]0;InitialTitle\007");
assertEquals("InitialTitle", mTerminal.getTitle());
enterString("\033[22t");
assertEquals("InitialTitle", mTerminal.getTitle());
enterString("\033]0;UpdatedTitle\007");
assertEquals("UpdatedTitle", mTerminal.getTitle());
enterString("\033[23t");
assertEquals("InitialTitle", mTerminal.getTitle());
enterString("\033[23t\033[23t\033[23t");
assertEquals("InitialTitle", mTerminal.getTitle());
}
public void testSetColor() throws Exception {
// "OSC 4; $INDEX; $COLORSPEC BEL" => Change color $INDEX to the color specified by $COLORSPEC.
withTerminalSized(4, 4).enterString("\033]4;5;#00FF00\007");
assertEquals(Integer.toHexString(0xFF00FF00), Integer.toHexString(mTerminal.mColors.mCurrentColors[5]));
enterString("\033]4;5;#00FFAB\007");
assertEquals(mTerminal.mColors.mCurrentColors[5], 0xFF00FFAB);
enterString("\033]4;255;#ABFFAB\007");
assertEquals(mTerminal.mColors.mCurrentColors[255], 0xFFABFFAB);
// Two indexed colors at once:
enterString("\033]4;7;#00FF00;8;#0000FF\007");
assertEquals(mTerminal.mColors.mCurrentColors[7], 0xFF00FF00);
assertEquals(mTerminal.mColors.mCurrentColors[8], 0xFF0000FF);
}
void assertIndexColorsMatch(int[] expected) {
for (int i = 0; i < 255; i++)
assertEquals("index=" + i, expected[i], mTerminal.mColors.mCurrentColors[i]);
}
public void testResetColor() throws Exception {
withTerminalSized(4, 4);
int[] initialColors = new int[TextStyle.NUM_INDEXED_COLORS];
System.arraycopy(mTerminal.mColors.mCurrentColors, 0, initialColors, 0, initialColors.length);
int[] expectedColors = new int[initialColors.length];
System.arraycopy(mTerminal.mColors.mCurrentColors, 0, expectedColors, 0, expectedColors.length);
Random rand = new Random();
for (int endType = 0; endType < 3; endType++) {
// Both BEL (7) and ST (ESC \) can end an OSC sequence.
String ender = (endType == 0) ? "\007" : "\033\\";
for (int i = 0; i < 255; i++) {
expectedColors[i] = 0xFF000000 + (rand.nextInt() & 0xFFFFFF);
int r = (expectedColors[i] >> 16) & 0xFF;
int g = (expectedColors[i] >> 8) & 0xFF;
int b = expectedColors[i] & 0xFF;
String rgbHex = String.format("%02x", r) + String.format("%02x", g) + String.format("%02x", b);
enterString("\033]4;" + i + ";#" + rgbHex + ender);
assertEquals(expectedColors[i], mTerminal.mColors.mCurrentColors[i]);
}
}
enterString("\033]104;0\007");
expectedColors[0] = TerminalColors.COLOR_SCHEME.mDefaultColors[0];
assertIndexColorsMatch(expectedColors);
enterString("\033]104;1;2\007");
expectedColors[1] = TerminalColors.COLOR_SCHEME.mDefaultColors[1];
expectedColors[2] = TerminalColors.COLOR_SCHEME.mDefaultColors[2];
assertIndexColorsMatch(expectedColors);
enterString("\033]104\007"); // Reset all colors.
assertIndexColorsMatch(TerminalColors.COLOR_SCHEME.mDefaultColors);
}
public void disabledTestSetClipboard() {
// Cannot run this as a unit test since Base64 is a android.util class.
enterString("\033]52;c;" + Base64.encodeToString("Hello, world".getBytes(), 0) + "\007");
}
public void testResettingTerminalResetsColor() throws Exception {
// "OSC 4; $INDEX; $COLORSPEC BEL" => Change color $INDEX to the color specified by $COLORSPEC.
withTerminalSized(4, 4).enterString("\033]4;5;#00FF00\007");
enterString("\033]4;5;#00FFAB\007").assertColor(5, 0xFF00FFAB);
enterString("\033]4;255;#ABFFAB\007").assertColor(255, 0xFFABFFAB);
mTerminal.reset();
assertIndexColorsMatch(TerminalColors.COLOR_SCHEME.mDefaultColors);
}
public void testSettingDynamicColors() {
// "${OSC}${DYNAMIC};${COLORSPEC}${BEL_OR_STRINGTERMINATOR}" => Change ${DYNAMIC} color to the color specified by $COLORSPEC where:
// DYNAMIC=10: Text foreground color.
// DYNAMIC=11: Text background color.
// DYNAMIC=12: Text cursor color.
withTerminalSized(3, 3).enterString("\033]10;#ABCD00\007").assertColor(TextStyle.COLOR_INDEX_FOREGROUND, 0xFFABCD00);
enterString("\033]11;#0ABCD0\007").assertColor(TextStyle.COLOR_INDEX_BACKGROUND, 0xFF0ABCD0);
enterString("\033]12;#00ABCD\007").assertColor(TextStyle.COLOR_INDEX_CURSOR, 0xFF00ABCD);
// Two special colors at once
// ("Each successive parameter changes the next color in the list. The value of P s tells the starting point in the list"):
enterString("\033]10;#FF0000;#00FF00\007").assertColor(TextStyle.COLOR_INDEX_FOREGROUND, 0xFFFF0000);
assertColor(TextStyle.COLOR_INDEX_BACKGROUND, 0xFF00FF00);
// Three at once:
enterString("\033]10;#0000FF;#00FF00;#FF0000\007").assertColor(TextStyle.COLOR_INDEX_FOREGROUND, 0xFF0000FF);
assertColor(TextStyle.COLOR_INDEX_BACKGROUND, 0xFF00FF00).assertColor(TextStyle.COLOR_INDEX_CURSOR, 0xFFFF0000);
// Without ending semicolon:
enterString("\033]10;#FF0000\007").assertColor(TextStyle.COLOR_INDEX_FOREGROUND, 0xFFFF0000);
// For background and cursor:
enterString("\033]11;#FFFF00;\007").assertColor(TextStyle.COLOR_INDEX_BACKGROUND, 0xFFFFFF00);
enterString("\033]12;#00FFFF;\007").assertColor(TextStyle.COLOR_INDEX_CURSOR, 0xFF00FFFF);
// Using string terminator:
String stringTerminator = "\033\\";
enterString("\033]10;#FF0000" + stringTerminator).assertColor(TextStyle.COLOR_INDEX_FOREGROUND, 0xFFFF0000);
// For background and cursor:
enterString("\033]11;#FFFF00;" + stringTerminator).assertColor(TextStyle.COLOR_INDEX_BACKGROUND, 0xFFFFFF00);
enterString("\033]12;#00FFFF;" + stringTerminator).assertColor(TextStyle.COLOR_INDEX_CURSOR, 0xFF00FFFF);
}
public void testReportSpecialColors() {
// "${OSC}${DYNAMIC};?${BEL}" => Terminal responds with the control sequence which would set the current color.
// Both xterm and libvte (gnome-terminal and others) use the longest color representation, which means that
// the response is "${OSC}rgb:RRRR/GGGG/BBBB"
withTerminalSized(3, 3).enterString("\033]10;#ABCD00\007").assertColor(TextStyle.COLOR_INDEX_FOREGROUND, 0xFFABCD00);
assertEnteringStringGivesResponse("\033]10;?\007", "\033]10;rgb:abab/cdcd/0000\007");
// Same as above but with string terminator. xterm uses the same string terminator in the response, which
// e.g. script posted at http://superuser.com/questions/157563/programmatic-access-to-current-xterm-background-color
// relies on:
assertEnteringStringGivesResponse("\033]10;?\033\\", "\033]10;rgb:abab/cdcd/0000\033\\");
}
}

View File

@@ -0,0 +1,117 @@
package com.termux.terminal;
public class RectangularAreasTest extends TerminalTestCase {
/** http://www.vt100.net/docs/vt510-rm/DECFRA */
public void testFillRectangularArea() {
withTerminalSized(3, 3).enterString("\033[88$x").assertLinesAre("XXX", "XXX", "XXX");
withTerminalSized(3, 3).enterString("\033[88;1;1;2;10$x").assertLinesAre("XXX", "XXX", " ");
withTerminalSized(3, 3).enterString("\033[88;2;1;3;10$x").assertLinesAre(" ", "XXX", "XXX");
withTerminalSized(3, 3).enterString("\033[88;1;1;100;1$x").assertLinesAre("X ", "X ", "X ");
withTerminalSized(3, 3).enterString("\033[88;1;1;100;2$x").assertLinesAre("XX ", "XX ", "XX ");
withTerminalSized(3, 3).enterString("\033[88;100;1;100;2$x").assertLinesAre(" ", " ", " ");
}
/** http://www.vt100.net/docs/vt510-rm/DECERA */
public void testEraseRectangularArea() {
withTerminalSized(3, 3).enterString("ABCDEFGHI\033[$z").assertLinesAre(" ", " ", " ");
withTerminalSized(3, 3).enterString("ABCDEFGHI\033[1;1;2;10$z").assertLinesAre(" ", " ", "GHI");
withTerminalSized(3, 3).enterString("ABCDEFGHI\033[2;1;3;10$z").assertLinesAre("ABC", " ", " ");
withTerminalSized(3, 3).enterString("ABCDEFGHI\033[1;1;100;1$z").assertLinesAre(" BC", " EF", " HI");
withTerminalSized(3, 3).enterString("ABCDEFGHI\033[1;1;100;2$z").assertLinesAre(" C", " F", " I");
withTerminalSized(3, 3).enterString("ABCDEFGHI\033[100;1;100;2$z").assertLinesAre("ABC", "DEF", "GHI");
withTerminalSized(3, 3).enterString("A\033[$zBC").assertLinesAre(" BC", " ", " ");
}
/** http://www.vt100.net/docs/vt510-rm/DECSED */
public void testSelectiveEraseInDisplay() {
// ${CSI}1"q enables protection, ${CSI}0"q disables it.
// ${CSI}?${0,1,2}J" erases (0=cursor to end, 1=start to cursor, 2=complete display).
withTerminalSized(3, 3).enterString("ABCDEFGHI\033[?2J").assertLinesAre(" ", " ", " ");
withTerminalSized(3, 3).enterString("ABC\033[1\"qDE\033[0\"qFGHI\033[?2J").assertLinesAre(" ", "DE ", " ");
withTerminalSized(3, 3).enterString("\033[1\"qABCDE\033[0\"qFGHI\033[?2J").assertLinesAre("ABC", "DE ", " ");
}
/** http://vt100.net/docs/vt510-rm/DECSEL */
public void testSelectiveEraseInLine() {
// ${CSI}1"q enables protection, ${CSI}0"q disables it.
// ${CSI}?${0,1,2}K" erases (0=cursor to end, 1=start to cursor, 2=complete line).
withTerminalSized(3, 3).enterString("ABCDEFGHI\033[?2K").assertLinesAre("ABC", "DEF", " ");
withTerminalSized(3, 3).enterString("ABCDE\033[?0KFGHI").assertLinesAre("ABC", "DEF", "GHI");
withTerminalSized(3, 3).enterString("ABCDE\033[?1KFGHI").assertLinesAre("ABC", " F", "GHI");
withTerminalSized(3, 3).enterString("ABCDE\033[?2KFGHI").assertLinesAre("ABC", " F", "GHI");
withTerminalSized(3, 3).enterString("ABCDEFGHI\033[2;2H\033[?0K").assertLinesAre("ABC", "D ", "GHI");
withTerminalSized(3, 3).enterString("ABC\033[1\"qD\033[0\"qE\033[?2KFGHI").assertLinesAre("ABC", "D F", "GHI");
}
/** http://www.vt100.net/docs/vt510-rm/DECSERA */
public void testSelectiveEraseInRectangle() {
// ${CSI}1"q enables protection, ${CSI}0"q disables it.
// ${CSI}?${TOP};${LEFT};${BOTTOM};${RIGHT}${" erases.
withTerminalSized(3, 3).enterString("ABCDEFGHI\033[${").assertLinesAre(" ", " ", " ");
withTerminalSized(3, 3).enterString("ABCDEFGHI\033[1;1;2;10${").assertLinesAre(" ", " ", "GHI");
withTerminalSized(3, 3).enterString("ABCDEFGHI\033[2;1;3;10${").assertLinesAre("ABC", " ", " ");
withTerminalSized(3, 3).enterString("ABCDEFGHI\033[1;1;100;1${").assertLinesAre(" BC", " EF", " HI");
withTerminalSized(3, 3).enterString("ABCDEFGHI\033[1;1;100;2${").assertLinesAre(" C", " F", " I");
withTerminalSized(3, 3).enterString("ABCDEFGHI\033[100;1;100;2${").assertLinesAre("ABC", "DEF", "GHI");
withTerminalSized(3, 3).enterString("ABCD\033[1\"qE\033[0\"qFGHI\033[${").assertLinesAre(" ", " E ", " ");
withTerminalSized(3, 3).enterString("ABCD\033[1\"qE\033[0\"qFGHI\033[1;1;2;10${").assertLinesAre(" ", " E ", "GHI");
}
/** http://vt100.net/docs/vt510-rm/DECCRA */
public void testRectangularCopy() {
// "${CSI}${SRC_TOP};${SRC_LEFT};${SRC_BOTTOM};${SRC_RIGHT};${SRC_PAGE};${DST_TOP};${DST_LEFT};${DST_PAGE}\$v"
withTerminalSized(7, 3).enterString("ABC\r\nDEF\r\nGHI\033[1;1;2;2;1;2;5;1$v").assertLinesAre("ABC ", "DEF AB ", "GHI DE ");
withTerminalSized(7, 3).enterString("ABC\r\nDEF\r\nGHI\033[1;1;3;3;1;1;4;1$v").assertLinesAre("ABCABC ", "DEFDEF ", "GHIGHI ");
withTerminalSized(7, 3).enterString("ABC\r\nDEF\r\nGHI\033[1;1;3;3;1;1;3;1$v").assertLinesAre("ABABC ", "DEDEF ", "GHGHI ");
withTerminalSized(7, 3).enterString(" ABC\r\n DEF\r\n GHI\033[1;4;3;6;1;1;1;1$v").assertLinesAre("ABCABC ", "DEFDEF ",
"GHIGHI ");
withTerminalSized(7, 3).enterString(" ABC\r\n DEF\r\n GHI\033[1;4;3;6;1;1;2;1$v").assertLinesAre(" ABCBC ", " DEFEF ",
" GHIHI ");
withTerminalSized(3, 3).enterString("ABC\r\nDEF\r\nGHI\033[1;1;2;2;1;2;2;1$v").assertLinesAre("ABC", "DAB", "GDE");
// Enable ${CSI}?6h origin mode (DECOM) and ${CSI}?69h for left/right margin (DECLRMM) enabling, ${CSI}${LEFTMARGIN};${RIGHTMARGIN}s
// for DECSLRM margin setting.
withTerminalSized(5, 5).enterString("\033[?6h\033[?69h\033[2;4s");
enterString("ABCDEFGHIJK").assertLinesAre(" ABC ", " DEF ", " GHI ", " JK ", " ");
enterString("\033[1;1;2;2;1;2;2;1$v").assertLinesAre(" ABC ", " DAB ", " GDE ", " JK ", " ");
}
/** http://vt100.net/docs/vt510-rm/DECCARA */
public void testChangeAttributesInRectangularArea() {
final int b = TextStyle.CHARACTER_ATTRIBUTE_BOLD;
// "${CSI}${TOP};${LEFT};${BOTTOM};${RIGHT};${ATTRIBUTES}\$r"
withTerminalSized(3, 3).enterString("ABCDEFGHI\033[1;1;2;2;1$r").assertLinesAre("ABC", "DEF", "GHI");
assertEffectAttributesSet(effectLine(b, b, b), effectLine(b, b, 0), effectLine(0, 0, 0));
// Now with http://www.vt100.net/docs/vt510-rm/DECSACE ("${CSI}2*x") specifying rectangle:
withTerminalSized(3, 3).enterString("\033[2*xABCDEFGHI\033[1;1;2;2;1$r").assertLinesAre("ABC", "DEF", "GHI");
assertEffectAttributesSet(effectLine(b, b, 0), effectLine(b, b, 0), effectLine(0, 0, 0));
}
/** http://vt100.net/docs/vt510-rm/DECCARA */
public void testReverseAttributesInRectangularArea() {
final int b = TextStyle.CHARACTER_ATTRIBUTE_BOLD;
final int u = TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE;
final int bu = TextStyle.CHARACTER_ATTRIBUTE_BOLD | TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE;
// "${CSI}${TOP};${LEFT};${BOTTOM};${RIGHT};${ATTRIBUTES}\$t"
withTerminalSized(3, 3).enterString("ABCDEFGHI\033[1;1;2;2;1$t").assertLinesAre("ABC", "DEF", "GHI");
assertEffectAttributesSet(effectLine(b, b, b), effectLine(b, b, 0), effectLine(0, 0, 0));
// Now with http://www.vt100.net/docs/vt510-rm/DECSACE ("${CSI}2*x") specifying rectangle:
withTerminalSized(3, 3).enterString("\033[2*xABCDEFGHI\033[1;1;2;2;1$t").assertLinesAre("ABC", "DEF", "GHI");
assertEffectAttributesSet(effectLine(b, b, 0), effectLine(b, b, 0), effectLine(0, 0, 0));
// Check reversal by initially bolding the B:
withTerminalSized(3, 3).enterString("\033[2*xA\033[1mB\033[0mCDEFGHI\033[1;1;2;2;1$t").assertLinesAre("ABC", "DEF", "GHI");
assertEffectAttributesSet(effectLine(b, 0, 0), effectLine(b, b, 0), effectLine(0, 0, 0));
// Check reversal by initially underlining A, bolding B, then reversing both bold and underline:
withTerminalSized(3, 3).enterString("\033[2*x\033[4mA\033[0m\033[1mB\033[0mCDEFGHI\033[1;1;2;2;1;4$t").assertLinesAre("ABC", "DEF",
"GHI");
assertEffectAttributesSet(effectLine(b, u, 0), effectLine(bu, bu, 0), effectLine(0, 0, 0));
}
}

View File

@@ -0,0 +1,212 @@
package com.termux.terminal;
public class ResizeTest extends TerminalTestCase {
public void testResizeWhenHasHistory() {
final int cols = 3;
withTerminalSized(cols, 3).enterString("111222333444555666777888999").assertCursorAt(2, 2).assertLinesAre("777", "888", "999");
resize(cols, 5).assertCursorAt(4, 2).assertLinesAre("555", "666", "777", "888", "999");
resize(cols, 3).assertCursorAt(2, 2).assertLinesAre("777", "888", "999");
}
public void testResizeWhenInAltBuffer() {
final int rows = 3, cols = 3;
withTerminalSized(cols, rows).enterString("a\r\ndef$").assertLinesAre("a ", "def", "$ ").assertCursorAt(2, 1);
// Resize and back again while in main buffer:
resize(cols, 5).assertLinesAre("a ", "def", "$ ", " ", " ").assertCursorAt(2, 1);
resize(cols, rows).assertLinesAre("a ", "def", "$ ").assertCursorAt(2, 1);
// Switch to alt buffer:
enterString("\033[?1049h").assertLinesAre(" ", " ", " ").assertCursorAt(2, 1);
enterString("h").assertLinesAre(" ", " ", " h ").assertCursorAt(2, 2);
resize(cols, 5).resize(cols, rows);
// Switch from alt buffer:
enterString("\033[?1049l").assertLinesAre("a ", "def", "$ ").assertCursorAt(2, 1);
}
public void testShrinkingInAltBuffer() {
final int rows = 5;
final int cols = 3;
withTerminalSized(cols, rows).enterString("A\r\nB\r\nC\r\nD\r\nE").assertLinesAre("A ", "B ", "C ", "D ", "E ");
enterString("\033[?1049h").assertLinesAre(" ", " ", " ", " ", " ");
resize(3, 3).enterString("\033[?1049lF").assertLinesAre("C ", "D ", "EF ");
}
public void testResizeAfterNewlineWhenInAltBuffer() {
final int rows = 3;
final int cols = 3;
withTerminalSized(cols, rows);
enterString("a\r\nb\r\nc\r\nd\r\ne\r\nf\r\n").assertLinesAre("e ", "f ", " ").assertCursorAt(2, 0);
assertLineWraps(false, false, false);
// Switch to alt buffer:
enterString("\033[?1049h").assertLinesAre(" ", " ", " ").assertCursorAt(2, 0);
enterString("h").assertLinesAre(" ", " ", "h ").assertCursorAt(2, 1);
// Grow by two rows:
resize(cols, 5).assertLinesAre(" ", " ", "h ", " ", " ").assertCursorAt(2, 1);
resize(cols, rows).assertLinesAre(" ", " ", "h ").assertCursorAt(2, 1);
// Switch from alt buffer:
enterString("\033[?1049l").assertLinesAre("e ", "f ", " ").assertCursorAt(2, 0);
}
public void testResizeAfterHistoryWraparound() {
final int rows = 3;
final int cols = 10;
withTerminalSized(cols, rows);
StringBuilder buffer = new StringBuilder();
for (int i = 0; i < 1000; i++) {
String s = Integer.toString(i);
enterString(s);
buffer.setLength(0);
buffer.append(s);
while (buffer.length() < cols)
buffer.append(' ');
if (i > rows) {
assertLineIs(rows - 1, buffer.toString());
}
enterString("\r\n");
}
assertLinesAre("998 ", "999 ", " ");
mTerminal.resize(cols, 2);
assertLinesAre("999 ", " ");
mTerminal.resize(cols, 5);
assertLinesAre("996 ", "997 ", "998 ", "999 ", " ");
mTerminal.resize(cols, rows);
assertLinesAre("998 ", "999 ", " ");
}
public void testVerticalResize() {
final int rows = 5;
final int cols = 3;
withTerminalSized(cols, rows);
// Foreground color to 119:
enterString("\033[38;5;119m");
// Background color to 129:
enterString("\033[48;5;129m");
// Clear with ED, Erase in Display:
enterString("\033[2J");
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
int style = getStyleAt(r, c);
assertEquals(119, TextStyle.decodeForeColor(style));
assertEquals(129, TextStyle.decodeBackColor(style));
}
}
enterString("11\r\n22");
assertLinesAre("11 ", "22 ", " ", " ", " ").assertLineWraps(false, false, false, false, false);
resize(cols, rows - 2).assertLinesAre("11 ", "22 ", " ");
// After resize, screen should still be same color:
for (int r = 0; r < rows - 2; r++) {
for (int c = 0; c < cols; c++) {
int style = getStyleAt(r, c);
assertEquals(119, TextStyle.decodeForeColor(style));
assertEquals(129, TextStyle.decodeBackColor(style));
}
}
// Background color to 200 and grow back size (which should be cleared to the new background color):
enterString("\033[48;5;200m");
resize(cols, rows);
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
int style = getStyleAt(r, c);
assertEquals(119, TextStyle.decodeForeColor(style));
assertEquals("wrong at row=" + r, r >= 3 ? 200 : 129, TextStyle.decodeBackColor(style));
}
}
}
public void testHorizontalResize() {
final int rows = 5;
final int cols = 5;
withTerminalSized(cols, rows);
// Background color to 129:
// enterString("\033[48;5;129m").assertLinesAre(" ", " ", " ", " ", " ");
enterString("1111\r\n2222\r\n3333\r\n4444\r\n5555").assertCursorAt(4, 4);
// assertEquals(129, TextStyle.decodeBackColor(getStyleAt(2, 2)));
assertLinesAre("1111 ", "2222 ", "3333 ", "4444 ", "5555 ").assertLineWraps(false, false, false, false, false);
resize(cols + 2, rows).assertLinesAre("1111 ", "2222 ", "3333 ", "4444 ", "5555 ").assertCursorAt(4, 4);
assertLineWraps(false, false, false, false, false);
resize(cols, rows).assertLinesAre("1111 ", "2222 ", "3333 ", "4444 ", "5555 ").assertCursorAt(4, 4);
assertLineWraps(false, false, false, false, false);
resize(cols - 1, rows).assertLinesAre("2222", "3333", "4444", "5555", " ").assertCursorAt(4, 0);
assertLineWraps(false, false, false, true, false);
resize(cols - 2, rows).assertLinesAre("3 ", "444", "4 ", "555", "5 ").assertCursorAt(4, 1);
assertLineWraps(false, true, false, true, false);
// Back to original size:
resize(cols, rows).assertLinesAre("1111 ", "2222 ", "3333 ", "4444 ", "5555 ").assertCursorAt(4, 4);
assertLineWraps(false, false, false, false, false);
}
public void testLineWrap() {
final int rows = 3, cols = 5;
withTerminalSized(cols, rows).enterString("111111").assertLinesAre("11111", "1 ", " ");
assertCursorAt(1, 1).assertLineWraps(true, false, false);
resize(7, rows).assertCursorAt(0, 6).assertLinesAre("111111 ", " ", " ").assertLineWraps(false, false, false);
resize(cols, rows).assertCursorAt(1, 1).assertLinesAre("11111", "1 ", " ").assertLineWraps(true, false, false);
enterString("2").assertLinesAre("11111", "12 ", " ").assertLineWraps(true, false, false);
enterString("123").assertLinesAre("11111", "12123", " ").assertLineWraps(true, false, false);
enterString("W").assertLinesAre("11111", "12123", "W ").assertLineWraps(true, true, false);
withTerminalSized(cols, rows).enterString("1234512345");
assertLinesAre("12345", "12345", " ").assertLineWraps(true, false, false);
enterString("W").assertLinesAre("12345", "12345", "W ").assertLineWraps(true, true, false);
}
public void testCursorPositionWhenShrinking() {
final int rows = 5, cols = 3;
withTerminalSized(cols, rows).enterString("$ ").assertLinesAre("$ ", " ", " ", " ", " ").assertCursorAt(0, 2);
resize(3, 3).assertLinesAre("$ ", " ", " ").assertCursorAt(0, 2);
resize(cols, rows).assertLinesAre("$ ", " ", " ", " ", " ").assertCursorAt(0, 2);
}
public void testResizeWithCombiningCharInLastColumn() {
withTerminalSized(3, 3).enterString("ABC\u0302DEF").assertLinesAre("ABC\u0302", "DEF", " ");
resize(4, 3).assertLinesAre("ABC\u0302D", "EF ", " ");
// Same as above but with colors:
withTerminalSized(3, 3).enterString("\033[37mA\033[35mB\033[33mC\u0302\033[32mD\033[31mE\033[34mF").assertLinesAre("ABC\u0302",
"DEF", " ");
resize(4, 3).assertLinesAre("ABC\u0302D", "EF ", " ");
assertForegroundIndices(effectLine(7, 5, 3, 2), effectLine(1, 4, 4, 4), effectLine(4, 4, 4, 4));
}
public void testResizeWithLineWrappingContinuing() {
withTerminalSized(5, 3).enterString("\r\nAB DE").assertLinesAre(" ", "AB DE", " ");
resize(4, 3).assertLinesAre("AB D", "E ", " ");
resize(3, 3).assertLinesAre("AB ", "DE ", " ");
resize(5, 3).assertLinesAre(" ", "AB DE", " ");
}
public void testResizeWithWideChars() {
final int rows = 3, cols = 4;
String twoCharsWidthOne = new String(Character.toChars(TerminalRowTest.TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_1));
withTerminalSized(cols, rows).enterString(twoCharsWidthOne).enterString("\r\n");
enterString(twoCharsWidthOne).assertLinesAre(twoCharsWidthOne + " ", twoCharsWidthOne + " ", " ");
resize(3, 3).assertLinesAre(twoCharsWidthOne + " ", twoCharsWidthOne + " ", " ");
enterString(twoCharsWidthOne).assertLinesAre(twoCharsWidthOne + " ", twoCharsWidthOne + twoCharsWidthOne + " ", " ");
}
public void testResizeWithMoreWideChars() {
final int rows = 4, cols = 5;
withTerminalSized(cols, rows).enterString("qqrr").assertLinesAre("qqrr ", " ", " ", " ");
resize(2, rows).assertLinesAre("qq", "rr", " ", " ");
resize(5, rows).assertLinesAre("qqrr ", " ", " ", " ");
withTerminalSized(cols, rows).enterString("").assertLinesAre(" ", " ", " ", " ");
resize(2, rows).assertLinesAre("", "", " ", " ");
resize(5, rows).assertLinesAre(" ", " ", " ", " ");
}
}

View File

@@ -0,0 +1,40 @@
package com.termux.terminal;
public class ScreenBufferTest extends TerminalTest {
public void testBasics() {
TerminalBuffer screen = new TerminalBuffer(5, 3, 3);
assertEquals("", screen.getTranscriptText());
screen.setChar(0, 0, 'a', 0);
assertEquals("a", screen.getTranscriptText());
screen.setChar(0, 0, 'b', 0);
assertEquals("b", screen.getTranscriptText());
screen.setChar(2, 0, 'c', 0);
assertEquals("b c", screen.getTranscriptText());
screen.setChar(2, 2, 'f', 0);
assertEquals("b c\n\n f", screen.getTranscriptText());
screen.blockSet(0, 0, 2, 2, 'X', 0);
}
public void testBlockSet() {
TerminalBuffer screen = new TerminalBuffer(5, 3, 3);
screen.blockSet(0, 0, 2, 2, 'X', 0);
assertEquals("XX\nXX", screen.getTranscriptText());
screen.blockSet(1, 1, 2, 2, 'Y', 0);
assertEquals("XX\nXYY\n YY", screen.getTranscriptText());
}
public void testGetSelectedText() {
withTerminalSized(5, 3).enterString("ABCDEFGHIJ").assertLinesAre("ABCDE", "FGHIJ", " ");
assertEquals("AB", mTerminal.getSelectedText(0, 0, 1, 0));
assertEquals("BC", mTerminal.getSelectedText(1, 0, 2, 0));
assertEquals("CDE", mTerminal.getSelectedText(2, 0, 4, 0));
assertEquals("FG", mTerminal.getSelectedText(0, 1, 1, 1));
assertEquals("GH", mTerminal.getSelectedText(1, 1, 2, 1));
assertEquals("HIJ", mTerminal.getSelectedText(2, 1, 4, 1));
assertEquals("ABCDEFG", mTerminal.getSelectedText(0, 0, 1, 1));
withTerminalSized(5, 3).enterString("ABCDE\r\nFGHIJ").assertLinesAre("ABCDE", "FGHIJ", " ");
assertEquals("ABCDE\nFG", mTerminal.getSelectedText(0, 0, 1, 1));
}
}

View File

@@ -0,0 +1,97 @@
package com.termux.terminal;
/**
* ${CSI}${top};${bottom}r" - set Scrolling Region [top;bottom] (default = full size of window) (DECSTBM).
*
* "DECSTBM moves the cursor to column 1, line 1 of the page" (http://www.vt100.net/docs/vt510-rm/DECSTBM).
*/
public class ScrollRegionTest extends TerminalTestCase {
public void testScrollRegionTop() {
withTerminalSized(3, 4).enterString("111222333444").assertLinesAre("111", "222", "333", "444");
enterString("\033[2r").assertCursorAt(0, 0);
enterString("\r\n\r\n\r\n\r\nCDEFGH").assertLinesAre("111", "444", "CDE", "FGH").assertHistoryStartsWith("333");
enterString("IJK").assertLinesAre("111", "CDE", "FGH", "IJK").assertHistoryStartsWith("444");
// Reset scroll region and enter line:
enterString("\033[r").enterString("\r\n\r\n\r\n").enterString("LMNOPQ").assertLinesAre("CDE", "FGH", "LMN", "OPQ");
}
public void testScrollRegionBottom() {
withTerminalSized(3, 4).enterString("111222333444");
assertLinesAre("111", "222", "333", "444");
enterString("\033[1;3r").assertCursorAt(0, 0);
enterString("\r\n\r\nCDEFGH").assertLinesAre("222", "CDE", "FGH", "444").assertHistoryStartsWith("111");
// Reset scroll region and enter line:
enterString("\033[r").enterString("\r\n\r\n\r\n").enterString("IJKLMN").assertLinesAre("CDE", "FGH", "IJK", "LMN");
}
public void testScrollRegionResetWithOriginMode() {
withTerminalSized(3, 4).enterString("111222333444");
assertLinesAre("111", "222", "333", "444");
// "\033[?6h" sets origin mode, so that the later DECSTBM resets cursor to below margin:
enterString("\033[?6h\033[2r").assertCursorAt(1, 0);
}
public void testScrollRegionLeft() {
// ${CSI}?69h for DECLRMM enabling, ${CSI}${LEFTMARGIN};${RIGHTMARGIN}s for DECSLRM margin setting.
withTerminalSized(3, 3).enterString("\033[?69h\033[2sABCDEFG").assertLinesAre("ABC", " DE", " FG");
enterString("HI").assertLinesAre("ADE", " FG", " HI").enterString("JK").assertLinesAre("AFG", " HI", " JK");
enterString("\n").assertLinesAre("AHI", " JK", " ");
}
public void testScrollRegionRight() {
// ${CSI}?69h for DECLRMM enabling, ${CSI}${LEFTMARGIN};${RIGHTMARGIN}s for DECSLRM margin setting.
withTerminalSized(3, 3).enterString("YYY\033[?69h\033[1;2sABCDEF").assertLinesAre("ABY", "CD ", "EF ");
enterString("GH").assertLinesAre("CDY", "EF ", "GH ").enterString("IJ").assertLinesAre("EFY", "GH ", "IJ ");
enterString("\n").assertLinesAre("GHY", "IJ ", " ");
}
public void testScrollRegionOnAllSides() {
// ${CSI}?69h for DECLRMM enabling, ${CSI}${LEFTMARGIN};${RIGHTMARGIN}s for DECSLRM margin setting.
withTerminalSized(4, 4).enterString("ABCDEFGHIJKLMNOP").assertLinesAre("ABCD", "EFGH", "IJKL", "MNOP");
// http://www.vt100.net/docs/vt510-rm/DECOM
enterString("\033[?6h\033[2;3r").assertCursorAt(1, 0);
enterString("\033[?69h\033[2;3s").assertCursorAt(1, 1);
enterString("QRST").assertLinesAre("ABCD", "EQRH", "ISTL", "MNOP");
enterString("UV").assertLinesAre("ABCD", "ESTH", "IUVL", "MNOP");
}
public void testDECCOLMResetsScrollMargin() {
// DECCOLM — Select 80 or 132 Columns per Page (http://www.vt100.net/docs/vt510-rm/DECCOLM) has the important
// side effect to clear scroll margins, which is useful for e.g. the "reset" utility to clear scroll margins.
withTerminalSized(3, 4).enterString("111222333444").assertLinesAre("111", "222", "333", "444");
enterString("\033[2r\033[?3h\r\nABCDEFGHIJKL").assertLinesAre("ABC", "DEF", "GHI", "JKL");
}
public void testScrollOutsideVerticalRegion() {
withTerminalSized(3, 4).enterString("\033[0;2rhi\033[4;0Hyou").assertLinesAre("hi ", " ", " ", "you");
//enterString("see").assertLinesAre("hi ", " ", " ", "see");
}
public void testNELRespectsLeftMargin() {
// vttest "Menu 11.3.2: VT420 Cursor-Movement Test", select "10. Test other movement (CR/HT/LF/FF) within margins".
// The NEL (ESC E) sequence moves cursor to first position on next line, where first position depends on origin mode and margin.
withTerminalSized(3, 3).enterString("\033[?69h\033[2sABC\033ED").assertLinesAre("ABC", "D ", " ");
withTerminalSized(3, 3).enterString("\033[?69h\033[2sABC\033[?6h\033ED").assertLinesAre("ABC", " D ", " ");
}
public void testBackwardIndex() {
// vttest "Menu 11.3.2: VT420 Cursor-Movement Test", test 7.
// Without margins:
withTerminalSized(3, 3).enterString("ABCDEF\0336H").assertLinesAre("ABC", "DHF", " ");
enterString("\0336\0336I").assertLinesAre("ABC", "IHF", " ");
enterString("\0336\0336").assertLinesAre(" AB", " IH", " ");
// With left margin:
withTerminalSized(3, 3).enterString("\033[?69h\033[2sABCDEF\0336\0336").assertLinesAre("A B", " D", " F");
}
public void testForwardIndex() {
// vttest "Menu 11.3.2: VT420 Cursor-Movement Test", test 8.
// Without margins:
withTerminalSized(3, 3).enterString("ABCD\0339E").assertLinesAre("ABC", "D E", " ");
enterString("\0339").assertLinesAre("BC ", " E ", " ");
// With right margin:
withTerminalSized(3, 3).enterString("\033[?69h\033[0;2sABCD\0339").assertLinesAre("B ", "D ", " ");
}
}

View File

@@ -0,0 +1,429 @@
package com.termux.terminal;
import java.util.Arrays;
import junit.framework.TestCase;
public class TerminalRowTest extends TestCase {
/** The properties of these code points are validated in {@link #testStaticConstants()}. */
private static final int ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1 = 0x679C;
private static final int ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2 = 0x679D;
private static final int TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_1 = 0x2070E;
private static final int TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_2 = 0x20731;
/** Unicode Character 'MUSICAL SYMBOL G CLEF' (U+1D11E). Two java chars required for this. */
static final int TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_1 = 0x1D11E;
/** Unicode Character 'MUSICAL SYMBOL G CLEF OTTAVA ALTA' (U+1D11F). Two java chars required for this. */
private static final int TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_2 = 0x1D11F;
private final int COLUMNS = 80;
/** A combining character. */
private static final int DIARESIS_CODEPOINT = 0x0308;
private TerminalRow row;
@Override
protected void setUp() throws Exception {
row = new TerminalRow(COLUMNS, TextStyle.NORMAL);
}
private void assertLineStartsWith(int... codePoints) {
char[] chars = row.mText;
int charIndex = 0;
for (int i = 0; i < codePoints.length; i++) {
int lineCodePoint = chars[charIndex++];
if (Character.isHighSurrogate((char) lineCodePoint)) {
lineCodePoint = Character.toCodePoint((char) lineCodePoint, chars[charIndex++]);
}
assertEquals("Differing a code point index=" + i, codePoints[i], lineCodePoint);
}
}
private void assertColumnCharIndicesStartsWith(int... indices) {
for (int i = 0; i < indices.length; i++) {
int expected = indices[i];
int actual = row.findStartOfColumn(i);
assertEquals("At index=" + i, expected, actual);
}
}
public void testSimpleDiaresis() {
row.setChar(0, DIARESIS_CODEPOINT, 0);
assertEquals(81, row.getSpaceUsed());
row.setChar(0, DIARESIS_CODEPOINT, 0);
assertEquals(82, row.getSpaceUsed());
assertLineStartsWith(' ', DIARESIS_CODEPOINT, DIARESIS_CODEPOINT, ' ');
}
public void testStaticConstants() {
assertEquals(1, Character.charCount(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1));
assertEquals(1, Character.charCount(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2));
assertEquals(2, WcWidth.width(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1));
assertEquals(2, WcWidth.width(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2));
assertEquals(2, Character.charCount(TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_1));
assertEquals(2, Character.charCount(TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_2));
assertEquals(1, WcWidth.width(TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_1));
assertEquals(1, WcWidth.width(TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_2));
assertEquals(2, Character.charCount(TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_1));
assertEquals(2, Character.charCount(TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_2));
assertEquals(2, WcWidth.width(TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_1));
assertEquals(2, WcWidth.width(TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_2));
assertEquals(1, Character.charCount(DIARESIS_CODEPOINT));
assertEquals(0, WcWidth.width(DIARESIS_CODEPOINT));
}
public void testOneColumn() {
assertEquals(0, row.findStartOfColumn(0));
row.setChar(0, 'a', 0);
assertEquals(0, row.findStartOfColumn(0));
}
public void testAscii() {
assertEquals(0, row.findStartOfColumn(0));
row.setChar(0, 'a', 0);
assertLineStartsWith('a', ' ', ' ');
assertEquals(1, row.findStartOfColumn(1));
assertEquals(80, row.getSpaceUsed());
row.setChar(0, 'b', 0);
assertEquals(1, row.findStartOfColumn(1));
assertEquals(2, row.findStartOfColumn(2));
assertEquals(80, row.getSpaceUsed());
assertColumnCharIndicesStartsWith(0, 1, 2, 3);
char[] someChars = new char[] { 'a', 'c', 'e', '4', '5', '6', '7', '8' };
char[] rawLine = new char[80];
Arrays.fill(rawLine, ' ');
for (int i = 0; i < 1000; i++) {
int lineIndex = (int) Math.random() * rawLine.length;
int charIndex = (int) Math.random() * someChars.length;
rawLine[lineIndex] = someChars[charIndex];
row.setChar(lineIndex, someChars[charIndex], 0);
}
char[] lineChars = row.mText;
for (int i = 0; i < rawLine.length; i++) {
assertEquals(rawLine[i], lineChars[i]);
}
}
public void testUnicode() {
assertEquals(0, row.findStartOfColumn(0));
assertEquals(80, row.getSpaceUsed());
row.setChar(0, TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_1, 0);
assertEquals(81, row.getSpaceUsed());
assertEquals(0, row.findStartOfColumn(0));
assertEquals(2, row.findStartOfColumn(1));
assertLineStartsWith(TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_1, ' ', ' ');
assertColumnCharIndicesStartsWith(0, 2, 3, 4);
row.setChar(0, 'a', 0);
assertEquals(80, row.getSpaceUsed());
assertEquals(0, row.findStartOfColumn(0));
assertEquals(1, row.findStartOfColumn(1));
assertLineStartsWith('a', ' ', ' ');
assertColumnCharIndicesStartsWith(0, 1, 2, 3);
row.setChar(0, TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_1, 0);
row.setChar(1, 'a', 0);
assertLineStartsWith(TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_1, 'a', ' ');
row.setChar(0, TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_1, 0);
row.setChar(1, TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_2, 0);
assertLineStartsWith(TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_1, TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_2, ' ');
assertColumnCharIndicesStartsWith(0, 2, 4, 5);
assertEquals(82, row.getSpaceUsed());
}
public void testDoubleWidth() {
row.setChar(0, 'a', 0);
row.setChar(1, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2, 0);
assertLineStartsWith('a', ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2, ' ');
assertColumnCharIndicesStartsWith(0, 1, 1, 2);
row.setChar(0, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, 0);
assertLineStartsWith(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, ' ', ' ');
assertColumnCharIndicesStartsWith(0, 0, 1, 2);
row.setChar(0, ' ', 0);
assertLineStartsWith(' ', ' ', ' ', ' ');
assertColumnCharIndicesStartsWith(0, 1, 2, 3, 4);
row.setChar(0, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, 0);
row.setChar(2, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2, 0);
assertLineStartsWith(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2);
assertColumnCharIndicesStartsWith(0, 0, 1, 1, 2);
row.setChar(0, 'a', 0);
assertLineStartsWith('a', ' ', ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2, ' ');
}
/** Just as {@link #testDoubleWidth()} but requires a surrogate pair. */
public void testDoubleWidthSurrogage() {
row.setChar(0, 'a', 0);
assertColumnCharIndicesStartsWith(0, 1, 2, 3, 4);
row.setChar(1, TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_2, 0);
assertColumnCharIndicesStartsWith(0, 1, 1, 3, 4);
assertLineStartsWith('a', TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_2, ' ');
row.setChar(0, TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_1, 0);
assertColumnCharIndicesStartsWith(0, 0, 2, 3, 4);
assertLineStartsWith(TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_1, ' ', ' ', ' ');
row.setChar(0, ' ', 0);
assertLineStartsWith(' ', ' ', ' ', ' ');
row.setChar(0, TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_1, 0);
row.setChar(1, TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_2, 0);
assertLineStartsWith(' ', TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_2, ' ');
row.setChar(0, 'a', 0);
assertLineStartsWith('a', TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_2, ' ');
}
public void testReplacementChar() {
row.setChar(0, TerminalEmulator.UNICODE_REPLACEMENT_CHAR, 0);
row.setChar(1, 'Y', 0);
assertLineStartsWith(TerminalEmulator.UNICODE_REPLACEMENT_CHAR, 'Y', ' ', ' ');
}
public void testSurrogateCharsWithNormalDisplayWidth() {
// These requires a UTF-16 surrogate pair, and has a display width of one.
int first = 0x1D306;
int second = 0x1D307;
// Assert the above statement:
assertEquals(2, Character.toChars(first).length);
assertEquals(2, Character.toChars(second).length);
row.setChar(0, second, 0);
assertEquals(second, Character.toCodePoint(row.mText[0], row.mText[1]));
assertEquals(' ', row.mText[2]);
assertEquals(2, row.findStartOfColumn(1));
row.setChar(0, first, 0);
assertEquals(first, Character.toCodePoint(row.mText[0], row.mText[1]));
assertEquals(' ', row.mText[2]);
assertEquals(2, row.findStartOfColumn(1));
row.setChar(1, second, 0);
row.setChar(2, 'a', 0);
assertEquals(first, Character.toCodePoint(row.mText[0], row.mText[1]));
assertEquals(second, Character.toCodePoint(row.mText[2], row.mText[3]));
assertEquals('a', row.mText[4]);
assertEquals(' ', row.mText[5]);
assertEquals(0, row.findStartOfColumn(0));
assertEquals(2, row.findStartOfColumn(1));
assertEquals(4, row.findStartOfColumn(2));
assertEquals(5, row.findStartOfColumn(3));
assertEquals(6, row.findStartOfColumn(4));
row.setChar(0, ' ', 0);
assertEquals(' ', row.mText[0]);
assertEquals(second, Character.toCodePoint(row.mText[1], row.mText[2]));
assertEquals('a', row.mText[3]);
assertEquals(' ', row.mText[4]);
assertEquals(0, row.findStartOfColumn(0));
assertEquals(1, row.findStartOfColumn(1));
assertEquals(3, row.findStartOfColumn(2));
assertEquals(4, row.findStartOfColumn(3));
assertEquals(5, row.findStartOfColumn(4));
for (int i = 0; i < 80; i++) {
row.setChar(i, i % 2 == 0 ? first : second, 0);
}
for (int i = 0; i < 80; i++) {
int idx = row.findStartOfColumn(i);
assertEquals(i % 2 == 0 ? first : second, Character.toCodePoint(row.mText[idx], row.mText[idx + 1]));
}
for (int i = 0; i < 80; i++) {
row.setChar(i, i % 2 == 0 ? 'a' : 'b', 0);
}
for (int i = 0; i < 80; i++) {
int idx = row.findStartOfColumn(i);
assertEquals(i, idx);
assertEquals(i % 2 == 0 ? 'a' : 'b', row.mText[i]);
}
}
public void testOverwritingDoubleDisplayWidthWithNormalDisplayWidth() {
// Initial "OO "
row.setChar(0, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, 0);
assertEquals(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, row.mText[0]);
assertEquals(' ', row.mText[1]);
assertEquals(0, row.findStartOfColumn(0));
assertEquals(0, row.findStartOfColumn(1));
assertEquals(1, row.findStartOfColumn(2));
// Setting first column to a clears second: "a "
row.setChar(0, 'a', 0);
assertEquals('a', row.mText[0]);
assertEquals(' ', row.mText[1]);
assertEquals(0, row.findStartOfColumn(0));
assertEquals(1, row.findStartOfColumn(1));
assertEquals(2, row.findStartOfColumn(2));
// Back to initial "OO "
row.setChar(0, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, 0);
assertEquals(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, row.mText[0]);
assertEquals(' ', row.mText[1]);
assertEquals(0, row.findStartOfColumn(0));
assertEquals(0, row.findStartOfColumn(1));
assertEquals(1, row.findStartOfColumn(2));
// Setting first column to a clears first: " a "
row.setChar(1, 'a', 0);
assertEquals(' ', row.mText[0]);
assertEquals('a', row.mText[1]);
assertEquals(' ', row.mText[2]);
assertEquals(0, row.findStartOfColumn(0));
assertEquals(1, row.findStartOfColumn(1));
assertEquals(2, row.findStartOfColumn(2));
}
public void testOverwritingDoubleDisplayWidthWithSelf() {
row.setChar(0, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, 0);
row.setChar(0, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, 0);
assertEquals(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, row.mText[0]);
assertEquals(' ', row.mText[1]);
assertEquals(0, row.findStartOfColumn(0));
assertEquals(0, row.findStartOfColumn(1));
assertEquals(1, row.findStartOfColumn(2));
}
public void testNormalCharsWithDoubleDisplayWidth() {
// These fit in one java char, and has a display width of two.
assertTrue(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1 != ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2);
assertEquals(1, Character.charCount(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1));
assertEquals(1, Character.charCount(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2));
assertEquals(2, WcWidth.width(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1));
assertEquals(2, WcWidth.width(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2));
row.setChar(0, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, 0);
assertEquals(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, row.mText[0]);
assertEquals(0, row.findStartOfColumn(1));
assertEquals(' ', row.mText[1]);
row.setChar(0, 'a', 0);
assertEquals('a', row.mText[0]);
assertEquals(' ', row.mText[1]);
assertEquals(1, row.findStartOfColumn(1));
row.setChar(0, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, 0);
assertEquals(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, row.mText[0]);
// The first character fills both first columns.
assertEquals(0, row.findStartOfColumn(1));
row.setChar(2, 'a', 0);
assertEquals(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, row.mText[0]);
assertEquals('a', row.mText[1]);
assertEquals(1, row.findStartOfColumn(2));
row.setChar(0, 'c', 0);
assertEquals('c', row.mText[0]);
assertEquals(' ', row.mText[1]);
assertEquals('a', row.mText[2]);
assertEquals(' ', row.mText[3]);
assertEquals(0, row.findStartOfColumn(0));
assertEquals(1, row.findStartOfColumn(1));
assertEquals(2, row.findStartOfColumn(2));
}
public void testNormalCharsWithDoubleDisplayWidthOverlapping() {
// These fit in one java char, and has a display width of two.
row.setChar(0, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, 0);
row.setChar(2, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2, 0);
row.setChar(4, 'a', 0);
// O = ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO
// A = ANOTHER_JAVA_CHAR_DISPLAY_WIDTH_TWO
// "OOAAa "
assertEquals(0, row.findStartOfColumn(0));
assertEquals(0, row.findStartOfColumn(1));
assertEquals(1, row.findStartOfColumn(2));
assertEquals(1, row.findStartOfColumn(3));
assertEquals(2, row.findStartOfColumn(4));
assertEquals(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, row.mText[0]);
assertEquals(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2, row.mText[1]);
assertEquals('a', row.mText[2]);
assertEquals(' ', row.mText[3]);
row.setChar(1, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2, 0);
// " AA a "
assertEquals(' ', row.mText[0]);
assertEquals(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2, row.mText[1]);
assertEquals(' ', row.mText[2]);
assertEquals('a', row.mText[3]);
assertEquals(' ', row.mText[4]);
assertEquals(0, row.findStartOfColumn(0));
assertEquals(1, row.findStartOfColumn(1));
assertEquals(1, row.findStartOfColumn(2));
assertEquals(2, row.findStartOfColumn(3));
assertEquals(3, row.findStartOfColumn(4));
}
// https://github.com/jackpal/Android-Terminal-Emulator/issues/145
public void testCrashATE145() {
// 0xC2541 is unassigned, use display width 1 for UNICODE_REPLACEMENT_CHAR.
// assertEquals(1, WcWidth.width(0xC2541));
assertEquals(2, Character.charCount(0xC2541));
assertEquals(2, WcWidth.width(0x73EE));
assertEquals(1, Character.charCount(0x73EE));
assertEquals(0, WcWidth.width(0x009F));
assertEquals(1, Character.charCount(0x009F));
int[] points = new int[] { 0xC2541, 'a', '8', 0x73EE, 0x009F, 0x881F, 0x8324, 0xD4C9, 0xFFFD, 'B', 0x009B, 0x61C9, 'Z' };
// int[] expected = new int[] { TerminalEmulator.UNICODE_REPLACEMENT_CHAR, 'a', '8', 0x73EE, 0x009F, 0x881F, 0x8324, 0xD4C9, 0xFFFD,
// 'B', 0x009B, 0x61C9, 'Z' };
int currentColumn = 0;
for (int i = 0; i < points.length; i++) {
row.setChar(currentColumn, points[i], 0);
currentColumn += WcWidth.width(points[i]);
}
// assertLineStartsWith(points);
// assertEquals(Character.highSurrogate(0xC2541), line.mText[0]);
// assertEquals(Character.lowSurrogate(0xC2541), line.mText[1]);
// assertEquals('a', line.mText[2]);
// assertEquals('8', line.mText[3]);
// assertEquals(Character.highSurrogate(0x73EE), line.mText[4]);
// assertEquals(Character.lowSurrogate(0x73EE), line.mText[5]);
//
// char[] chars = line.mText;
// int charIndex = 0;
// for (int i = 0; i < points.length; i++) {
// char c = chars[charIndex];
// charIndex++;
// int thisPoint = (int) c;
// if (Character.isHighSurrogate(c)) {
// thisPoint = Character.toCodePoint(c, chars[charIndex]);
// charIndex++;
// }
// assertEquals("At index=" + i + ", charIndex=" + charIndex + ", char=" + (char) thisPoint, points[i], thisPoint);
// }
}
public void testNormalization() {
// int lowerCaseN = 0x006E;
// int combiningTilde = 0x0303;
// int combined = 0x00F1;
row.setChar(0, 0x006E, 0);
assertEquals(80, row.getSpaceUsed());
row.setChar(0, 0x0303, 0);
assertEquals(81, row.getSpaceUsed());
// assertEquals("\u00F1 ", new String(term.getScreen().getLine(0)));
assertLineStartsWith(0x006E, 0x0303, ' ');
}
public void testInsertWideAtLastColumn() {
row.setChar(COLUMNS - 2, 'Z', 0);
row.setChar(COLUMNS - 1, 'a', 0);
assertEquals('Z', row.mText[row.findStartOfColumn(COLUMNS - 2)]);
assertEquals('a', row.mText[row.findStartOfColumn(COLUMNS - 1)]);
row.setChar(COLUMNS - 1, 'ö', 0);
assertEquals('Z', row.mText[row.findStartOfColumn(COLUMNS - 2)]);
assertEquals('ö', row.mText[row.findStartOfColumn(COLUMNS - 1)]);
// line.setChar(COLUMNS - 1, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1);
// assertEquals('Z', line.mText[line.findStartOfColumn(COLUMNS - 2)]);
// assertEquals(' ', line.mText[line.findStartOfColumn(COLUMNS - 1)]);
}
}

View File

@@ -0,0 +1,260 @@
package com.termux.terminal;
import java.io.UnsupportedEncodingException;
public class TerminalTest extends TerminalTestCase {
public void testCursorPositioning() throws Exception {
withTerminalSized(10, 10).placeCursorAndAssert(1, 2).placeCursorAndAssert(3, 5).placeCursorAndAssert(2, 2).enterString("A")
.assertCursorAt(2, 3);
}
public void testScreen() throws UnsupportedEncodingException {
withTerminalSized(3, 3);
assertLinesAre(" ", " ", " ");
assertEquals("", mTerminal.getScreen().getTranscriptText());
enterString("hi").assertLinesAre("hi ", " ", " ");
assertEquals("hi", mTerminal.getScreen().getTranscriptText());
enterString("\r\nu");
assertEquals("hi\nu", mTerminal.getScreen().getTranscriptText());
mTerminal.reset();
assertEquals("hi\nu", mTerminal.getScreen().getTranscriptText());
withTerminalSized(3, 3).enterString("hello");
assertEquals("hello", mTerminal.getScreen().getTranscriptText());
enterString("\r\nworld");
assertEquals("hello\nworld", mTerminal.getScreen().getTranscriptText());
}
public void testScrollDownInAltBuffer() {
withTerminalSized(3, 3).enterString("\033[?1049h");
enterString("\033[38;5;111m1\r\n");
enterString("\033[38;5;112m2\r\n");
enterString("\033[38;5;113m3\r\n");
enterString("\033[38;5;114m4\r\n");
enterString("\033[38;5;115m5");
assertLinesAre("3 ", "4 ", "5 ");
assertForegroundColorAt(0, 0, 113);
assertForegroundColorAt(1, 0, 114);
assertForegroundColorAt(2, 0, 115);
}
public void testMouseClick() throws Exception {
withTerminalSized(10, 10);
assertFalse(mTerminal.isMouseTrackingActive());
enterString("\033[?1000h");
assertTrue(mTerminal.isMouseTrackingActive());
enterString("\033[?1000l");
assertFalse(mTerminal.isMouseTrackingActive());
enterString("\033[?1000h");
assertTrue(mTerminal.isMouseTrackingActive());
enterString("\033[?1006h");
mTerminal.sendMouseEvent(TerminalEmulator.MOUSE_LEFT_BUTTON, 3, 4, true);
assertEquals("\033[<0;3;4M", mOutput.getOutputAndClear());
mTerminal.sendMouseEvent(TerminalEmulator.MOUSE_LEFT_BUTTON, 3, 4, false);
assertEquals("\033[<0;3;4m", mOutput.getOutputAndClear());
}
public void testNormalization() throws UnsupportedEncodingException {
// int lowerCaseN = 0x006E;
// int combiningTilde = 0x0303;
// int combined = 0x00F1;
withTerminalSized(3, 3).assertLinesAre(" ", " ", " ");
enterString("\u006E\u0303");
assertEquals(1, WcWidth.width("\u006E\u0303".toCharArray(), 0));
// assertEquals("\u00F1 ", new String(mTerminal.getScreen().getLine(0)));
assertLinesAre("\u006E\u0303 ", " ", " ");
}
/** On "\e[18t" xterm replies with "\e[8;${HEIGHT};${WIDTH}t" */
public void testReportTerminalSize() throws Exception {
withTerminalSized(5, 5);
assertEnteringStringGivesResponse("\033[18t", "\033[8;5;5t");
for (int width = 3; width < 12; width++) {
for (int height = 3; height < 12; height++) {
mTerminal.resize(width, height);
assertEnteringStringGivesResponse("\033[18t", "\033[8;" + height + ";" + width + "t");
}
}
}
/** Device Status Report (DSR) and Report Cursor Position (CPR). */
public void testDeviceStatusReport() throws Exception {
withTerminalSized(5, 5);
assertEnteringStringGivesResponse("\033[5n", "\033[0n");
assertEnteringStringGivesResponse("\033[6n", "\033[1;1R");
enterString("AB");
assertEnteringStringGivesResponse("\033[6n", "\033[1;3R");
enterString("\r\n");
assertEnteringStringGivesResponse("\033[6n", "\033[2;1R");
}
public void testSetCursorStyle() throws Exception {
withTerminalSized(5, 5);
assertEquals(TerminalEmulator.CURSOR_STYLE_BLOCK, mTerminal.getCursorStyle());
enterString("\033[3 q");
assertEquals(TerminalEmulator.CURSOR_STYLE_UNDERLINE, mTerminal.getCursorStyle());
enterString("\033[5 q");
assertEquals(TerminalEmulator.CURSOR_STYLE_BAR, mTerminal.getCursorStyle());
enterString("\033[0 q");
assertEquals(TerminalEmulator.CURSOR_STYLE_BLOCK, mTerminal.getCursorStyle());
enterString("\033[6 q");
assertEquals(TerminalEmulator.CURSOR_STYLE_BAR, mTerminal.getCursorStyle());
enterString("\033[4 q");
assertEquals(TerminalEmulator.CURSOR_STYLE_UNDERLINE, mTerminal.getCursorStyle());
enterString("\033[1 q");
assertEquals(TerminalEmulator.CURSOR_STYLE_BLOCK, mTerminal.getCursorStyle());
enterString("\033[4 q");
assertEquals(TerminalEmulator.CURSOR_STYLE_UNDERLINE, mTerminal.getCursorStyle());
enterString("\033[2 q");
assertEquals(TerminalEmulator.CURSOR_STYLE_BLOCK, mTerminal.getCursorStyle());
}
public void testPaste() {
withTerminalSized(5, 5);
mTerminal.paste("hi");
assertEquals("hi", mOutput.getOutputAndClear());
enterString("\033[?2004h");
mTerminal.paste("hi");
assertEquals("\033[200~" + "hi" + "\033[201~", mOutput.getOutputAndClear());
enterString("\033[?2004l");
mTerminal.paste("hi");
assertEquals("hi", mOutput.getOutputAndClear());
}
public void testSelectGraphics() {
withTerminalSized(5, 5);
enterString("\033[31m");
assertEquals(mTerminal.mForeColor, 1);
enterString("\033[32m");
assertEquals(mTerminal.mForeColor, 2);
enterString("\033[43m");
assertEquals(2, mTerminal.mForeColor);
assertEquals(3, mTerminal.mBackColor);
// SGR 0 should reset both foreground and background color.
enterString("\033[0m");
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
// 256 colors:
enterString("\033[38;5;119m");
assertEquals(119, mTerminal.mForeColor);
assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
enterString("\033[48;5;129m");
assertEquals(119, mTerminal.mForeColor);
assertEquals(129, mTerminal.mBackColor);
// Invalid parameter:
enterString("\033[48;8;129m");
assertEquals(119, mTerminal.mForeColor);
assertEquals(129, mTerminal.mBackColor);
// Multiple parameters at once:
enterString("\033[38;5;178;48;5;179;m");
assertEquals(178, mTerminal.mForeColor);
assertEquals(179, mTerminal.mBackColor);
}
public void testBackgroundColorErase() {
final int rows = 3;
final int cols = 3;
withTerminalSized(cols, rows);
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
int style = getStyleAt(r, c);
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, TextStyle.decodeForeColor(style));
assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, TextStyle.decodeBackColor(style));
}
}
// Foreground color to 119:
enterString("\033[38;5;119m");
// Background color to 129:
enterString("\033[48;5;129m");
// Clear with ED, Erase in Display:
enterString("\033[2J");
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
int style = getStyleAt(r, c);
assertEquals(119, TextStyle.decodeForeColor(style));
assertEquals(129, TextStyle.decodeBackColor(style));
}
}
// Background color to 139:
enterString("\033[48;5;139m");
// Insert two blank lines.
enterString("\033[2L");
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
int style = getStyleAt(r, c);
assertEquals((r == 0 || r == 1) ? 139 : 129, TextStyle.decodeBackColor(style));
}
}
withTerminalSized(cols, rows);
// Background color to 129:
enterString("\033[48;5;129m");
// Erase two characters, filling them with background color:
enterString("\033[2X");
assertEquals(129, TextStyle.decodeBackColor(getStyleAt(0, 0)));
assertEquals(129, TextStyle.decodeBackColor(getStyleAt(0, 1)));
assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, TextStyle.decodeBackColor(getStyleAt(0, 2)));
}
public void testParseColor() {
assertEquals(0xFF0000FA, TerminalColors.parse("#0000FA"));
assertEquals(0xFF000000, TerminalColors.parse("#000000"));
assertEquals(0xFF000000, TerminalColors.parse("#000"));
assertEquals(0xFF000000, TerminalColors.parse("#000000000"));
assertEquals(0xFF53186f, TerminalColors.parse("#53186f"));
assertEquals(0xFFFF00FF, TerminalColors.parse("rgb:F/0/F"));
assertEquals(0xFF0000FA, TerminalColors.parse("rgb:00/00/FA"));
assertEquals(0xFF53186f, TerminalColors.parse("rgb:53/18/6f"));
assertEquals(0, TerminalColors.parse("invalid_0000FA"));
assertEquals(0, TerminalColors.parse("#3456"));
}
/** The ncurses library still uses this. */
public void testLineDrawing() {
// 016 - shift out / G1. 017 - shift in / G0. "ESC ) 0" - use line drawing for G1
withTerminalSized(4, 2).enterString("q\033)0q\016q\017q").assertLinesAre("qq─q", " ");
// "\0337", saving cursor should save G0, G1 and invoked charset and "ESC 8" should restore.
withTerminalSized(4, 2).enterString("\033)0\016qqq\0337\017\0338q").assertLinesAre("────", " ");
}
public void testSoftTerminalReset() {
// See http://vt100.net/docs/vt510-rm/DECSTR and https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=650304
// "\033[?7l" is DECRST to disable wrap-around, and DECSTR ("\033[!p") should reset it.
withTerminalSized(3, 3).enterString("\033[?7lABCD").assertLinesAre("ABD", " ", " ");
enterString("\033[!pEF").assertLinesAre("ABE", "F ", " ");
}
public void testBel() {
withTerminalSized(3, 3);
assertEquals(0, mOutput.bellsRung);
enterString("\07");
assertEquals(1, mOutput.bellsRung);
enterString("hello\07");
assertEquals(2, mOutput.bellsRung);
enterString("\07hello");
assertEquals(3, mOutput.bellsRung);
enterString("hello\07world");
assertEquals(4, mOutput.bellsRung);
}
public void testAutomargins() throws UnsupportedEncodingException {
withTerminalSized(3, 3).enterString("abc").assertLinesAre("abc", " ", " ").assertCursorAt(0, 2);
enterString("d").assertLinesAre("abc", "d ", " ").assertCursorAt(1, 1);
withTerminalSized(3, 3).enterString("abc\r ").assertLinesAre(" bc", " ", " ").assertCursorAt(0, 1);
}
}

View File

@@ -0,0 +1,306 @@
package com.termux.terminal;
import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import junit.framework.AssertionFailedError;
import junit.framework.TestCase;
import com.termux.terminal.TerminalEmulator;
import com.termux.terminal.TerminalOutput;
public abstract class TerminalTestCase extends TestCase {
public static class MockTerminalOutput extends TerminalOutput {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
public final List<ChangedTitle> titleChanges = new ArrayList<>();
public final List<String> clipboardPuts = new ArrayList<>();
public int bellsRung = 0;
@Override
public void write(byte[] data, int offset, int count) {
baos.write(data, offset, count);
}
public String getOutputAndClear() {
try {
String result = new String(baos.toByteArray(), "UTF-8");
baos.reset();
return result;
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
@Override
public void titleChanged(String oldTitle, String newTitle) {
titleChanges.add(new ChangedTitle(oldTitle, newTitle));
}
@Override
public void clipboardText(String text) {
clipboardPuts.add(text);
}
@Override
public void onBell() {
bellsRung++;
}
}
public TerminalEmulator mTerminal;
public MockTerminalOutput mOutput;
public static class ChangedTitle {
final String oldTitle;
final String newTitle;
public ChangedTitle(String oldTitle, String newTitle) {
this.oldTitle = oldTitle;
this.newTitle = newTitle;
}
@Override
public boolean equals(Object o) {
ChangedTitle other = (ChangedTitle) o;
return Objects.equals(oldTitle, other.oldTitle) && Objects.equals(newTitle, other.newTitle);
}
@Override
public int hashCode() {
return Objects.hash(oldTitle, newTitle);
}
@Override
public String toString() {
return "ChangedTitle[oldTitle=" + oldTitle + ", newTitle=" + newTitle + "]";
}
}
public TerminalTestCase enterString(String s) {
byte[] bytes = s.getBytes(StandardCharsets.UTF_8);
mTerminal.append(bytes, bytes.length);
assertInvariants();
return this;
}
public void assertEnteringStringGivesResponse(String input, String expectedResponse) {
enterString(input);
String response = mOutput.getOutputAndClear();
assertEquals(expectedResponse, response);
}
@Override
protected void setUp() throws Exception {
mOutput = new MockTerminalOutput();
}
protected TerminalTestCase withTerminalSized(int columns, int rows) {
mTerminal = new TerminalEmulator(mOutput, columns, rows, rows * 2);
return this;
}
public void assertHistoryStartsWith(String... rows) {
assertTrue("About to check " + rows.length + " lines, but only " + mTerminal.getScreen().getActiveTranscriptRows() + " in history",
mTerminal.getScreen().getActiveTranscriptRows() >= rows.length);
for (int i = 0; i < rows.length; i++) {
assertLineIs(-i - 1, rows[i]);
}
}
private static class LineWrapper {
TerminalRow mLine;
public LineWrapper(TerminalRow line) {
mLine = line;
}
@Override
public int hashCode() {
return System.identityHashCode(mLine);
}
@Override
public boolean equals(Object o) {
return ((LineWrapper) o).mLine == mLine;
}
}
protected TerminalTestCase assertInvariants() {
TerminalBuffer screen = mTerminal.getScreen();
TerminalRow[] lines = screen.mLines;
Set<LineWrapper> linesSet = new HashSet<>();
for (int i = 0; i < lines.length; i++) {
if (lines[i] == null) continue;
assertTrue("Line exists at multiple places: " + i, linesSet.add(new LineWrapper(lines[i])));
char[] text = lines[i].mText;
int usedChars = lines[i].getSpaceUsed();
int currentColumn = 0;
for (int j = 0; j < usedChars; j++) {
char c = text[j];
int codePoint;
if (Character.isHighSurrogate(c)) {
char lowSurrogate = text[++j];
assertTrue("High surrogate without following low surrogate", Character.isLowSurrogate(lowSurrogate));
codePoint = Character.toCodePoint(c, lowSurrogate);
} else {
assertFalse("Low surrogate without preceding high surrogate", Character.isLowSurrogate(c));
codePoint = c;
}
assertFalse("Screen should never contain unassigned characters", Character.getType(codePoint) == Character.UNASSIGNED);
int width = WcWidth.width(codePoint);
assertFalse("The first column should not start with combining character", currentColumn == 0 && width < 0);
if (width > 0) currentColumn += width;
}
assertEquals("Line whose width does not match screens. line=" + new String(lines[i].mText, 0, lines[i].getSpaceUsed()),
screen.mColumns, currentColumn);
}
assertEquals("The alt buffer should have have no history", mTerminal.mAltBuffer.mTotalRows, mTerminal.mAltBuffer.mScreenRows);
if (mTerminal.isAlternateBufferActive()) {
assertEquals("The alt buffer should be the same size as the screen", mTerminal.mRows, mTerminal.mAltBuffer.mTotalRows);
}
return this;
}
protected void assertLineIs(int line, String expected) {
TerminalRow l = mTerminal.getScreen().allocateFullLineIfNecessary(mTerminal.getScreen().externalToInternalRow(line));
char[] chars = l.mText;
int textLen = l.getSpaceUsed();
if (textLen != expected.length()) fail("Expected '" + expected + "' (len=" + expected.length() + "), was='"
+ new String(chars, 0, textLen) + "' (len=" + textLen + ")");
for (int i = 0; i < textLen; i++) {
if (expected.charAt(i) != chars[i])
fail("Expected '" + expected + "', was='" + new String(chars, 0, textLen) + "' - first different at index=" + i);
}
}
public TerminalTestCase assertLinesAre(String... lines) {
assertEquals(lines.length, mTerminal.getScreen().mScreenRows);
for (int i = 0; i < lines.length; i++)
try {
assertLineIs(i, lines[i]);
} catch (AssertionFailedError e) {
throw new AssertionFailedError("Line: " + i + " - " + e.getMessage());
}
return this;
}
public TerminalTestCase resize(int cols, int rows) {
mTerminal.resize(cols, rows);
assertInvariants();
return this;
}
public TerminalTestCase assertLineWraps(boolean... lines) {
for (int i = 0; i < lines.length; i++)
assertEquals("line=" + i, lines[i], mTerminal.getScreen().mLines[mTerminal.getScreen().externalToInternalRow(i)].mLineWrap);
return this;
}
protected TerminalTestCase assertLineStartsWith(int line, int... codePoints) {
char[] chars = mTerminal.getScreen().mLines[mTerminal.getScreen().externalToInternalRow(line)].mText;
int charIndex = 0;
for (int i = 0; i < codePoints.length; i++) {
int lineCodePoint = chars[charIndex++];
if (Character.isHighSurrogate((char) lineCodePoint)) {
lineCodePoint = Character.toCodePoint((char) lineCodePoint, chars[charIndex++]);
}
assertEquals("Differing a code point index=" + i, codePoints[i], lineCodePoint);
}
return this;
}
protected TerminalTestCase placeCursorAndAssert(int row, int col) {
// +1 due to escape sequence being one based.
enterString("\033[" + (row + 1) + ";" + (col + 1) + "H");
assertCursorAt(row, col);
return this;
}
public TerminalTestCase assertCursorAt(int row, int col) {
int actualRow = mTerminal.getCursorRow();
int actualCol = mTerminal.getCursorCol();
if (!(row == actualRow && col == actualCol))
fail("Expected cursor at (row,col)=(" + row + ", " + col + ") but was (" + actualRow + ", " + actualCol + ")");
return this;
}
/** For testing only. Encoded style according to {@link TextStyle}. */
public int getStyleAt(int externalRow, int column) {
return mTerminal.getScreen().getStyleAt(externalRow, column);
}
public static class EffectLine {
final int[] styles;
public EffectLine(int[] styles) {
this.styles = styles;
}
}
protected EffectLine effectLine(int... bits) {
return new EffectLine(bits);
}
public TerminalTestCase assertEffectAttributesSet(EffectLine... lines) {
assertEquals(lines.length, mTerminal.getScreen().mScreenRows);
for (int i = 0; i < lines.length; i++) {
int[] line = lines[i].styles;
for (int j = 0; j < line.length; j++) {
int effectsAtCell = TextStyle.decodeEffect(getStyleAt(i, j));
int attributes = line[j];
if ((effectsAtCell & attributes) != attributes) fail("Line=" + i + ", column=" + j + ", expected "
+ describeStyle(attributes) + " set, was " + describeStyle(effectsAtCell));
}
}
return this;
}
public TerminalTestCase assertForegroundIndices(EffectLine... lines) {
assertEquals(lines.length, mTerminal.getScreen().mScreenRows);
for (int i = 0; i < lines.length; i++) {
int[] line = lines[i].styles;
for (int j = 0; j < line.length; j++) {
int actualColor = TextStyle.decodeForeColor(getStyleAt(i, j));
int expectedColor = line[j];
if (actualColor != expectedColor) fail("Line=" + i + ", column=" + j + ", expected color "
+ Integer.toHexString(expectedColor) + " set, was " + Integer.toHexString(actualColor));
}
}
return this;
}
private static String describeStyle(int styleBits) {
return "'" + ((styleBits & TextStyle.CHARACTER_ATTRIBUTE_BLINK) != 0 ? ":BLINK:" : "")
+ ((styleBits & TextStyle.CHARACTER_ATTRIBUTE_BOLD) != 0 ? ":BOLD:" : "")
+ ((styleBits & TextStyle.CHARACTER_ATTRIBUTE_INVERSE) != 0 ? ":INVERSE:" : "")
+ ((styleBits & TextStyle.CHARACTER_ATTRIBUTE_INVISIBLE) != 0 ? ":INVISIBLE:" : "")
+ ((styleBits & TextStyle.CHARACTER_ATTRIBUTE_ITALIC) != 0 ? ":ITALIC:" : "")
+ ((styleBits & TextStyle.CHARACTER_ATTRIBUTE_PROTECTED) != 0 ? ":PROTECTED:" : "")
+ ((styleBits & TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH) != 0 ? ":STRIKETHROUGH:" : "")
+ ((styleBits & TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE) != 0 ? ":UNDERLINE:" : "") + "'";
}
public void assertForegroundColorAt(int externalRow, int column, int color) {
int style = mTerminal.getScreen().mLines[mTerminal.getScreen().externalToInternalRow(externalRow)].getStyle(column);
assertEquals(color, TextStyle.decodeForeColor(style));
}
public TerminalTestCase assertColor(int colorIndex, int expected) {
int actual = mTerminal.mColors.mCurrentColors[colorIndex];
if (expected != actual) {
fail("Color index=" + colorIndex + ", expected=" + Integer.toHexString(expected) + ", was=" + Integer.toHexString(actual));
}
return this;
}
}

View File

@@ -0,0 +1,49 @@
package com.termux.terminal;
import junit.framework.TestCase;
public class TextStyleTest extends TestCase {
private static final int[] ALL_EFFECTS = new int[] { 0, TextStyle.CHARACTER_ATTRIBUTE_BOLD, TextStyle.CHARACTER_ATTRIBUTE_ITALIC,
TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE, TextStyle.CHARACTER_ATTRIBUTE_BLINK, TextStyle.CHARACTER_ATTRIBUTE_INVERSE,
TextStyle.CHARACTER_ATTRIBUTE_INVISIBLE, TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH, TextStyle.CHARACTER_ATTRIBUTE_PROTECTED,
TextStyle.CHARACTER_ATTRIBUTE_DIM };
public void testEncodingSingle() {
for (int fx : ALL_EFFECTS) {
for (int fg = 0; fg < TextStyle.NUM_INDEXED_COLORS; fg++) {
for (int bg = 0; bg < TextStyle.NUM_INDEXED_COLORS; bg++) {
int encoded = TextStyle.encode(fg, bg, fx);
assertEquals(fg, TextStyle.decodeForeColor(encoded));
assertEquals(bg, TextStyle.decodeBackColor(encoded));
assertEquals(fx, TextStyle.decodeEffect(encoded));
}
}
}
}
public void testEncodingCombinations() {
for (int f1 : ALL_EFFECTS) {
for (int f2 : ALL_EFFECTS) {
int combined = f1 | f2;
assertEquals(combined, TextStyle.decodeEffect(TextStyle.encode(0, 0, combined)));
}
}
}
public void testEncodingStrikeThrough() {
int encoded = TextStyle.encode(TextStyle.COLOR_INDEX_FOREGROUND, TextStyle.COLOR_INDEX_BACKGROUND,
TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH);
assertTrue((TextStyle.decodeEffect(encoded) | TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH) != 0);
}
public void testEncodingProtected() {
int encoded = TextStyle.encode(TextStyle.COLOR_INDEX_FOREGROUND, TextStyle.COLOR_INDEX_BACKGROUND,
TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH);
assertTrue((TextStyle.decodeEffect(encoded) & TextStyle.CHARACTER_ATTRIBUTE_PROTECTED) == 0);
encoded = TextStyle.encode(TextStyle.COLOR_INDEX_FOREGROUND, TextStyle.COLOR_INDEX_BACKGROUND,
TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH | TextStyle.CHARACTER_ATTRIBUTE_PROTECTED);
assertTrue((TextStyle.decodeEffect(encoded) & TextStyle.CHARACTER_ATTRIBUTE_PROTECTED) != 0);
}
}

View File

@@ -0,0 +1,87 @@
package com.termux.terminal;
import java.io.UnsupportedEncodingException;
public class UnicodeInputTest extends TerminalTestCase {
public void testIllFormedUtf8SuccessorByteNotConsumed() throws Exception {
// The Unicode Standard Version 6.2 Core Specification (http://www.unicode.org/versions/Unicode6.2.0/ch03.pdf):
// "If the converter encounters an ill-formed UTF-8 code unit sequence which starts with a valid first byte, but which does not
// continue with valid successor bytes (see Table 3-7), it must not consume the successor bytes as part of the ill-formed
// subsequence whenever those successor bytes themselves constitute part of a well-formed UTF-8 code unit subsequence."
withTerminalSized(5, 5);
mTerminal.append(new byte[] { (byte) 0b11101111, (byte) 'a' }, 2);
assertLineIs(0, ((char) TerminalEmulator.UNICODE_REPLACEMENT_CHAR) + "a ");
}
public void testUnassignedCodePoint() throws UnsupportedEncodingException {
withTerminalSized(3, 3);
// UTF-8 for U+C2541, an unassigned code point:
byte[] b = new byte[] { (byte) 0xf3, (byte) 0x82, (byte) 0x95, (byte) 0x81 };
mTerminal.append(b, b.length);
enterString("Y");
assertEquals(1, Character.charCount(TerminalEmulator.UNICODE_REPLACEMENT_CHAR));
assertLineStartsWith(0, TerminalEmulator.UNICODE_REPLACEMENT_CHAR, (int) 'Y', ' ');
}
public void testStuff() {
withTerminalSized(80, 24);
byte[] b = new byte[] { (byte) 0xf3, (byte) 0x82, (byte) 0x95, (byte) 0x81, (byte) 0x61, (byte) 0x38, (byte) 0xe7, (byte) 0x8f,
(byte) 0xae, (byte) 0xc2, (byte) 0x9f, (byte) 0xe8, (byte) 0xa0, (byte) 0x9f, (byte) 0xe8, (byte) 0x8c, (byte) 0xa4,
(byte) 0xed, (byte) 0x93, (byte) 0x89, (byte) 0xef, (byte) 0xbf, (byte) 0xbd, (byte) 0x42, (byte) 0xc2, (byte) 0x9b,
(byte) 0xe6, (byte) 0x87, (byte) 0x89, (byte) 0x5a };
mTerminal.append(b, b.length);
}
public void testSimpleCombining() throws Exception {
withTerminalSized(3, 2).enterString(" a\u0302 ").assertLinesAre(" a\u0302 ", " ");
}
public void testCombiningCharacterInFirstColumn() throws Exception {
withTerminalSized(5, 3).enterString("test\r\nhi\r\n").assertLinesAre("test ", "hi ", " ");
// U+0302 is COMBINING CIRCUMFLEX ACCENT. Test case from mosh (http://mosh.mit.edu/).
withTerminalSized(5, 5).enterString("test\r\nabc\r\n\u0302\r\ndef\r\n");
assertLinesAre("test ", "abc ", " \u0302 ", "def ", " ");
}
public void testCombiningCharacterInLastColumn() throws Exception {
withTerminalSized(3, 2).enterString(" a\u0302").assertLinesAre(" a\u0302", " ");
withTerminalSized(3, 2).enterString(" à̲").assertLinesAre(" à̲", " ");
withTerminalSized(3, 2).enterString("Aà̲F").assertLinesAre("Aà̲F", " ");
}
public void testWideCharacterInLastColumn() throws Exception {
withTerminalSized(3, 2).enterString("\u0302").assertLinesAre(" ", "\u0302 ");
withTerminalSized(3, 2).enterString("").assertLinesAre("", " ").assertCursorAt(0, 2);
enterString("a").assertLinesAre("", "a ");
}
public void testWideCharacterDeletion() throws Exception {
// CSI Ps D Cursor Backward Ps Times
withTerminalSized(3, 2).enterString("\033[Da").assertLinesAre(" a ", " ");
withTerminalSized(3, 2).enterString("\033[2Da").assertLinesAre("a ", " ");
withTerminalSized(3, 2).enterString("\033[2D枝").assertLinesAre("", " ");
withTerminalSized(3, 2).enterString("\033[1D枝").assertLinesAre("", " ");
withTerminalSized(5, 2).enterString("\033[Da").assertLinesAre(" 枝a ", " ");
withTerminalSized(5, 2).enterString("a \033[D\u0302").assertLinesAre("a\u0302 ", " ");
withTerminalSized(5, 2).enterString("\033[D\u0302").assertLinesAre("\u0302 ", " ");
enterString("Z").assertLinesAre("\u0302Z ", " ");
enterString("\033[D ").assertLinesAre("\u0302 ", " ");
// Go back two columns, standing at the second half of the wide character:
enterString("\033[2DU").assertLinesAre(" U ", " ");
}
public void testWideCharOverwriting() {
withTerminalSized(3, 2).enterString("abc\033[3D枝").assertLinesAre("枝c", " ");
}
public void testOverlongUtf8Encoding() throws Exception {
// U+0020 should be encoded as 0x20, 0xc0 0xa0 is an overlong encoding
// so should be replaced with the replacement char U+FFFD.
withTerminalSized(5, 5).mTerminal.append(new byte[] { (byte) 0xc0, (byte) 0xa0, 'Y' }, 3);
assertLineIs(0, "\uFFFDY ");
}
}

View File

@@ -0,0 +1,62 @@
package com.termux.terminal;
import junit.framework.TestCase;
public class WcWidthTest extends TestCase {
private static void assertWidthIs(int expectedWidth, int codePoint) {
int wcWidth = WcWidth.width(codePoint);
assertEquals(expectedWidth, wcWidth);
}
public void testPrintableAscii() {
for (int i = 0x20; i <= 0x7E; i++) {
assertWidthIs(1, i);
}
}
public void testSomeWide() {
assertWidthIs(2, '');
assertWidthIs(2, '');
assertWidthIs(2, '');
assertWidthIs(2, '中');
assertWidthIs(2, '文');
assertWidthIs(2, 0x679C);
assertWidthIs(2, 0x679D);
assertWidthIs(2, 0x2070E);
assertWidthIs(2, 0x20731);
assertWidthIs(1, 0x1F781);
}
public void testSomeNonWide() {
assertWidthIs(1, 0x1D11E);
assertWidthIs(1, 0x1D11F);
}
public void testCombining() {
assertWidthIs(0, 0x0302);
assertWidthIs(0, 0x0308);
}
public void testWatch() {
assertWidthIs(1, 0x231a);
}
public void testSofthyphen() {
// http://osdir.com/ml/internationalization.linux/2003-05/msg00006.html:
// "Existing implementation practice in terminals is that the SOFT HYPHEN is
// a spacing graphical character, and the purpose of my wcwidth() was to
// predict the advancement of the cursor position after a string is sent to
// a terminal. Hence, I have no choice but to keep wcwidth(SOFT HYPHEN) = 1.
// VT100-style terminals do not hyphenate."
assertWidthIs(1, 0x00AD);
}
public void testHangul() {
assertWidthIs(2, 0x11A3);
}
}

17
build-jnilibs.sh Executable file
View File

@@ -0,0 +1,17 @@
#!/bin/sh
# Script to build jni libraries while waiting for the new gradle build system:
# http://tools.android.com/tech-docs/new-build-system/gradle-experimental#TOC-Ndk-Integration
set -e -u
PROJECTDIR=`mktemp -d`
JNIDIR=$PROJECTDIR/jni
LIBSDIR=$PROJECTDIR/libs
mkdir $JNIDIR
cp app/src/main/jni/* $JNIDIR/
ndk-build NDK_PROJECT_PATH=$PROJECTDIR
cp -Rf $LIBSDIR/* app/src/main/jniLibs/
rm -Rf $PROJECTDIR

23
build.gradle Normal file
View File

@@ -0,0 +1,23 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.3.1'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}

18
gradle.properties Normal file
View File

@@ -0,0 +1,18 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx10248m -XX:MaxPermSize=256m
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,6 @@
#Thu Oct 22 22:50:58 CEST 2015
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.7-bin.zip

160
gradlew vendored Executable file
View File

@@ -0,0 +1,160 @@
#!/usr/bin/env bash
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >&-
APP_HOME="`pwd -P`"
cd "$SAVED" >&-
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"

90
gradlew.bat vendored Normal file
View File

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

1
settings.gradle Normal file
View File

@@ -0,0 +1 @@
include ':app'