Initial commit
41
.gitignore
vendored
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 *;
|
||||
#}
|
13
app/src/androidTest/java/com/termux/ApplicationTest.java
Normal 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);
|
||||
}
|
||||
}
|
49
app/src/main/AndroidManifest.xml
Normal 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>
|
229
app/src/main/assets/help.html
Normal 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 <query></dt><dd>Search among available packages.</dd>
|
||||
<dt>apt install <package></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 <package></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 <package></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 <package></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>
|
43
app/src/main/java/com/termux/app/DialogUtils.java
Normal 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();
|
||||
}
|
||||
|
||||
}
|
68
app/src/main/java/com/termux/app/FullScreenHelper.java
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
752
app/src/main/java/com/termux/app/TermuxActivity.java
Normal 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();
|
||||
}
|
||||
|
||||
}
|
46
app/src/main/java/com/termux/app/TermuxHelpActivity.java
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
193
app/src/main/java/com/termux/app/TermuxInstaller.java
Normal 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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
89
app/src/main/java/com/termux/app/TermuxPreferences.java
Normal 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();
|
||||
}
|
||||
|
||||
}
|
346
app/src/main/java/com/termux/app/TermuxService.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
1799
app/src/main/java/com/termux/drawer/DrawerLayout.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
1525
app/src/main/java/com/termux/drawer/ViewDragHelper.java
Normal file
10
app/src/main/java/com/termux/drawer/package-info.java
Normal 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;
|
||||
|
108
app/src/main/java/com/termux/terminal/ByteQueue.java
Normal 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;
|
||||
}
|
||||
}
|
10
app/src/main/java/com/termux/terminal/EmulatorDebug.java
Normal 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";
|
||||
|
||||
}
|
55
app/src/main/java/com/termux/terminal/JNI.java
Normal 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);
|
||||
|
||||
}
|
310
app/src/main/java/com/termux/terminal/KeyHandler.java
Normal 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;
|
||||
}
|
||||
}
|
435
app/src/main/java/com/termux/terminal/TerminalBuffer.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
102
app/src/main/java/com/termux/terminal/TerminalColorScheme.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
76
app/src/main/java/com/termux/terminal/TerminalColors.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
2290
app/src/main/java/com/termux/terminal/TerminalEmulator.java
Normal file
26
app/src/main/java/com/termux/terminal/TerminalOutput.java
Normal 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();
|
||||
|
||||
}
|
231
app/src/main/java/com/termux/terminal/TerminalRow.java
Normal 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];
|
||||
}
|
||||
|
||||
}
|
314
app/src/main/java/com/termux/terminal/TerminalSession.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
55
app/src/main/java/com/termux/terminal/TextStyle.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
108
app/src/main/java/com/termux/terminal/WcWidth.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
100
app/src/main/java/com/termux/view/GestureAndScaleRecognizer.java
Normal 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();
|
||||
}
|
||||
|
||||
}
|
20
app/src/main/java/com/termux/view/TerminalKeyListener.java
Normal 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);
|
||||
|
||||
}
|
232
app/src/main/java/com/termux/view/TerminalRenderer.java
Normal 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();
|
||||
}
|
||||
}
|
826
app/src/main/java/com/termux/view/TerminalView.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
5
app/src/main/jni/Android.mk
Normal file
@@ -0,0 +1,5 @@
|
||||
LOCAL_PATH:= $(call my-dir)
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE:= libtermux
|
||||
LOCAL_SRC_FILES:= termux.c
|
||||
include $(BUILD_SHARED_LIBRARY)
|
5
app/src/main/jni/Application.mk
Normal 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
@@ -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);
|
||||
}
|
BIN
app/src/main/jniLibs/armeabi-v7a/libtermux.so
Executable file
BIN
app/src/main/jniLibs/x86/libtermux.so
Executable file
BIN
app/src/main/res/drawable-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
app/src/main/res/drawable-hdpi/ic_service_notification.png
Normal file
After Width: | Height: | Size: 695 B |
BIN
app/src/main/res/drawable-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 786 B |
BIN
app/src/main/res/drawable-mdpi/ic_service_notification.png
Normal file
After Width: | Height: | Size: 597 B |
BIN
app/src/main/res/drawable-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
app/src/main/res/drawable-xhdpi/ic_service_notification.png
Normal file
After Width: | Height: | Size: 779 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_service_notification.png
Normal file
After Width: | Height: | Size: 983 B |
BIN
app/src/main/res/drawable-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
app/src/main/res/drawable-xxxhdpi/ic_service_notification.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
app/src/main/res/drawable/banner.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
4
app/src/main/res/drawable/current_session.xml
Normal 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>
|
@@ -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>
|
7
app/src/main/res/drawable/session_ripple.xml
Normal 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>
|
22
app/src/main/res/drawable/terminal_scroll_shape.xml
Normal 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>
|
58
app/src/main/res/layout/drawer_layout.xml
Normal 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>
|
9
app/src/main/res/layout/line_in_drawer.xml
Normal 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" />
|
61
app/src/main/res/values/strings.xml
Normal 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>
|
20
app/src/main/res/values/styles.xml
Normal 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>
|
54
app/src/test/java/com/termux/terminal/ByteQueueTest.java
Normal 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));
|
||||
}
|
||||
|
||||
}
|
@@ -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");
|
||||
}
|
||||
|
||||
}
|
175
app/src/test/java/com/termux/terminal/CursorAndScreenTest.java
Normal 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)));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
62
app/src/test/java/com/termux/terminal/DecSetTest.java
Normal 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());
|
||||
}
|
||||
|
||||
}
|
@@ -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");
|
||||
}
|
||||
|
||||
}
|
33
app/src/test/java/com/termux/terminal/HistoryTest.java
Normal 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");
|
||||
}
|
||||
|
||||
}
|
171
app/src/test/java/com/termux/terminal/KeyHandlerTest.java
Normal 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));
|
||||
}
|
||||
}
|
@@ -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\\");
|
||||
}
|
||||
|
||||
}
|
117
app/src/test/java/com/termux/terminal/RectangularAreasTest.java
Normal 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));
|
||||
}
|
||||
|
||||
}
|
212
app/src/test/java/com/termux/terminal/ResizeTest.java
Normal 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("QR").assertLinesAre("QR ", " ", " ", " ");
|
||||
resize(2, rows).assertLinesAre("Q", "R", " ", " ");
|
||||
resize(5, rows).assertLinesAre("QR ", " ", " ", " ");
|
||||
}
|
||||
|
||||
}
|
40
app/src/test/java/com/termux/terminal/ScreenBufferTest.java
Normal 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));
|
||||
}
|
||||
}
|
97
app/src/test/java/com/termux/terminal/ScrollRegionTest.java
Normal 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 ", " ");
|
||||
}
|
||||
|
||||
}
|
429
app/src/test/java/com/termux/terminal/TerminalRowTest.java
Normal 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)]);
|
||||
}
|
||||
|
||||
}
|
260
app/src/test/java/com/termux/terminal/TerminalTest.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
306
app/src/test/java/com/termux/terminal/TerminalTestCase.java
Normal 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;
|
||||
}
|
||||
}
|
49
app/src/test/java/com/termux/terminal/TextStyleTest.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
87
app/src/test/java/com/termux/terminal/UnicodeInputTest.java
Normal 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 ");
|
||||
}
|
||||
|
||||
}
|
62
app/src/test/java/com/termux/terminal/WcWidthTest.java
Normal 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, 'A');
|
||||
assertWidthIs(2, 'B');
|
||||
assertWidthIs(2, 'C');
|
||||
assertWidthIs(2, '中');
|
||||
assertWidthIs(2, '文');
|
||||
|
||||
assertWidthIs(2, 0x679C);
|
||||
assertWidthIs(2, 0x679D);
|
||||
|
||||
assertWidthIs(2, 0x2070E);
|
||||
assertWidthIs(2, 0x20731);
|
||||
|
||||
assertWidthIs(1, 0x1F781);
|
||||
}
|
||||
|
||||
public void testSomeNonWide() {
|
||||
assertWidthIs(1, 0x1D11E);
|
||||
assertWidthIs(1, 0x1D11F);
|
||||
}
|
||||
|
||||
public void testCombining() {
|
||||
assertWidthIs(0, 0x0302);
|
||||
assertWidthIs(0, 0x0308);
|
||||
}
|
||||
|
||||
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
@@ -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
@@ -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
@@ -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
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
@@ -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
@@ -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
@@ -0,0 +1 @@
|
||||
include ':app'
|