Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d69485b70b | ||
|
|
421dfcca39 | ||
|
|
3aaa0ab267 | ||
|
|
e7f9647beb | ||
|
|
5558f371b4 | ||
|
|
0882ed6470 | ||
|
|
5c02448521 | ||
|
|
17382fb190 | ||
|
|
d6eea83bfc | ||
|
|
51181c2d49 | ||
|
|
480b8a4f7e | ||
|
|
f989157f10 | ||
|
|
0e942f90a6 | ||
|
|
5b8eca46a1 | ||
|
|
493900d60b | ||
|
|
c6d6a63637 | ||
|
|
ca71265f23 | ||
|
|
46c9c4b80e | ||
|
|
6ca055bb25 | ||
|
|
ce7ad530cd | ||
|
|
d0015cbe82 | ||
|
|
9e19217f8f | ||
|
|
048af64093 | ||
|
|
a8f7bf1b6e | ||
|
|
62e229e184 | ||
|
|
36e4d94093 | ||
|
|
d2c9c5a0f0 | ||
|
|
6405180cb8 | ||
|
|
1b6919bb23 | ||
|
|
e52cd2dd41 | ||
|
|
54857d5fd4 | ||
|
|
38dd99e827 | ||
|
|
7256b04317 | ||
|
|
01a1c6de0f | ||
|
|
497fc3ecd0 | ||
|
|
b2b39abacd | ||
|
|
bee305e53f | ||
|
|
c8d2f28ed8 | ||
|
|
19eb371d23 | ||
|
|
6caaae4fd6 | ||
|
|
ed544102bc | ||
|
|
8f1ab1bc17 | ||
|
|
50337cbf9d | ||
|
|
fa9ea2db5c | ||
|
|
7a659ebd21 | ||
|
|
54bc1ed791 | ||
|
|
fe4365c94b | ||
|
|
0c13ea1bd4 | ||
|
|
862b461a07 | ||
|
|
845976be0f | ||
|
|
60bdaa3bf6 | ||
|
|
eeb873f4e4 | ||
|
|
207ddf9fdc | ||
|
|
5ca82ea095 | ||
|
|
913c474d32 | ||
|
|
657c270d97 | ||
|
|
7763931035 | ||
|
|
50005bc794 | ||
|
|
96f5ed985a | ||
|
|
c3aa9d9662 | ||
|
|
47634ca237 | ||
|
|
d9ec1bf40b | ||
|
|
bc1b742a36 | ||
|
|
86e2945069 | ||
|
|
961e06379b | ||
|
|
468185efb3 | ||
|
|
e006e36dd0 | ||
|
|
dd38965c46 | ||
|
|
79d56b778d | ||
|
|
2326c52199 | ||
|
|
f907684ef2 | ||
|
|
9d37461ac7 | ||
|
|
4de0f98fa4 | ||
|
|
f153a72592 | ||
|
|
b0f4efb0bc | ||
|
|
5c03c2d77e | ||
|
|
bce65f7db1 | ||
|
|
e18579164f | ||
|
|
16273a1981 | ||
|
|
ce82979e2b |
@@ -12,3 +12,5 @@ root = true
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,6 +5,7 @@
|
||||
# Built application files
|
||||
build/
|
||||
*.apk
|
||||
*.so
|
||||
|
||||
# Crashlytics configuations
|
||||
com_crashlytics_export_strings.xml
|
||||
@@ -30,6 +31,7 @@ local.properties
|
||||
.idea/modules.xml
|
||||
.idea/scopes/scope_settings.xml
|
||||
.idea/vcs.xml
|
||||
.idea/dictionaries/
|
||||
*.iml
|
||||
|
||||
# OS-specific files
|
||||
|
||||
8
.idea/codeStyleSettings.xml
generated
8
.idea/codeStyleSettings.xml
generated
@@ -63,9 +63,7 @@
|
||||
</extensions>
|
||||
</Objective-C-extensions>
|
||||
<XML>
|
||||
<option name="XML_KEEP_LINE_BREAKS" value="false" />
|
||||
<option name="XML_ALIGN_ATTRIBUTES" value="false" />
|
||||
<option name="XML_SPACE_INSIDE_EMPTY_TAG" value="true" />
|
||||
<option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
|
||||
</XML>
|
||||
<codeStyleSettings language="XML">
|
||||
<option name="FORCE_REARRANGE_MODE" value="1" />
|
||||
@@ -79,7 +77,7 @@
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:android</NAME>
|
||||
<XML_NAMESPACE />
|
||||
<XML_NAMESPACE>Namespace:</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
@@ -89,7 +87,7 @@
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:.*</NAME>
|
||||
<XML_NAMESPACE />
|
||||
<XML_NAMESPACE>Namespace:</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
|
||||
1
.idea/gradle.xml
generated
1
.idea/gradle.xml
generated
@@ -3,6 +3,7 @@
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="disableWrapperSourceDistributionNotification" value="true" />
|
||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="1.8" />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
Termux app
|
||||
==========
|
||||
[](https://travis-ci.org/termux/termux-app)
|
||||
[](https://gitter.im/termux/termux)
|
||||
|
||||
|
||||
Termux is an Android terminal app and Linux environment.
|
||||
@@ -19,7 +20,7 @@ Released under [the GPLv3 license](https://www.gnu.org/licenses/gpl.html). Conta
|
||||
|
||||
Building JNI libraries
|
||||
======================
|
||||
For ease of use, the JNI libraries are checked into version control. Execute the `build-jnilibs.sh` script to rebuild them.
|
||||
Execute the `build-jnilibs.sh` script to build the required JNI libraries.
|
||||
|
||||
Terminal resources
|
||||
==================
|
||||
|
||||
@@ -9,17 +9,17 @@ android {
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
jni.srcDirs = []
|
||||
}
|
||||
main {
|
||||
jni.srcDirs = []
|
||||
}
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.termux"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 23
|
||||
versionCode 21
|
||||
versionName "0.21"
|
||||
versionCode 32
|
||||
versionName "0.32"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
android:sharedUserId="com.termux"
|
||||
android:sharedUserLabel="@string/shared_user_label" >
|
||||
|
||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
@@ -30,21 +30,43 @@
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="com.termux.app.TermuxHelpActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/application_help" />
|
||||
android:theme="@android:style/Theme.Material.Light.DarkActionBar"
|
||||
android:parentActivityName=".app.TermuxActivity"
|
||||
android:label="@string/application_name" />
|
||||
|
||||
<activity
|
||||
android:name="com.termux.filepicker.TermuxFileReceiverActivity"
|
||||
android:label="@string/application_name"
|
||||
android:taskAffinity="com.termux.filereceiver"
|
||||
android:excludeFromRecents="true"
|
||||
android:noHistory="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="application/*" />
|
||||
<data android:mimeType="audio/*" />
|
||||
<data android:mimeType="image/*" />
|
||||
<data android:mimeType="message/*" />
|
||||
<data android:mimeType="multipart/*" />
|
||||
<data android:mimeType="text/*" />
|
||||
<data android:mimeType="video/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="com.termux.filepicker.TermuxFilePickerActivity"
|
||||
android:label="@string/application_name"
|
||||
android:theme="@android:style/Theme.Material"
|
||||
android:theme="@android:style/Theme.Material.Light.DarkActionBar"
|
||||
android:noHistory="true">
|
||||
<intent-filter>
|
||||
<!--
|
||||
|
||||
@@ -1,229 +0,0 @@
|
||||
<!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>
|
||||
@@ -3,21 +3,42 @@ package com.termux.app;
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.text.Selection;
|
||||
import android.util.TypedValue;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.ViewGroup.LayoutParams;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
final class DialogUtils {
|
||||
public final class DialogUtils {
|
||||
|
||||
public interface TextSetListener {
|
||||
void onTextSet(String text);
|
||||
}
|
||||
|
||||
static void textInput(Activity activity, int titleText, int positiveButtonText, String initialText, final TextSetListener onPositive) {
|
||||
public static void textInput(Activity activity, int titleText, String initialText,
|
||||
int positiveButtonText, final TextSetListener onPositive,
|
||||
int neutralButtonText, final TextSetListener onNeutral,
|
||||
int negativeButtonText, final TextSetListener onNegative,
|
||||
final DialogInterface.OnDismissListener onDismiss) {
|
||||
final EditText input = new EditText(activity);
|
||||
input.setSingleLine();
|
||||
if (initialText != null) input.setText(initialText);
|
||||
if (initialText != null) {
|
||||
input.setText(initialText);
|
||||
Selection.setSelection(input.getText(), initialText.length());
|
||||
}
|
||||
|
||||
final AlertDialog[] dialogHolder = new AlertDialog[1];
|
||||
input.setImeActionLabel(activity.getResources().getString(positiveButtonText), KeyEvent.KEYCODE_ENTER);
|
||||
input.setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
||||
@Override
|
||||
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
|
||||
onPositive.onTextSet(input.getText().toString());
|
||||
dialogHolder[0].dismiss();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
float dipInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, activity.getResources().getDisplayMetrics());
|
||||
// https://www.google.com/design/spec/components/dialogs.html#dialogs-specs
|
||||
@@ -27,17 +48,43 @@ final class DialogUtils {
|
||||
LinearLayout layout = new LinearLayout(activity);
|
||||
layout.setOrientation(LinearLayout.VERTICAL);
|
||||
layout.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
|
||||
// layout.setGravity(Gravity.CLIP_VERTICAL);
|
||||
layout.setPadding(paddingTopAndSides, paddingTopAndSides, paddingTopAndSides, paddingBottom);
|
||||
layout.addView(input);
|
||||
|
||||
new AlertDialog.Builder(activity).setTitle(titleText).setView(layout).setPositiveButton(positiveButtonText, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface d, int whichButton) {
|
||||
onPositive.onTextSet(input.getText().toString());
|
||||
}
|
||||
}).setNegativeButton(android.R.string.cancel, null).show();
|
||||
input.requestFocus();
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity)
|
||||
.setTitle(titleText).setView(layout)
|
||||
.setPositiveButton(positiveButtonText, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface d, int whichButton) {
|
||||
onPositive.onTextSet(input.getText().toString());
|
||||
}
|
||||
});
|
||||
|
||||
if (onNeutral != null) {
|
||||
builder.setNeutralButton(neutralButtonText, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
onNeutral.onTextSet(input.getText().toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (onNegative == null) {
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
} else {
|
||||
builder.setNegativeButton(negativeButtonText, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
onNegative.onTextSet(input.getText().toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (onDismiss != null) builder.setOnDismissListener(onDismiss);
|
||||
|
||||
dialogHolder[0] = builder.create();
|
||||
dialogHolder[0].setCanceledOnTouchOutside(false);
|
||||
dialogHolder[0].show();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,19 +1,8 @@
|
||||
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.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.ActivityNotFoundException;
|
||||
@@ -27,11 +16,15 @@ import android.content.DialogInterface.OnShowListener;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Typeface;
|
||||
import android.media.AudioAttributes;
|
||||
import android.media.SoundPool;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.os.Vibrator;
|
||||
@@ -64,6 +57,19 @@ import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
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 java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* A terminal emulator activity.
|
||||
*
|
||||
@@ -76,7 +82,8 @@ import android.widget.Toast;
|
||||
*/
|
||||
public final class TermuxActivity extends Activity implements ServiceConnection {
|
||||
|
||||
private static final int CONTEXTMENU_SELECT_ID = 0;
|
||||
private static final int CONTEXTMENU_SELECT_URL_ID = 0;
|
||||
private static final int CONTEXTMENU_SHARE_TRANSCRIPT_ID = 1;
|
||||
private static final int CONTEXTMENU_PASTE_ID = 3;
|
||||
private static final int CONTEXTMENU_KILL_PROCESS_ID = 4;
|
||||
private static final int CONTEXTMENU_RESET_TERMINAL_ID = 5;
|
||||
@@ -86,10 +93,12 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
|
||||
private static final int MAX_SESSIONS = 8;
|
||||
|
||||
private static final int REQUESTCODE_PERMISSION_STORAGE = 1234;
|
||||
|
||||
private static final String RELOAD_STYLE_ACTION = "com.termux.app.reload_style";
|
||||
|
||||
/** The main view of the activity showing the terminal. */
|
||||
@NonNull TerminalView mTerminalView;
|
||||
/** The main view of the activity showing the terminal. Initialized in onCreate(). */
|
||||
@SuppressWarnings("NullableProblems") @NonNull TerminalView mTerminalView;
|
||||
|
||||
final FullScreenHelper mFullScreenHelper = new FullScreenHelper(this);
|
||||
|
||||
@@ -114,17 +123,42 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
*/
|
||||
boolean mIsVisible;
|
||||
|
||||
private final SoundPool mBellSoundPool = new SoundPool.Builder().setMaxStreams(1).setAudioAttributes(
|
||||
new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||
.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build()).build();
|
||||
private int mBellSoundId;
|
||||
|
||||
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();
|
||||
if ("storage".equals(whatToReload)) {
|
||||
if (ensureStoragePermissionGranted()) TermuxInstaller.setupStorageSymlinks(TermuxActivity.this);
|
||||
return;
|
||||
}
|
||||
mTerminalView.checkForFontAndColors();
|
||||
mSettings.reloadFromProperties(TermuxActivity.this);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** For processes to access shared internal storage (/sdcard) we need this permission. */
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
public boolean ensureStoragePermissionGranted() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (checkSelfPermission(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
|
||||
return true;
|
||||
} else {
|
||||
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUESTCODE_PERMISSION_STORAGE);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// Always granted before Android 6.0.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
@@ -185,6 +219,9 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
getDrawer().closeDrawers();
|
||||
} else if (unicodeChar == 'f'/* full screen */) {
|
||||
toggleImmersive();
|
||||
} else if (unicodeChar == 'k'/* keyboard */) {
|
||||
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
|
||||
} else if (unicodeChar == 'm'/* menu */) {
|
||||
mTerminalView.showContextMenu();
|
||||
} else if (unicodeChar == 'r'/* rename */) {
|
||||
@@ -223,59 +260,54 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLongPress(MotionEvent event) {
|
||||
mTerminalView.showContextMenu();
|
||||
public void onSingleTapUp(MotionEvent e) {
|
||||
InputMethodManager mgr = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
mgr.showSoftInput(mTerminalView, InputMethodManager.SHOW_IMPLICIT);
|
||||
}
|
||||
|
||||
@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);
|
||||
public boolean shouldBackButtonBeMappedToEscape() {
|
||||
return mSettings.mBackIsEscape;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void copyModeChanged(boolean copyMode) {
|
||||
// Disable drawer while copying.
|
||||
getDrawer().setDrawerLockMode(copyMode ? DrawerLayout.LOCK_MODE_LOCKED_CLOSED : DrawerLayout.LOCK_MODE_UNLOCKED);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
findViewById(R.id.new_session_button).setOnClickListener(new OnClickListener() {
|
||||
View newSessionButton = findViewById(R.id.new_session_button);
|
||||
|
||||
newSessionButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
addNewSession(false, null);
|
||||
}
|
||||
});
|
||||
|
||||
findViewById(R.id.new_session_button).setOnLongClickListener(new OnLongClickListener() {
|
||||
@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;
|
||||
}
|
||||
});
|
||||
newSessionButton.setOnLongClickListener(new OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
DialogUtils.textInput(TermuxActivity.this, R.string.session_new_named_title, null, R.string.session_new_named_positive_button,
|
||||
new DialogUtils.TextSetListener() {
|
||||
@Override
|
||||
public void onTextSet(String text) {
|
||||
addNewSession(false, text);
|
||||
}
|
||||
}, R.string.new_session_failsafe, new DialogUtils.TextSetListener() {
|
||||
@Override
|
||||
public void onTextSet(String text) {
|
||||
addNewSession(true, text);
|
||||
}
|
||||
}
|
||||
, -1, null, null);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
findViewById(R.id.toggle_keyboard_button).setOnClickListener(new OnClickListener() {
|
||||
findViewById(R.id.toggle_keyboard_button).setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
@@ -291,8 +323,9 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
startService(serviceIntent);
|
||||
if (!bindService(serviceIntent, this, 0)) throw new RuntimeException("bindService() failed");
|
||||
|
||||
mTerminalView.checkForTypeface();
|
||||
mTerminalView.checkForColors();
|
||||
mTerminalView.checkForFontAndColors();
|
||||
|
||||
mBellSoundId = mBellSoundPool.load(this, R.raw.bell, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -342,14 +375,24 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
@Override
|
||||
public void onClipboardText(TerminalSession session, String text) {
|
||||
if (!mIsVisible) return;
|
||||
showToast("Clipboard set:\n\"" + text + "\"", true);
|
||||
showToast("Clipboard:\n\"" + text + "\"", false);
|
||||
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
clipboard.setPrimaryClip(new ClipData(null, new String[] { "text/plain" }, new ClipData.Item(text)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBell(TerminalSession session) {
|
||||
if (mIsVisible) ((Vibrator) getSystemService(VIBRATOR_SERVICE)).vibrate(50);
|
||||
if (mIsVisible) {
|
||||
switch (mSettings.mBellBehaviour) {
|
||||
case TermuxPreferences.BELL_BEEP:
|
||||
mBellSoundPool.play(mBellSoundId, 1.f, 1.f, 1, 0, 1.f);
|
||||
break;
|
||||
case TermuxPreferences.BELL_VIBRATE:
|
||||
((Vibrator) getSystemService(VIBRATOR_SERVICE)).vibrate(50);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -418,6 +461,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
TermuxInstaller.setupIfNeeded(TermuxActivity.this, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (mTermService == null) return; // Activity might have been destroyed.
|
||||
try {
|
||||
if (TermuxPreferences.isShowWelcomeDialog(TermuxActivity.this)) {
|
||||
new AlertDialog.Builder(TermuxActivity.this).setTitle(R.string.welcome_dialog_title).setMessage(R.string.welcome_dialog_body)
|
||||
@@ -447,14 +491,13 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
void renameSession(final TerminalSession sessionToRename) {
|
||||
DialogUtils.textInput(this, R.string.session_rename_title, R.string.session_rename_positive_button, sessionToRename.mSessionName,
|
||||
new DialogUtils.TextSetListener() {
|
||||
@Override
|
||||
public void onTextSet(String text) {
|
||||
sessionToRename.mSessionName = text;
|
||||
}
|
||||
});
|
||||
}
|
||||
DialogUtils.textInput(this, R.string.session_rename_title, sessionToRename.mSessionName, R.string.session_rename_positive_button, new DialogUtils.TextSetListener() {
|
||||
@Override
|
||||
public void onTextSet(String text) {
|
||||
sessionToRename.mSessionName = text;
|
||||
}
|
||||
}, -1, null, -1, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
@@ -480,6 +523,10 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
}
|
||||
|
||||
registerReceiver(mBroadcastReceiever, new IntentFilter(RELOAD_STYLE_ACTION));
|
||||
|
||||
// The current terminal session may have changed while being away, force
|
||||
// a refresh of the displayed terminal:
|
||||
mTerminalView.onScreenUpdated();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -566,9 +613,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
TerminalSession currentSession = getCurrentTermSession();
|
||||
if (currentSession == null) return;
|
||||
|
||||
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
menu.add(Menu.NONE, CONTEXTMENU_PASTE_ID, Menu.NONE, R.string.paste_text).setEnabled(clipboard.hasPrimaryClip());
|
||||
menu.add(Menu.NONE, CONTEXTMENU_SELECT_ID, Menu.NONE, R.string.select);
|
||||
menu.add(Menu.NONE, CONTEXTMENU_SELECT_URL_ID, Menu.NONE, R.string.select_url);
|
||||
menu.add(Menu.NONE, CONTEXTMENU_SHARE_TRANSCRIPT_ID, Menu.NONE, R.string.select_all_and_share);
|
||||
menu.add(Menu.NONE, CONTEXTMENU_RESET_TERMINAL_ID, Menu.NONE, R.string.reset_terminal);
|
||||
menu.add(Menu.NONE, CONTEXTMENU_KILL_PROCESS_ID, Menu.NONE, R.string.kill_process).setEnabled(currentSession.isRunning());
|
||||
menu.add(Menu.NONE, CONTEXTMENU_TOGGLE_FULLSCREEN_ID, Menu.NONE, R.string.toggle_fullscreen).setCheckable(true).setChecked(mSettings.isFullScreen());
|
||||
@@ -644,84 +690,78 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
|
||||
@Override
|
||||
public boolean onContextItemSelected(MenuItem item) {
|
||||
TerminalSession session = getCurrentTermSession();
|
||||
|
||||
switch (item.getItemId()) {
|
||||
case CONTEXTMENU_SELECT_ID:
|
||||
CharSequence[] items = new CharSequence[] { getString(R.string.select_text), getString(R.string.select_url),
|
||||
getString(R.string.select_all_and_share) };
|
||||
new AlertDialog.Builder(this).setItems(items, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
switch (which) {
|
||||
case 0:
|
||||
mTerminalView.toggleSelectingText();
|
||||
break;
|
||||
case 1:
|
||||
showUrlSelection();
|
||||
break;
|
||||
case 2:
|
||||
TerminalSession session = getCurrentTermSession();
|
||||
if (session != null) {
|
||||
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||
intent.setType("text/plain");
|
||||
intent.putExtra(Intent.EXTRA_TEXT, session.getEmulator().getScreen().getTranscriptText().trim());
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.share_transcript_title));
|
||||
startActivity(Intent.createChooser(intent, getString(R.string.share_transcript_chooser_title)));
|
||||
}
|
||||
break;
|
||||
case CONTEXTMENU_SELECT_URL_ID:
|
||||
showUrlSelection();
|
||||
return true;
|
||||
case CONTEXTMENU_SHARE_TRANSCRIPT_ID:
|
||||
if (session != null) {
|
||||
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||
intent.setType("text/plain");
|
||||
intent.putExtra(Intent.EXTRA_TEXT, session.getEmulator().getScreen().getTranscriptText().trim());
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.share_transcript_title));
|
||||
startActivity(Intent.createChooser(intent, getString(R.string.share_transcript_chooser_title)));
|
||||
}
|
||||
return true;
|
||||
case CONTEXTMENU_PASTE_ID:
|
||||
doPaste();
|
||||
return true;
|
||||
case CONTEXTMENU_KILL_PROCESS_ID:
|
||||
final AlertDialog.Builder b = new AlertDialog.Builder(this);
|
||||
b.setIcon(android.R.drawable.ic_dialog_alert);
|
||||
b.setMessage(R.string.confirm_kill_process);
|
||||
b.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
dialog.dismiss();
|
||||
getCurrentTermSession().finishIfRunning();
|
||||
}
|
||||
dialog.dismiss();
|
||||
});
|
||||
b.setNegativeButton(android.R.string.no, null);
|
||||
b.show();
|
||||
return true;
|
||||
case CONTEXTMENU_RESET_TERMINAL_ID: {
|
||||
if (session != null) {
|
||||
session.reset();
|
||||
showToast(getResources().getString(R.string.reset_toast_notification), true);
|
||||
}
|
||||
}).show();
|
||||
return true;
|
||||
case CONTEXTMENU_PASTE_ID:
|
||||
doPaste();
|
||||
return true;
|
||||
case CONTEXTMENU_KILL_PROCESS_ID:
|
||||
final AlertDialog.Builder b = new AlertDialog.Builder(this);
|
||||
b.setIcon(android.R.drawable.ic_dialog_alert);
|
||||
b.setMessage(R.string.confirm_kill_process);
|
||||
b.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
dialog.dismiss();
|
||||
getCurrentTermSession().finishIfRunning();
|
||||
}
|
||||
});
|
||||
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 | IllegalArgumentException e) {
|
||||
// The startActivity() call is not documented to throw IllegalArgumentException.
|
||||
// However, crash reporting shows that it sometimes does, so catch it here.
|
||||
new AlertDialog.Builder(this).setMessage(R.string.styling_not_installed)
|
||||
.setPositiveButton(R.string.styling_install, new android.content.DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://play.google.com/store/apps/details?id=com.termux.styling")));
|
||||
}
|
||||
}).setNegativeButton(android.R.string.cancel, null).show();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
case CONTEXTMENU_TOGGLE_FULLSCREEN_ID:
|
||||
toggleImmersive();
|
||||
return true;
|
||||
case CONTEXTMENU_HELP_ID:
|
||||
startActivity(new Intent(this, TermuxHelpActivity.class));
|
||||
return true;
|
||||
default:
|
||||
return super.onContextItemSelected(item);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
|
||||
if (requestCode == REQUESTCODE_PERMISSION_STORAGE && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
TermuxInstaller.setupStorageSymlinks(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,14 @@ import android.content.ActivityNotFoundException;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.ViewGroup;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.RelativeLayout;
|
||||
|
||||
/** Basic embedded browser for viewing the bundled help page. */
|
||||
/** Basic embedded browser for viewing help pages. */
|
||||
public final class TermuxHelpActivity extends Activity {
|
||||
|
||||
private WebView mWebView;
|
||||
@@ -16,22 +20,47 @@ public final class TermuxHelpActivity extends Activity {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
final RelativeLayout progressLayout = new RelativeLayout(this);
|
||||
RelativeLayout.LayoutParams lParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
lParams.addRule(RelativeLayout.CENTER_IN_PARENT);
|
||||
ProgressBar progressBar = new ProgressBar(this);
|
||||
progressBar.setIndeterminate(true);
|
||||
progressBar.setLayoutParams(lParams);
|
||||
progressLayout.addView(progressBar);
|
||||
|
||||
mWebView = new WebView(this);
|
||||
setContentView(mWebView);
|
||||
WebSettings settings = mWebView.getSettings();
|
||||
settings.setCacheMode(WebSettings.LOAD_NO_CACHE);
|
||||
settings.setAppCacheEnabled(false);
|
||||
setContentView(progressLayout);
|
||||
mWebView.clearCache(true);
|
||||
|
||||
mWebView.setWebViewClient(new WebViewClient() {
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
||||
if (url.startsWith("https://termux.com")) {
|
||||
// Inline help.
|
||||
setContentView(progressLayout);
|
||||
return false;
|
||||
}
|
||||
|
||||
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.
|
||||
// Android TV does not have a system browser.
|
||||
setContentView(progressLayout);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageFinished(WebView view, String url) {
|
||||
setContentView(mWebView);
|
||||
}
|
||||
});
|
||||
mWebView.loadUrl("file:///android_asset/help.html");
|
||||
mWebView.loadUrl("https://termux.com/help.html");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
package com.termux.app;
|
||||
|
||||
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.os.Environment;
|
||||
import android.system.Os;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.terminal.EmulatorDebug;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
@@ -11,21 +27,6 @@ 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 android.view.WindowManager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.terminal.EmulatorDebug;
|
||||
|
||||
/**
|
||||
* Install the Termux bootstrap packages if necessary by following the below steps:
|
||||
*
|
||||
@@ -175,17 +176,19 @@ final class TermuxInstaller {
|
||||
}.start();
|
||||
}
|
||||
|
||||
/** Get bootstrap zip url for this systems cpu architecture. */
|
||||
static URL determineZipUrl() throws MalformedURLException {
|
||||
String arch = System.getProperty("os.arch");
|
||||
if (arch.startsWith("arm") || arch.equals("aarch64")) {
|
||||
// Handle different arm variants such as armv7l:
|
||||
arch = "arm";
|
||||
} else if (arch.equals("x86_64")) {
|
||||
arch = "i686";
|
||||
}
|
||||
return new URL("http://apt.termux.com/bootstrap/bootstrap-" + arch + ".zip");
|
||||
}
|
||||
/** Get bootstrap zip url for this systems cpu architecture. */
|
||||
static URL determineZipUrl() throws MalformedURLException {
|
||||
String arch = System.getProperty("os.arch");
|
||||
if (arch.startsWith("armv8")) {
|
||||
arch = "aarch64";
|
||||
} else if (arch.startsWith("arm")) {
|
||||
// Handle different arm variants such as armv7l:
|
||||
arch = "arm";
|
||||
} else if (arch.startsWith("x86")) { // "x86" on arcwelder, "x86_64" on 64-bit android.
|
||||
arch = "i686";
|
||||
}
|
||||
return new URL("https://termux.net/bootstrap/bootstrap-" + arch + ".zip");
|
||||
}
|
||||
|
||||
/** Delete a folder and all its content or throw. */
|
||||
static void deleteFolder(File fileOrDirectory) {
|
||||
@@ -200,4 +203,51 @@ final class TermuxInstaller {
|
||||
}
|
||||
}
|
||||
|
||||
public static void setupStorageSymlinks(final Context context) {
|
||||
final String LOG_TAG = "termux-storage";
|
||||
new Thread() {
|
||||
public void run() {
|
||||
try {
|
||||
File storageDir = new File(TermuxService.HOME_PATH, "storage");
|
||||
|
||||
if (storageDir.exists() && !storageDir.delete()) {
|
||||
Log.e(LOG_TAG, "Could not delete old $HOME/storage");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!storageDir.mkdirs()) {
|
||||
Log.e(LOG_TAG, "Unable to mkdirs() for $HOME/storage");
|
||||
return;
|
||||
}
|
||||
|
||||
File sharedDir = Environment.getExternalStorageDirectory();
|
||||
Os.symlink(sharedDir.getAbsolutePath(), new File(storageDir, "shared").getAbsolutePath());
|
||||
|
||||
File downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
|
||||
Os.symlink(downloadsDir.getAbsolutePath(), new File(storageDir, "downloads").getAbsolutePath());
|
||||
|
||||
File dcimDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM);
|
||||
Os.symlink(dcimDir.getAbsolutePath(), new File(storageDir, "dcim").getAbsolutePath());
|
||||
|
||||
File picturesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
|
||||
Os.symlink(picturesDir.getAbsolutePath(), new File(storageDir, "pictures").getAbsolutePath());
|
||||
|
||||
File musicDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC);
|
||||
Os.symlink(musicDir.getAbsolutePath(), new File(storageDir, "music").getAbsolutePath());
|
||||
|
||||
File moviesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES);
|
||||
Os.symlink(moviesDir.getAbsolutePath(), new File(storageDir, "movies").getAbsolutePath());
|
||||
|
||||
final File[] dirs = context.getExternalFilesDirs(null);
|
||||
if (dirs != null && dirs.length >= 2) {
|
||||
final File externalDir = dirs[1];
|
||||
Os.symlink(externalDir.getAbsolutePath(), new File(storageDir, "external").getAbsolutePath());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "Error setting up link", e);
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,16 +1,34 @@
|
||||
package com.termux.app;
|
||||
|
||||
import com.termux.terminal.TerminalSession;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.IntDef;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.termux.terminal.TerminalSession;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.Properties;
|
||||
|
||||
final class TermuxPreferences {
|
||||
|
||||
@IntDef({BELL_VIBRATE, BELL_BEEP, BELL_IGNORE})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
public @interface AsciiBellBehaviour {}
|
||||
|
||||
static final int BELL_VIBRATE = 1;
|
||||
static final int BELL_BEEP = 2;
|
||||
static final int BELL_IGNORE = 3;
|
||||
|
||||
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";
|
||||
@@ -19,7 +37,13 @@ final class TermuxPreferences {
|
||||
private boolean mFullScreen;
|
||||
private int mFontSize;
|
||||
|
||||
@AsciiBellBehaviour
|
||||
int mBellBehaviour = BELL_VIBRATE;
|
||||
|
||||
boolean mBackIsEscape = true;
|
||||
|
||||
TermuxPreferences(Context context) {
|
||||
reloadFromProperties(context);
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
|
||||
float dipInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, context.getResources().getDisplayMetrics());
|
||||
@@ -86,4 +110,33 @@ final class TermuxPreferences {
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(SHOW_WELCOME_DIALOG_KEY, false).apply();
|
||||
}
|
||||
|
||||
public void reloadFromProperties(Context context) {
|
||||
try {
|
||||
File propsFile = new File(TermuxService.HOME_PATH + "/.config/termux/termux.properties");
|
||||
Properties props = new Properties();
|
||||
if (propsFile.isFile() && propsFile.canRead()) {
|
||||
try (FileInputStream in = new FileInputStream(propsFile)) {
|
||||
props.load(in);
|
||||
}
|
||||
}
|
||||
|
||||
switch (props.getProperty("bell-character", "vibrate")) {
|
||||
case "beep":
|
||||
mBellBehaviour = BELL_BEEP;
|
||||
break;
|
||||
case "ignore":
|
||||
mBellBehaviour = BELL_IGNORE;
|
||||
break;
|
||||
default: // "vibrate".
|
||||
mBellBehaviour = BELL_VIBRATE;
|
||||
break;
|
||||
}
|
||||
|
||||
mBackIsEscape = "escape".equals(props.getProperty("back-key", "back"));
|
||||
} catch (Exception e) {
|
||||
Toast.makeText(context, "Error loading properties: " + e.getMessage(), Toast.LENGTH_LONG).show();
|
||||
Log.e("termux", "Error loading props", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -55,7 +55,11 @@ public final class TermuxService extends Service implements SessionChangedCallba
|
||||
/** Intent action to toggle the wifi lock, {@link #mWifiLock}, which this service may hold. */
|
||||
private static final String ACTION_LOCK_WIFI = "com.termux.service_toggle_wifi_lock";
|
||||
/** Intent action to launch a new terminal session. Executed from TermuxWidgetProvider. */
|
||||
private static final String ACTION_EXECUTE = "com.termux.service_execute";
|
||||
public static final String ACTION_EXECUTE = "com.termux.service_execute";
|
||||
|
||||
public static final String EXTRA_ARGUMENTS = "com.termux.execute.arguments";
|
||||
|
||||
public static final String EXTRA_CURRENT_WORKING_DIRECTORY = "com.termux.execute.cwd";
|
||||
|
||||
/** This service is only bound from inside the same process and never uses IPC. */
|
||||
class LocalBinder extends Binder {
|
||||
@@ -113,8 +117,8 @@ public final class TermuxService extends Service implements SessionChangedCallba
|
||||
} else if (ACTION_EXECUTE.equals(action)) {
|
||||
Uri executableUri = intent.getData();
|
||||
String executablePath = (executableUri == null ? null : executableUri.getPath());
|
||||
String[] arguments = (executableUri == null ? null : intent.getStringArrayExtra("com.termux.execute.arguments"));
|
||||
String cwd = intent.getStringExtra("com.termux.execute.cwd");
|
||||
String[] arguments = (executableUri == null ? null : intent.getStringArrayExtra(EXTRA_ARGUMENTS));
|
||||
String cwd = intent.getStringExtra(EXTRA_CURRENT_WORKING_DIRECTORY);
|
||||
TerminalSession newSession = createTermSession(executablePath, arguments, cwd, false);
|
||||
|
||||
// Transform executable path to session name, e.g. "/bin/do-something.sh" => "do something.sh".
|
||||
@@ -237,17 +241,22 @@ public final class TermuxService extends Service implements SessionChangedCallba
|
||||
final String prefixEnv = "PREFIX=" + PREFIX_PATH;
|
||||
final String androidRootEnv = "ANDROID_ROOT=" + System.getenv("ANDROID_ROOT");
|
||||
final String androidDataEnv = "ANDROID_DATA=" + System.getenv("ANDROID_DATA");
|
||||
// EXTERNAL_STORAGE is needed for /system/bin/am to work on at least
|
||||
// Samsung S7 - see https://plus.google.com/110070148244138185604/posts/gp8Lk3aCGp3.
|
||||
final String externalStorageEnv = "EXTERNAL_STORAGE=" + System.getenv("EXTERNAL_STORAGE");
|
||||
String[] env;
|
||||
if (failSafe) {
|
||||
env = new String[] { termEnv, homeEnv, prefixEnv, androidRootEnv, androidDataEnv };
|
||||
// Keep the default path so that system binaries can be used in the failsafe session.
|
||||
final String pathEnv = "PATH=" + System.getenv("PATH");
|
||||
env = new String[] { termEnv, homeEnv, prefixEnv, androidRootEnv, androidDataEnv, pathEnv, externalStorageEnv };
|
||||
} else {
|
||||
final String ps1Env = "PS1=$ ";
|
||||
final String ldEnv = "LD_LIBRARY_PATH=" + PREFIX_PATH + "/lib";
|
||||
final String langEnv = "LANG=en_US.UTF-8";
|
||||
final String pathEnv = "PATH=" + PREFIX_PATH + "/bin:" + PREFIX_PATH + "/bin/applets:" + System.getenv("PATH");
|
||||
final String pathEnv = "PATH=" + PREFIX_PATH + "/bin:" + PREFIX_PATH + "/bin/applets";
|
||||
final String pwdEnv = "PWD=" + cwd;
|
||||
|
||||
env = new String[] { termEnv, homeEnv, prefixEnv, ps1Env, ldEnv, langEnv, pathEnv, pwdEnv, androidRootEnv, androidDataEnv };
|
||||
env = new String[] { termEnv, homeEnv, prefixEnv, ps1Env, ldEnv, langEnv, pathEnv, pwdEnv, androidRootEnv, androidDataEnv, externalStorageEnv };
|
||||
}
|
||||
|
||||
String shellName;
|
||||
|
||||
@@ -16,8 +16,6 @@ package com.termux.drawer;
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
@@ -27,8 +25,6 @@ import android.graphics.PixelFormat;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.os.SystemClock;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.Gravity;
|
||||
@@ -39,6 +35,8 @@ import android.view.ViewGroup;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* DrawerLayout acts as a top-level container for window content that allows for interactive "drawer" views to be pulled
|
||||
* out from the edge of the window.
|
||||
@@ -138,11 +136,11 @@ public class DrawerLayout extends ViewGroup {
|
||||
|
||||
private final ChildAccessibilityDelegate mChildAccessibilityDelegate = new ChildAccessibilityDelegate();
|
||||
|
||||
private int mMinDrawerMargin;
|
||||
private final int mMinDrawerMargin;
|
||||
|
||||
private int mScrimColor = DEFAULT_SCRIM_COLOR;
|
||||
private float mScrimOpacity;
|
||||
private Paint mScrimPaint = new Paint();
|
||||
private final Paint mScrimPaint = new Paint();
|
||||
|
||||
private final ViewDragHelper mLeftDragger;
|
||||
private final ViewDragHelper mRightDragger;
|
||||
@@ -1422,38 +1420,6 @@ public class DrawerLayout extends ViewGroup {
|
||||
return super.onKeyUp(keyCode, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(Parcelable state) {
|
||||
final SavedState ss = (SavedState) state;
|
||||
super.onRestoreInstanceState(ss.getSuperState());
|
||||
|
||||
if (ss.openDrawerGravity != Gravity.NO_GRAVITY) {
|
||||
final View toOpen = findDrawerWithGravity(ss.openDrawerGravity);
|
||||
if (toOpen != null) {
|
||||
openDrawer(toOpen);
|
||||
}
|
||||
}
|
||||
|
||||
setDrawerLockMode(ss.lockModeLeft, Gravity.LEFT);
|
||||
setDrawerLockMode(ss.lockModeRight, Gravity.RIGHT);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Parcelable onSaveInstanceState() {
|
||||
final Parcelable superState = super.onSaveInstanceState();
|
||||
final SavedState ss = new SavedState(superState);
|
||||
|
||||
final View openDrawer = findOpenDrawer();
|
||||
if (openDrawer != null) {
|
||||
ss.openDrawerGravity = ((LayoutParams) openDrawer.getLayoutParams()).gravity;
|
||||
}
|
||||
|
||||
ss.lockModeLeft = mLockModeLeft;
|
||||
ss.lockModeRight = mLockModeRight;
|
||||
|
||||
return ss;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addView(View child, int index, ViewGroup.LayoutParams params) {
|
||||
super.addView(child, index, params);
|
||||
@@ -1486,30 +1452,6 @@ public class DrawerLayout extends ViewGroup {
|
||||
&& child.getImportantForAccessibility() != View.IMPORTANT_FOR_ACCESSIBILITY_NO;
|
||||
}
|
||||
|
||||
/**
|
||||
* State persisted across instances
|
||||
*/
|
||||
protected static class SavedState extends BaseSavedState {
|
||||
int openDrawerGravity = Gravity.NO_GRAVITY;
|
||||
int lockModeLeft = LOCK_MODE_UNLOCKED;
|
||||
int lockModeRight = LOCK_MODE_UNLOCKED;
|
||||
|
||||
public SavedState(Parcel in) {
|
||||
super(in);
|
||||
openDrawerGravity = in.readInt();
|
||||
}
|
||||
|
||||
public SavedState(Parcelable superState) {
|
||||
super(superState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
super.writeToParcel(dest, flags);
|
||||
dest.writeInt(openDrawerGravity);
|
||||
}
|
||||
}
|
||||
|
||||
private class ViewDragCallback extends ViewDragHelper.Callback {
|
||||
private final int mAbsGravity;
|
||||
private ViewDragHelper mDragger;
|
||||
|
||||
@@ -57,7 +57,7 @@ public class ViewDragHelper {
|
||||
/**
|
||||
* Edge flag indicating that the left edge should be affected.
|
||||
*/
|
||||
public static final int EDGE_LEFT = 1 << 0;
|
||||
public static final int EDGE_LEFT = 1 /*1 << 0*/;
|
||||
|
||||
/**
|
||||
* Edge flag indicating that the right edge should be affected.
|
||||
@@ -82,7 +82,7 @@ public class ViewDragHelper {
|
||||
/**
|
||||
* Indicates that a check should occur along the horizontal axis
|
||||
*/
|
||||
public static final int DIRECTION_HORIZONTAL = 1 << 0;
|
||||
public static final int DIRECTION_HORIZONTAL = 1 /*1 << 0*/;
|
||||
|
||||
/**
|
||||
* Indicates that a check should occur along the vertical axis
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
@@ -15,7 +16,7 @@ import java.io.FileNotFoundException;
|
||||
public class TermuxFilePickerProvider extends ContentProvider {
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -25,7 +26,20 @@ public class TermuxFilePickerProvider extends ContentProvider {
|
||||
|
||||
@Override
|
||||
public String getType(@NonNull Uri uri) {
|
||||
return null;
|
||||
String contentType = null;
|
||||
String path = uri.getPath();
|
||||
int lastDotIndex = path.lastIndexOf('.');
|
||||
String possibleFileExtension = path.substring(lastDotIndex + 1, path.length());
|
||||
if (possibleFileExtension.contains("/")) {
|
||||
// The dot was in the path, so not a file extension.
|
||||
} else {
|
||||
MimeTypeMap mimeTypes = MimeTypeMap.getSingleton();
|
||||
// Lower casing makes it work with e.g. "JPG":
|
||||
contentType = mimeTypes.getMimeTypeFromExtension(possibleFileExtension.toLowerCase());
|
||||
}
|
||||
|
||||
if (contentType == null) contentType = "application/octet-stream";
|
||||
return contentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
package com.termux.filepicker;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.util.Log;
|
||||
import android.util.Patterns;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.app.DialogUtils;
|
||||
import com.termux.app.TermuxService;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class TermuxFileReceiverActivity extends Activity {
|
||||
|
||||
static final String TERMUX_RECEIVEDIR = TermuxService.FILES_PATH + "/home/downloads";
|
||||
static final String EDITOR_PROGRAM = TermuxService.HOME_PATH + "/bin/termux-file-editor";
|
||||
static final String URL_OPENER_PROGRAM = TermuxService.HOME_PATH + "/bin/termux-url-opener";
|
||||
|
||||
/**
|
||||
* If the activity should be finished when the name input dialog is dismissed. This is disabled
|
||||
* before showing an error dialog, since the act of showing the error dialog will cause the
|
||||
* name input dialog to be implicitly dismissed, and we do not want to finish the activity directly
|
||||
* when showing the error dialog.
|
||||
*/
|
||||
private boolean mFinishOnDismissNameDialog = true;
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
final Intent intent = getIntent();
|
||||
final String action = intent.getAction();
|
||||
final String type = intent.getType();
|
||||
final String scheme = intent.getScheme();
|
||||
|
||||
if (Intent.ACTION_SEND.equals(action) && type != null) {
|
||||
final String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
|
||||
final Uri sharedUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
|
||||
|
||||
if (sharedText != null) {
|
||||
if (Patterns.WEB_URL.matcher(sharedText).matches()) {
|
||||
handleUrlAndFinish(sharedText);
|
||||
} else {
|
||||
String subject = intent.getStringExtra(Intent.EXTRA_SUBJECT);
|
||||
if (subject == null) subject = intent.getStringExtra(Intent.EXTRA_TITLE);
|
||||
if (subject != null) subject += ".txt";
|
||||
promptNameAndSave(new ByteArrayInputStream(sharedText.getBytes(StandardCharsets.UTF_8)), subject);
|
||||
}
|
||||
} else if (sharedUri != null) {
|
||||
handleContentUri(sharedUri, intent.getStringExtra(Intent.EXTRA_TITLE));
|
||||
} else {
|
||||
showErrorDialogAndQuit("Send action without content - nothing to save.");
|
||||
}
|
||||
} else if ("content".equals(scheme)) {
|
||||
handleContentUri(intent.getData(), intent.getStringExtra(Intent.EXTRA_TITLE));
|
||||
} else if ("file".equals(scheme)) {
|
||||
// When e.g. clicking on a downloaded apk:
|
||||
String path = intent.getData().getPath();
|
||||
File file = new File(path);
|
||||
try {
|
||||
FileInputStream in = new FileInputStream(file);
|
||||
promptNameAndSave(in, file.getName());
|
||||
} catch (FileNotFoundException e) {
|
||||
showErrorDialogAndQuit("Cannot open file: " + e.getMessage() + ".");
|
||||
}
|
||||
} else {
|
||||
showErrorDialogAndQuit("Unable to receive any file or URL.");
|
||||
}
|
||||
}
|
||||
|
||||
void showErrorDialogAndQuit(String message) {
|
||||
mFinishOnDismissNameDialog = false;
|
||||
new AlertDialog.Builder(this).setMessage(message).setOnDismissListener(new DialogInterface.OnDismissListener() {
|
||||
@Override
|
||||
public void onDismiss(DialogInterface dialog) {
|
||||
finish();
|
||||
}
|
||||
}).setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
finish();
|
||||
}
|
||||
}).show();
|
||||
}
|
||||
|
||||
void handleContentUri(final Uri uri, String subjectFromIntent) {
|
||||
try {
|
||||
String attachmentFileName = null;
|
||||
|
||||
String[] projection = new String[]{OpenableColumns.DISPLAY_NAME};
|
||||
try (Cursor c = getContentResolver().query(uri, projection, null, null, null)) {
|
||||
if (c != null && c.moveToFirst()) {
|
||||
final int fileNameColumnId = c.getColumnIndex(OpenableColumns.DISPLAY_NAME);
|
||||
if (fileNameColumnId >= 0) attachmentFileName = c.getString(fileNameColumnId);
|
||||
}
|
||||
}
|
||||
|
||||
if (attachmentFileName == null) attachmentFileName = subjectFromIntent;
|
||||
|
||||
InputStream in = getContentResolver().openInputStream(uri);
|
||||
promptNameAndSave(in, attachmentFileName);
|
||||
} catch (Exception e) {
|
||||
showErrorDialogAndQuit("Unable to handle shared content:\n\n" + e.getMessage());
|
||||
Log.e("termux", "handleContentUri(uri=" + uri + ") failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
void promptNameAndSave(final InputStream in, final String attachmentFileName) {
|
||||
DialogUtils.textInput(this, R.string.file_received_title, attachmentFileName
|
||||
, android.R.string.ok, new DialogUtils.TextSetListener() {
|
||||
@Override
|
||||
public void onTextSet(final String text) {
|
||||
if (saveStreamWithName(in, text) == null) return;
|
||||
finish();
|
||||
}
|
||||
}, R.string.file_received_open_folder_button, new DialogUtils.TextSetListener() {
|
||||
@Override
|
||||
public void onTextSet(String text) {
|
||||
if (saveStreamWithName(in, text) == null) return;
|
||||
|
||||
Intent executeIntent = new Intent(TermuxService.ACTION_EXECUTE);
|
||||
executeIntent.putExtra(TermuxService.EXTRA_CURRENT_WORKING_DIRECTORY, TERMUX_RECEIVEDIR);
|
||||
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
|
||||
startService(executeIntent);
|
||||
finish();
|
||||
}
|
||||
}, R.string.file_received_edit_button, new DialogUtils.TextSetListener() {
|
||||
@Override
|
||||
public void onTextSet(String text) {
|
||||
File outFile = saveStreamWithName(in, text);
|
||||
if (outFile == null) return;
|
||||
|
||||
final File editorProgramFile = new File(EDITOR_PROGRAM);
|
||||
if (!editorProgramFile.isFile()) {
|
||||
showErrorDialogAndQuit("The following file does not exist:\n$HOME/bin/termux-file-editor\n\n"
|
||||
+ "Create this file as a script or a symlink - it will be called with the received file as only argument.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Do this for the user if necessary:
|
||||
editorProgramFile.setExecutable(true);
|
||||
|
||||
final Uri scriptUri = new Uri.Builder().scheme("file").path(EDITOR_PROGRAM).build();
|
||||
|
||||
Intent executeIntent = new Intent(TermuxService.ACTION_EXECUTE, scriptUri);
|
||||
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
|
||||
executeIntent.putExtra(TermuxService.EXTRA_ARGUMENTS, new String[]{outFile.getAbsolutePath()});
|
||||
startService(executeIntent);
|
||||
finish();
|
||||
}
|
||||
}, new DialogInterface.OnDismissListener() {
|
||||
@Override
|
||||
public void onDismiss(DialogInterface dialog) {
|
||||
if (mFinishOnDismissNameDialog) finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public File saveStreamWithName(InputStream in, String attachmentFileName) {
|
||||
File receiveDir = new File(TERMUX_RECEIVEDIR);
|
||||
if (!receiveDir.isDirectory() && !receiveDir.mkdirs()) {
|
||||
showErrorDialogAndQuit("Cannot create directory: " + receiveDir.getAbsolutePath());
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
final File outFile = new File(receiveDir, attachmentFileName);
|
||||
try (FileOutputStream f = new FileOutputStream(outFile)) {
|
||||
byte[] buffer = new byte[4096];
|
||||
int readBytes;
|
||||
while ((readBytes = in.read(buffer)) > 0) {
|
||||
f.write(buffer, 0, readBytes);
|
||||
}
|
||||
}
|
||||
return outFile;
|
||||
} catch (IOException e) {
|
||||
showErrorDialogAndQuit("Error saving file:\n\n" + e);
|
||||
Log.e("termux", "Error saving file", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void handleUrlAndFinish(final String url) {
|
||||
final File urlOpenerProgramFile = new File(URL_OPENER_PROGRAM);
|
||||
if (!urlOpenerProgramFile.isFile()) {
|
||||
showErrorDialogAndQuit("The following file does not exist:\n$HOME/bin/termux-url-opener\n\n"
|
||||
+ "Create this file as a script or a symlink - it will be called with the shared URL as only argument.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Do this for the user if necessary:
|
||||
urlOpenerProgramFile.setExecutable(true);
|
||||
|
||||
final Uri urlOpenerProgramUri = new Uri.Builder().scheme("file").path(URL_OPENER_PROGRAM).build();
|
||||
|
||||
Intent executeIntent = new Intent(TermuxService.ACTION_EXECUTE, urlOpenerProgramUri);
|
||||
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
|
||||
executeIntent.putExtra(TermuxService.EXTRA_ARGUMENTS, new String[]{url});
|
||||
startService(executeIntent);
|
||||
finish();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -28,7 +28,7 @@ final class JNI {
|
||||
* @return the file descriptor resulting from opening /dev/ptmx master device. The sub process will have opened the
|
||||
* slave device counterpart (/dev/pts/$N) and have it as stdint, stdout and stderr.
|
||||
*/
|
||||
public static native int createSubprocess(String cmd, String cwd, String[] args, String[] envVars, int[] processId);
|
||||
public static native int createSubprocess(String cmd, String cwd, String[] args, String[] envVars, int[] processId, int rows, int columns);
|
||||
|
||||
/** Set the window size for a given pty, which allows connected programs to learn how large their screen is. */
|
||||
public static native void setPtyWindowSize(int fd, int rows, int cols);
|
||||
|
||||
@@ -61,6 +61,10 @@ public final class TerminalBuffer {
|
||||
TerminalRow lineObject = mLines[externalToInternalRow(row)];
|
||||
int x1Index = lineObject.findStartOfColumn(x1);
|
||||
int x2Index = (x2 < mColumns) ? lineObject.findStartOfColumn(x2) : lineObject.getSpaceUsed();
|
||||
if (x2Index == x1Index) {
|
||||
// Selected the start of a wide character.
|
||||
x2Index = lineObject.findStartOfColumn(x2+1);
|
||||
}
|
||||
char[] line = lineObject.mText;
|
||||
int lastPrintingCharIndex = -1;
|
||||
int i;
|
||||
@@ -71,7 +75,7 @@ public final class TerminalBuffer {
|
||||
} else {
|
||||
for (i = x1Index; i < x2Index; ++i) {
|
||||
char c = line[i];
|
||||
if (c != ' ' && !Character.isLowSurrogate(c)) lastPrintingCharIndex = i;
|
||||
if (c != ' ') lastPrintingCharIndex = i;
|
||||
}
|
||||
}
|
||||
if (lastPrintingCharIndex != -1) builder.append(line, x1Index, lastPrintingCharIndex - x1Index + 1);
|
||||
@@ -123,10 +127,14 @@ public final class TerminalBuffer {
|
||||
mLines[externalToInternalRow(row)].mLineWrap = true;
|
||||
}
|
||||
|
||||
private boolean getLineWrap(int row) {
|
||||
public boolean getLineWrap(int row) {
|
||||
return mLines[externalToInternalRow(row)].mLineWrap;
|
||||
}
|
||||
|
||||
public void clearLineWrap(int row) {
|
||||
mLines[externalToInternalRow(row)].mLineWrap = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize the screen which this transcript backs. Currently, this only works if the number of columns does not
|
||||
* change or the rows expand (that is, it only works when shrinking the number of rows).
|
||||
|
||||
@@ -33,8 +33,7 @@ public final class TerminalEmulator {
|
||||
private static final boolean LOG_ESCAPE_SEQUENCES = false;
|
||||
|
||||
public static final int MOUSE_LEFT_BUTTON = 0;
|
||||
public static final int MOUSE_MIDDLE_BUTTON = 1;
|
||||
public static final int MOUSE_RIGHT_BUTTON = 2;
|
||||
|
||||
/** Mouse moving while having left mouse button pressed. */
|
||||
public static final int MOUSE_LEFT_BUTTON_MOVED = 32;
|
||||
public static final int MOUSE_WHEELUP_BUTTON = 64;
|
||||
@@ -219,7 +218,7 @@ public final class TerminalEmulator {
|
||||
*/
|
||||
private int mScrollCounter = 0;
|
||||
|
||||
private int mUtf8ToFollow, mUtf8Index;
|
||||
private byte mUtf8ToFollow, mUtf8Index;
|
||||
private final byte[] mUtf8InputBuffer = new byte[4];
|
||||
|
||||
public final TerminalColors mColors = new TerminalColors();
|
||||
@@ -425,7 +424,11 @@ public final class TerminalEmulator {
|
||||
processCodePoint(/* escape (hexadecimal=0x1B, octal=033): */27);
|
||||
processCodePoint((codePoint & 0x7F) + 0x40);
|
||||
} else {
|
||||
if (Character.UNASSIGNED == Character.getType(codePoint)) codePoint = UNICODE_REPLACEMENT_CHAR;
|
||||
switch (Character.getType(codePoint)) {
|
||||
case Character.UNASSIGNED:
|
||||
case Character.SURROGATE:
|
||||
codePoint = UNICODE_REPLACEMENT_CHAR;
|
||||
}
|
||||
processCodePoint(codePoint);
|
||||
}
|
||||
}
|
||||
@@ -471,16 +474,28 @@ public final class TerminalEmulator {
|
||||
else
|
||||
mSession.onBell();
|
||||
break;
|
||||
case 8: // BS
|
||||
setCursorCol(Math.max(mLeftMargin, mCursorCol - 1));
|
||||
break;
|
||||
case 9: // Horizontal tab - move to next tab stop, but not past edge of screen
|
||||
int nextTabStop = nextTabStop(1);
|
||||
while (mCursorCol < nextTabStop) {
|
||||
// Emit newlines to get background color right.
|
||||
processCodePoint(' ');
|
||||
}
|
||||
break;
|
||||
case 8: // Backspace (BS, ^H).
|
||||
if (mLeftMargin == mCursorCol) {
|
||||
// Jump to previous line if it was auto-wrapped.
|
||||
int previousRow = mCursorRow - 1;
|
||||
if (previousRow >= 0 && mScreen.getLineWrap(previousRow)) {
|
||||
mScreen.clearLineWrap(previousRow);
|
||||
setCursorRowCol(previousRow, mRightMargin - 1);
|
||||
}
|
||||
} else {
|
||||
setCursorCol(mCursorCol - 1);
|
||||
}
|
||||
break;
|
||||
case 9: // Horizontal tab (HT, \t) - move to next tab stop, but not past edge of screen
|
||||
// XXX: Should perhaps use color if writing to new cells. Try with
|
||||
// printf "\033[41m\tXX\033[0m\n"
|
||||
// The OSX Terminal.app colors the spaces from the tab red, but xterm does not.
|
||||
// Note that Terminal.app only colors on new cells, in e.g.
|
||||
// printf "\033[41m\t\r\033[42m\tXX\033[0m\n"
|
||||
// the first cells are created with a red background, but when tabbing over
|
||||
// them again with a green background they are not overwritten.
|
||||
mCursorCol = nextTabStop(1);
|
||||
break;
|
||||
case 10: // Line feed (LF, \n).
|
||||
case 11: // Vertical tab (VT, \v).
|
||||
case 12: // Form feed (FF, \f).
|
||||
@@ -908,8 +923,9 @@ public final class TerminalEmulator {
|
||||
/** Process byte while in the {@link #ESC_CSI_QUESTIONMARK} escape state. */
|
||||
private void doCsiQuestionMark(int b) {
|
||||
switch (b) {
|
||||
case 'J': // Selective erase in display (DECSED - http://www.vt100.net/docs/vt510-rm/DECSED).
|
||||
case 'K': // Selective erase in line (DECSEL - http://vt100.net/docs/vt510-rm/DECSEL).
|
||||
case 'J': // Selective erase in display (DECSED) - http://www.vt100.net/docs/vt510-rm/DECSED.
|
||||
case 'K': // Selective erase in line (DECSEL) - http://vt100.net/docs/vt510-rm/DECSEL.
|
||||
mAboutToAutoWrap = false;
|
||||
int fillChar = ' ';
|
||||
int startCol = -1;
|
||||
int startRow = -1;
|
||||
@@ -1230,6 +1246,11 @@ public final class TerminalEmulator {
|
||||
mScreen.blockSet(mRightMargin - 1, mTopMargin, 1, rows, ' ', TextStyle.encode(mForeColor, mBackColor, 0));
|
||||
}
|
||||
break;
|
||||
case 'c': // RIS - Reset to Initial State (http://vt100.net/docs/vt510-rm/RIS).
|
||||
reset();
|
||||
blockClear(0, 0, mColumns, mRows);
|
||||
setCursorPosition(0, 0);
|
||||
break;
|
||||
case 'D': // INDEX
|
||||
doLinefeed();
|
||||
break;
|
||||
@@ -1322,9 +1343,8 @@ public final class TerminalEmulator {
|
||||
continueSequence(ESC_CSI_ARGS_ASTERIX);
|
||||
break;
|
||||
case '@': {
|
||||
// ESC [ Pn @ - ICH Insert Characters.
|
||||
// "This control function inserts one or more space (SP) characters starting at the cursor position."
|
||||
// http://www.vt100.net/docs/vt510-rm/ICH
|
||||
// "CSI{n}@" - Insert ${n} space characters (ICH) - http://www.vt100.net/docs/vt510-rm/ICH.
|
||||
mAboutToAutoWrap = false;
|
||||
int columnsAfterCursor = mColumns - mCursorCol;
|
||||
int spacesToInsert = Math.min(getArg0(1), columnsAfterCursor);
|
||||
int charsToMove = columnsAfterCursor - spacesToInsert;
|
||||
@@ -1361,7 +1381,7 @@ public final class TerminalEmulator {
|
||||
case 'I': // Cursor Horizontal Forward Tabulation (CHT). Move the active position n tabs forward.
|
||||
setCursorCol(nextTabStop(getArg0(1)));
|
||||
break;
|
||||
case 'J': // ESC [ Pn J - ED - Erase in Display
|
||||
case 'J': // "${CSI}${0,1,2}J" - Erase in Display (ED)
|
||||
// ED ignores the scrolling margins.
|
||||
switch (getArg0(0)) {
|
||||
case 0: // Erase from the active position to the end of the screen, inclusive (default).
|
||||
@@ -1378,8 +1398,9 @@ public final class TerminalEmulator {
|
||||
break;
|
||||
default:
|
||||
unknownSequence(b);
|
||||
break;
|
||||
return;
|
||||
}
|
||||
mAboutToAutoWrap = false;
|
||||
break;
|
||||
case 'K': // "CSI{n}K" - Erase in line (EL).
|
||||
switch (getArg0(0)) {
|
||||
@@ -1394,8 +1415,9 @@ public final class TerminalEmulator {
|
||||
break;
|
||||
default:
|
||||
unknownSequence(b);
|
||||
break;
|
||||
return;
|
||||
}
|
||||
mAboutToAutoWrap = false;
|
||||
break;
|
||||
case 'L': // "${CSI}{N}L" - insert ${N} lines (IL).
|
||||
{
|
||||
@@ -1408,6 +1430,7 @@ public final class TerminalEmulator {
|
||||
break;
|
||||
case 'M': // "${CSI}${N}M" - delete N lines (DL).
|
||||
{
|
||||
mAboutToAutoWrap = false;
|
||||
int linesAfterCursor = mBottomMargin - mCursorRow;
|
||||
int linesToDelete = Math.min(getArg0(1), linesAfterCursor);
|
||||
int linesToMove = linesAfterCursor - linesToDelete;
|
||||
@@ -1422,6 +1445,7 @@ public final class TerminalEmulator {
|
||||
// As characters are deleted, the remaining characters between the cursor and right margin move to the left.
|
||||
// Character attributes move with the characters. The terminal adds blank spaces with no visual character
|
||||
// attributes at the right margin. DCH has no effect outside the scrolling margins."
|
||||
mAboutToAutoWrap = false;
|
||||
int cellsAfterCursor = mColumns - mCursorCol;
|
||||
int cellsToDelete = Math.min(getArg0(1), cellsAfterCursor);
|
||||
int cellsToMove = cellsAfterCursor - cellsToDelete;
|
||||
@@ -1444,7 +1468,7 @@ public final class TerminalEmulator {
|
||||
final int linesToScrollArg = getArg0(1);
|
||||
final int linesBetweenTopAndBottomMargins = mBottomMargin - mTopMargin;
|
||||
final int linesToScroll = Math.min(linesBetweenTopAndBottomMargins, linesToScrollArg);
|
||||
mScreen.blockCopy(0, mTopMargin, mColumns, linesBetweenTopAndBottomMargins - linesToScroll, 0, linesToScroll);
|
||||
mScreen.blockCopy(0, mTopMargin, mColumns, linesBetweenTopAndBottomMargins - linesToScroll, 0, mTopMargin + linesToScroll);
|
||||
blockClear(0, mTopMargin, mColumns, linesToScroll);
|
||||
} else {
|
||||
// "${CSI}${func};${startx};${starty};${firstrow};${lastrow}T" - initiate highlight mouse tracking.
|
||||
@@ -1452,6 +1476,7 @@ public final class TerminalEmulator {
|
||||
}
|
||||
break;
|
||||
case 'X': // "${CSI}${N}X" - Erase ${N:=1} character(s) (ECH). FIXME: Clears character attributes?
|
||||
mAboutToAutoWrap = false;
|
||||
mScreen.blockSet(mCursorCol, mCursorRow, Math.min(getArg0(1), mColumns - mCursorCol), 1, ' ', getStyle());
|
||||
break;
|
||||
case 'Z': // Cursor Backward Tabulation (CBT). Move the active position n tabs backward.
|
||||
|
||||
@@ -184,6 +184,7 @@ public final class TerminalRow {
|
||||
mSpaceUsed += javaCharDifference;
|
||||
|
||||
// Store char. A combining character is stored at the end of the existing contents so that it modifies them:
|
||||
//noinspection ResultOfMethodCallIgnored - since we already now how many java chars is used.
|
||||
Character.toChars(codePoint, text, oldStartOfColumnIndex + (newIsCombining ? oldCharactersUsedForColumn : 0));
|
||||
|
||||
if (oldCodePointDisplayWidth == 2 && newCodePointDisplayWidth == 1) {
|
||||
|
||||
@@ -82,14 +82,17 @@ public final class TerminalSession extends TerminalOutput {
|
||||
/** Callback which gets notified when a session finishes or changes title. */
|
||||
final SessionChangedCallback mChangeCallback;
|
||||
|
||||
/** The pid of the shell process or -1 if not running. */
|
||||
/** The pid of the shell process. 0 if not started and -1 if finished running. */
|
||||
int mShellPid;
|
||||
int mShellExitStatus = -1;
|
||||
|
||||
/** The exit status of the shell process. Only valid if ${@link #mShellPid} is -1. */
|
||||
int mShellExitStatus;
|
||||
|
||||
/**
|
||||
* The file descriptor referencing the master half of a pseudo-terminal pair, resulting from calling
|
||||
* {@link JNI#createSubprocess(String, String, String[], String[], int[])}.
|
||||
* {@link JNI#createSubprocess(String, String, String[], String[], int[], int, int)}.
|
||||
*/
|
||||
final int mTerminalFileDescriptor;
|
||||
private int mTerminalFileDescriptor;
|
||||
|
||||
/** Set by the application for user identification of session, not by terminal. */
|
||||
public String mSessionName;
|
||||
@@ -128,20 +131,26 @@ public final class TerminalSession extends TerminalOutput {
|
||||
}
|
||||
};
|
||||
|
||||
private final String mShellPath;
|
||||
private final String mCwd;
|
||||
private final String[] mArgs;
|
||||
private final String[] mEnv;
|
||||
|
||||
public TerminalSession(String shellPath, String cwd, String[] args, String[] env, SessionChangedCallback changeCallback) {
|
||||
mChangeCallback = changeCallback;
|
||||
|
||||
int[] processId = new int[1];
|
||||
mTerminalFileDescriptor = JNI.createSubprocess(shellPath, cwd, args, env, processId);
|
||||
mShellPid = processId[0];
|
||||
this.mShellPath = shellPath;
|
||||
this.mCwd = cwd;
|
||||
this.mArgs = args;
|
||||
this.mEnv = env;
|
||||
}
|
||||
|
||||
/** Inform the attached pty of the new size and reflow or initialize the emulator. */
|
||||
public void updateSize(int columns, int rows) {
|
||||
JNI.setPtyWindowSize(mTerminalFileDescriptor, rows, columns);
|
||||
if (mEmulator == null) {
|
||||
initializeEmulator(columns, rows);
|
||||
} else {
|
||||
JNI.setPtyWindowSize(mTerminalFileDescriptor, rows, columns);
|
||||
mEmulator.resize(columns, rows);
|
||||
}
|
||||
}
|
||||
@@ -161,6 +170,11 @@ public final class TerminalSession extends TerminalOutput {
|
||||
*/
|
||||
public void initializeEmulator(int columns, int rows) {
|
||||
mEmulator = new TerminalEmulator(this, columns, rows, /* transcript= */5000);
|
||||
|
||||
int[] processId = new int[1];
|
||||
mTerminalFileDescriptor = JNI.createSubprocess(mShellPath, mCwd, mArgs, mEnv, processId, rows, columns);
|
||||
mShellPid = processId[0];
|
||||
|
||||
final FileDescriptor terminalFileDescriptorWrapped = wrapFileDescriptor(mTerminalFileDescriptor);
|
||||
|
||||
new Thread("TermSessionInputReader[pid=" + mShellPid + "]") {
|
||||
@@ -204,7 +218,7 @@ public final class TerminalSession extends TerminalOutput {
|
||||
/** Write data to the shell process. */
|
||||
@Override
|
||||
public void write(byte[] data, int offset, int count) {
|
||||
mTerminalToProcessIOQueue.write(data, offset, count);
|
||||
if (mShellPid > 0) mTerminalToProcessIOQueue.write(data, offset, count);
|
||||
}
|
||||
|
||||
/** Write the Unicode code point to the terminal encoded in UTF-8. */
|
||||
|
||||
@@ -6,15 +6,19 @@ import android.view.ScaleGestureDetector;
|
||||
/**
|
||||
* Input and scale listener which may be set on a {@link TerminalView} through
|
||||
* {@link TerminalView#setOnKeyListener(TerminalKeyListener)}.
|
||||
*
|
||||
* TODO: Rename to TerminalViewClient.
|
||||
*/
|
||||
public interface TerminalKeyListener {
|
||||
|
||||
/** Callback function on scale events according to {@link ScaleGestureDetector#getScaleFactor()}. */
|
||||
float onScale(float scale);
|
||||
|
||||
void onLongPress(MotionEvent e);
|
||||
|
||||
/** On a single tap on the terminal if terminal mouse reporting not enabled. */
|
||||
void onSingleTapUp(MotionEvent e);
|
||||
|
||||
boolean shouldBackButtonBeMappedToEscape();
|
||||
|
||||
void copyModeChanged(boolean copyMode);
|
||||
|
||||
}
|
||||
|
||||
@@ -1,29 +1,28 @@
|
||||
package com.termux.view;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.util.Properties;
|
||||
|
||||
import com.termux.terminal.EmulatorDebug;
|
||||
import com.termux.terminal.KeyHandler;
|
||||
import com.termux.terminal.TerminalColors;
|
||||
import com.termux.terminal.TerminalEmulator;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Build;
|
||||
import android.text.InputType;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.ActionMode;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyCharacterMap;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.BaseInputConnection;
|
||||
@@ -31,6 +30,19 @@ import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
import android.widget.Scroller;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.terminal.EmulatorDebug;
|
||||
import com.termux.terminal.KeyHandler;
|
||||
import com.termux.terminal.TerminalBuffer;
|
||||
import com.termux.terminal.TerminalColors;
|
||||
import com.termux.terminal.TerminalEmulator;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.util.Properties;
|
||||
|
||||
/** View displaying and interacting with a {@link TerminalSession}. */
|
||||
public final class TerminalView extends View {
|
||||
|
||||
@@ -52,9 +64,11 @@ public final class TerminalView extends View {
|
||||
/** Keeping track of the special keys acting as Ctrl and Fn for the soft keyboard and other hardware keys. */
|
||||
boolean mVirtualControlKeyDown, mVirtualFnKeyDown;
|
||||
|
||||
boolean mIsSelectingText = false;
|
||||
int mSelXAnchor = -1, mSelYAnchor = -1;
|
||||
boolean mIsSelectingText = false, mIsDraggingLeftSelection, mInitialTextSelection;
|
||||
int mSelX1 = -1, mSelX2 = -1, mSelY1 = -1, mSelY2 = -1;
|
||||
float mSelectionDownX, mSelectionDownY;
|
||||
private ActionMode mActionMode;
|
||||
private BitmapDrawable mLeftSelectionHandle, mRightSelectionHandle;
|
||||
|
||||
float mScaleFactor = 1.f;
|
||||
final GestureAndScaleRecognizer mGestureRecognizer;
|
||||
@@ -79,7 +93,7 @@ public final class TerminalView extends View {
|
||||
@Override
|
||||
public boolean onUp(MotionEvent e) {
|
||||
mScrollRemainder = 0.0f;
|
||||
if (mEmulator != null && mEmulator.isMouseTrackingActive()) {
|
||||
if (mEmulator != null && mEmulator.isMouseTrackingActive() && !mIsSelectingText) {
|
||||
// Quick event processing when mouse tracking is active - do not wait for check of double tapping
|
||||
// for zooming.
|
||||
sendMouseEventCode(e, TerminalEmulator.MOUSE_LEFT_BUTTON, true);
|
||||
@@ -92,6 +106,7 @@ public final class TerminalView extends View {
|
||||
@Override
|
||||
public boolean onSingleTapUp(MotionEvent e) {
|
||||
if (mEmulator == null) return true;
|
||||
if (mIsSelectingText) { toggleSelectingText(null); return true; }
|
||||
requestFocus();
|
||||
if (!mEmulator.isMouseTrackingActive()) {
|
||||
if (!e.isFromSource(InputDevice.SOURCE_MOUSE)) {
|
||||
@@ -104,7 +119,7 @@ public final class TerminalView extends View {
|
||||
|
||||
@Override
|
||||
public boolean onScroll(MotionEvent e2, float distanceX, float distanceY) {
|
||||
if (mEmulator == null) return true;
|
||||
if (mEmulator == null || mIsSelectingText) return true;
|
||||
if (mEmulator.isMouseTrackingActive() && e2.isFromSource(InputDevice.SOURCE_MOUSE)) {
|
||||
// If moving with mouse pointer while pressing button, report that instead of scroll.
|
||||
// This means that we never report moving with button press-events for touch input,
|
||||
@@ -122,6 +137,7 @@ public final class TerminalView extends View {
|
||||
|
||||
@Override
|
||||
public boolean onScale(float focusX, float focusY, float scale) {
|
||||
if (mEmulator == null || mIsSelectingText) return true;
|
||||
mScaleFactor *= scale;
|
||||
mScaleFactor = mOnKeyListener.onScale(mScaleFactor);
|
||||
return true;
|
||||
@@ -129,7 +145,7 @@ public final class TerminalView extends View {
|
||||
|
||||
@Override
|
||||
public boolean onFling(final MotionEvent e2, float velocityX, float velocityY) {
|
||||
if (mEmulator == null) return true;
|
||||
if (mEmulator == null || mIsSelectingText) return true;
|
||||
// Do not start scrolling until last fling has been taken care of:
|
||||
if (!mScroller.isFinished()) return true;
|
||||
|
||||
@@ -176,9 +192,9 @@ public final class TerminalView extends View {
|
||||
|
||||
@Override
|
||||
public void onLongPress(MotionEvent e) {
|
||||
if (mEmulator != null && !mGestureRecognizer.isInProgress()) {
|
||||
if (!mGestureRecognizer.isInProgress() && !mIsSelectingText) {
|
||||
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
|
||||
mOnKeyListener.onLongPress(e);
|
||||
toggleSelectingText(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -229,7 +245,7 @@ public final class TerminalView extends View {
|
||||
//
|
||||
// So a bit messy. If this gets too messy it's perhaps best resolved by reverting back to just
|
||||
// "TYPE_NULL" and let the Pinyin Input english keyboard be in word mode.
|
||||
outAttrs.inputType = InputType.TYPE_NULL | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD;
|
||||
outAttrs.inputType = InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD;
|
||||
|
||||
// Let part of the application show behind when in landscape:
|
||||
outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_FULLSCREEN;
|
||||
@@ -336,20 +352,36 @@ public final class TerminalView extends View {
|
||||
public void onScreenUpdated() {
|
||||
if (mEmulator == null) return;
|
||||
|
||||
boolean skipScrolling = false;
|
||||
if (mIsSelectingText) {
|
||||
// Do not scroll when selecting text.
|
||||
int rowsInHistory = mEmulator.getScreen().getActiveTranscriptRows();
|
||||
int rowShift = mEmulator.getScrollCounter();
|
||||
mSelY1 -= rowShift;
|
||||
mSelY2 -= rowShift;
|
||||
mSelYAnchor -= rowShift;
|
||||
if (-mTopRow + rowShift > rowsInHistory) {
|
||||
// .. unless we're hitting the end of history transcript, in which
|
||||
// case we abort text selection and scroll to end.
|
||||
toggleSelectingText(null);
|
||||
} else {
|
||||
skipScrolling = true;
|
||||
mTopRow -= rowShift;
|
||||
mSelY1 -= rowShift;
|
||||
mSelY2 -= rowShift;
|
||||
}
|
||||
}
|
||||
mEmulator.clearScrollCounter();
|
||||
|
||||
if (mTopRow != 0) {
|
||||
if (!skipScrolling && mTopRow != 0) {
|
||||
// Scroll down if not already there.
|
||||
if (mTopRow < -3) {
|
||||
// Awaken scroll bars only if scrolling a noticeable amount
|
||||
// - we do not want visible scroll bars during normal typing
|
||||
// of one row at a time.
|
||||
awakenScrollBars();
|
||||
}
|
||||
mTopRow = 0;
|
||||
scrollTo(0, 0);
|
||||
}
|
||||
|
||||
mEmulator.clearScrollCounter();
|
||||
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@@ -423,83 +455,105 @@ public final class TerminalView extends View {
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
@TargetApi(23)
|
||||
public boolean onTouchEvent(MotionEvent ev) {
|
||||
if (mEmulator == null) return true;
|
||||
final boolean eventFromMouse = ev.isFromSource(InputDevice.SOURCE_MOUSE);
|
||||
final int action = ev.getAction();
|
||||
|
||||
if (eventFromMouse) {
|
||||
if ((ev.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) {
|
||||
if (action == MotionEvent.ACTION_DOWN) showContextMenu();
|
||||
return true;
|
||||
} else if (mEmulator.isMouseTrackingActive() && (ev.getAction() == MotionEvent.ACTION_DOWN || ev.getAction() == MotionEvent.ACTION_UP)) {
|
||||
sendMouseEventCode(ev, TerminalEmulator.MOUSE_LEFT_BUTTON, ev.getAction() == MotionEvent.ACTION_DOWN);
|
||||
return true;
|
||||
} else if (!mEmulator.isMouseTrackingActive() && action == MotionEvent.ACTION_DOWN) {
|
||||
// Start text selection with mouse. Note that the check against MotionEvent.ACTION_DOWN is
|
||||
// important, since we otherwise would pick up secondary mouse button up actions.
|
||||
mIsSelectingText = true;
|
||||
}
|
||||
} else if (!mIsSelectingText) {
|
||||
mGestureRecognizer.onTouchEvent(ev);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mIsSelectingText) {
|
||||
int cy = (int) (ev.getY() / mRenderer.mFontLineSpacing) + mTopRow;
|
||||
int cx = (int) (ev.getX() / mRenderer.mFontWidth);
|
||||
// Offset for finger:
|
||||
final int SELECT_TEXT_OFFSET_Y = eventFromMouse ? 0 : -40;
|
||||
int cy = (int) ((ev.getY() + SELECT_TEXT_OFFSET_Y) / mRenderer.mFontLineSpacing) + mTopRow;
|
||||
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_UP:
|
||||
mInitialTextSelection = false;
|
||||
break;
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
mSelXAnchor = cx;
|
||||
mSelYAnchor = cy;
|
||||
mSelX1 = cx;
|
||||
mSelY1 = cy;
|
||||
mSelX2 = mSelX1;
|
||||
mSelY2 = mSelY1;
|
||||
invalidate();
|
||||
int distanceFromSel1 = Math.abs(cx-mSelX1) + Math.abs(cy-mSelY1);
|
||||
int distanceFromSel2 = Math.abs(cx-mSelX2) + Math.abs(cy-mSelY2);
|
||||
mIsDraggingLeftSelection = distanceFromSel1 <= distanceFromSel2;
|
||||
mSelectionDownX = ev.getX();
|
||||
mSelectionDownY = ev.getY();
|
||||
break;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
case MotionEvent.ACTION_UP:
|
||||
boolean touchBeforeAnchor = (cy < mSelYAnchor || (cy == mSelYAnchor && cx < mSelXAnchor));
|
||||
int minx = touchBeforeAnchor ? cx : mSelXAnchor;
|
||||
int maxx = !touchBeforeAnchor ? cx : mSelXAnchor;
|
||||
int miny = touchBeforeAnchor ? cy : mSelYAnchor;
|
||||
int maxy = !touchBeforeAnchor ? cy : mSelYAnchor;
|
||||
mSelX1 = minx;
|
||||
mSelY1 = miny;
|
||||
mSelX2 = maxx;
|
||||
mSelY2 = maxy;
|
||||
if (action == MotionEvent.ACTION_UP) {
|
||||
String selectedText = mEmulator.getSelectedText(mSelX1, mSelY1, mSelX2, mSelY2).trim();
|
||||
mTermSession.clipboardText(selectedText);
|
||||
toggleSelectingText();
|
||||
if (mInitialTextSelection) break;
|
||||
float deltaX = ev.getX() - mSelectionDownX;
|
||||
float deltaY = ev.getY() - mSelectionDownY;
|
||||
int deltaCols = (int) Math.ceil(deltaX / mRenderer.mFontWidth);
|
||||
int deltaRows = (int) Math.ceil(deltaY / mRenderer.mFontLineSpacing);
|
||||
mSelectionDownX += deltaCols * mRenderer.mFontWidth;
|
||||
mSelectionDownY += deltaRows * mRenderer.mFontLineSpacing;
|
||||
if (mIsDraggingLeftSelection) {
|
||||
mSelX1 += deltaCols;
|
||||
mSelY1 += deltaRows;
|
||||
} else {
|
||||
mSelX2 += deltaCols;
|
||||
mSelY2 += deltaRows;
|
||||
}
|
||||
|
||||
mSelX1 = Math.min(mEmulator.mColumns, Math.max(0, mSelX1));
|
||||
mSelX2 = Math.min(mEmulator.mColumns, Math.max(0, mSelX2));
|
||||
|
||||
if (mSelY1 == mSelY2 && mSelX1 > mSelX2 || mSelY1 > mSelY2) {
|
||||
// Switch handles.
|
||||
mIsDraggingLeftSelection = !mIsDraggingLeftSelection;
|
||||
int tmpX1 = mSelX1, tmpY1 = mSelY1;
|
||||
mSelX1 = mSelX2; mSelY1 = mSelY2;
|
||||
mSelX2 = tmpX1; mSelY2 = tmpY1;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) mActionMode.invalidateContentRect();
|
||||
invalidate();
|
||||
break;
|
||||
default:
|
||||
toggleSelectingText();
|
||||
invalidate();
|
||||
break;
|
||||
}
|
||||
mGestureRecognizer.onTouchEvent(ev);
|
||||
return true;
|
||||
} else if (ev.isFromSource(InputDevice.SOURCE_MOUSE)) {
|
||||
if (ev.isButtonPressed(MotionEvent.BUTTON_SECONDARY)) {
|
||||
if (action == MotionEvent.ACTION_DOWN) showContextMenu();
|
||||
return true;
|
||||
} else if (ev.isButtonPressed(MotionEvent.BUTTON_TERTIARY)) {
|
||||
ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clipData = clipboard.getPrimaryClip();
|
||||
if (clipData != null) {
|
||||
CharSequence paste = clipData.getItemAt(0).coerceToText(getContext());
|
||||
if (!TextUtils.isEmpty(paste)) mEmulator.paste(paste.toString());
|
||||
}
|
||||
} else if (mEmulator.isMouseTrackingActive()) { // BUTTON_PRIMARY.
|
||||
switch (ev.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
case MotionEvent.ACTION_UP:
|
||||
sendMouseEventCode(ev, TerminalEmulator.MOUSE_LEFT_BUTTON, ev.getAction() == MotionEvent.ACTION_DOWN);
|
||||
break;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
sendMouseEventCode(ev, TerminalEmulator.MOUSE_LEFT_BUTTON_MOVED, true);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
mGestureRecognizer.onTouchEvent(ev);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
|
||||
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "onKeyPreIme(keyCode=" + keyCode + ", event=" + event + ")");
|
||||
if (keyCode == KeyEvent.KEYCODE_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);
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
if (mIsSelectingText) {
|
||||
toggleSelectingText(null);
|
||||
return true;
|
||||
} else if (mOnKeyListener.shouldBackButtonBeMappedToEscape()) {
|
||||
// Intercept back button to treat it as escape:
|
||||
switch (event.getAction()) {
|
||||
case KeyEvent.ACTION_DOWN:
|
||||
return onKeyDown(keyCode, event);
|
||||
case KeyEvent.ACTION_UP:
|
||||
return onKeyUp(keyCode, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.onKeyPreIme(keyCode, event);
|
||||
@@ -518,7 +572,7 @@ public final class TerminalView extends View {
|
||||
if (handleVirtualKeys(keyCode, event, true)) {
|
||||
invalidate();
|
||||
return true;
|
||||
} else if (event.isSystem() && keyCode != KeyEvent.KEYCODE_BACK) {
|
||||
} else if (event.isSystem() && (!mOnKeyListener.shouldBackButtonBeMappedToEscape() || keyCode != KeyEvent.KEYCODE_BACK)) {
|
||||
return super.onKeyDown(keyCode, event);
|
||||
}
|
||||
|
||||
@@ -639,6 +693,10 @@ public final class TerminalView extends View {
|
||||
// As left alt+b, jumping forward in readline:
|
||||
codePoint = 'b';
|
||||
leftAltDownFromEvent = true;
|
||||
} else if (codePoint == 'v' || codePoint == 'V') {
|
||||
codePoint = -1;
|
||||
AudioManager audio = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
|
||||
audio.adjustSuggestedStreamVolume(AudioManager.ADJUST_SAME, AudioManager.USE_DEFAULT_STREAM_TYPE, AudioManager.FLAG_SHOW_UI);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -646,13 +704,20 @@ public final class TerminalView extends View {
|
||||
if (resultingKeyCode > -1) {
|
||||
handleKeyCode(resultingKeyCode, 0);
|
||||
} else {
|
||||
// The below two workarounds are needed on at least Logitech Keyboard k810 on Samsung Galaxy Tab Pro
|
||||
// (Android 4.4) with the stock Samsung Keyboard. They should be harmless when not used since the need
|
||||
// to input the original characters instead of the new ones using the keyboard should be low.
|
||||
// Rewrite U+02DC 'SMALL TILDE' to U+007E 'TILDE' for ~ to work in shells:
|
||||
if (codePoint == 0x02DC) codePoint = 0x07E;
|
||||
// Rewrite U+02CB 'MODIFIER LETTER GRAVE ACCENT' to U+0060 'GRAVE ACCENT' for ` (backticks) to work:
|
||||
if (codePoint == 0x02CB) codePoint = 0x60;
|
||||
// Work around bluetooth keyboards sending funny unicode characters instead
|
||||
// of the more normal ones from ASCII that terminal programs expect - the
|
||||
// desire to input the original characters should be low.
|
||||
switch (codePoint) {
|
||||
case 0x02DC: // SMALL TILDE.
|
||||
codePoint = 0x007E; // TILDE (~).
|
||||
break;
|
||||
case 0x02CB: // MODIFIER LETTER GRAVE ACCENT.
|
||||
codePoint = 0x0060; // GRAVE ACCENT (`).
|
||||
break;
|
||||
case 0x02C6: // MODIFIER LETTER CIRCUMFLEX ACCENT.
|
||||
codePoint = 0x005E; // CIRCUMFLEX ACCENT (^).
|
||||
break;
|
||||
}
|
||||
|
||||
// If left alt, send escape before the code point to make e.g. Alt+B and Alt+F work in readline:
|
||||
mTermSession.writeCodePoint(leftAltDownFromEvent, codePoint);
|
||||
@@ -712,67 +777,29 @@ public final class TerminalView extends View {
|
||||
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 checkForFontAndColors() {
|
||||
try {
|
||||
// Hard-coded paths since this file is used also in Termux:Float.
|
||||
@SuppressLint("SdCardPath") File fontFile = new File("/data/data/com.termux/files/home/.termux/font.ttf");
|
||||
@SuppressLint("SdCardPath") File colorsFile = new File("/data/data/com.termux/files/home/.termux/colors.properties");
|
||||
|
||||
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);
|
||||
final Properties props = new Properties();
|
||||
if (colorsFile.isFile()) {
|
||||
try (InputStream in = new FileInputStream(colorsFile)) {
|
||||
props.load(in);
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
TerminalColors.COLOR_SCHEME.updateWith(props);
|
||||
if (mEmulator != null) mEmulator.mColors.reset();
|
||||
|
||||
final Typeface newTypeface = fontFile.exists() ? Typeface.createFromFile(fontFile) : Typeface.MONOSPACE;
|
||||
mRenderer = new TerminalRenderer(mRenderer.mTextSize, newTypeface);
|
||||
updateSize();
|
||||
|
||||
invalidate();
|
||||
} catch (Exception e) {
|
||||
Log.e(EmulatorDebug.LOG_TAG, "Error in checkForFontAndColors()", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -810,13 +837,155 @@ public final class TerminalView extends View {
|
||||
canvas.drawColor(0XFF000000);
|
||||
} else {
|
||||
mRenderer.render(mEmulator, canvas, mTopRow, mSelY1, mSelY2, mSelX1, mSelX2);
|
||||
|
||||
if (mIsSelectingText) {
|
||||
final int gripHandleWidth = mLeftSelectionHandle.getIntrinsicWidth();
|
||||
final int gripHandleMargin = gripHandleWidth / 4; // See the png.
|
||||
|
||||
int right = Math.round((mSelX1) * mRenderer.mFontWidth) + gripHandleMargin;
|
||||
int top = (mSelY1+1 - mTopRow)*mRenderer.mFontLineSpacing + mRenderer.mFontLineSpacingAndAscent;
|
||||
mLeftSelectionHandle.setBounds(right - gripHandleWidth, top, right, top + mLeftSelectionHandle.getIntrinsicHeight());
|
||||
mLeftSelectionHandle.draw(canvas);
|
||||
|
||||
int left = Math.round((mSelX2+1)*mRenderer.mFontWidth) - gripHandleMargin;
|
||||
top = (mSelY2+1 - mTopRow) *mRenderer.mFontLineSpacing + mRenderer.mFontLineSpacingAndAscent;
|
||||
mRightSelectionHandle.setBounds(left, top, left + gripHandleWidth, top + mRightSelectionHandle.getIntrinsicHeight());
|
||||
mRightSelectionHandle.draw(canvas);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Toggle text selection mode in the view. */
|
||||
public void toggleSelectingText() {
|
||||
@TargetApi(23)
|
||||
public void toggleSelectingText(MotionEvent ev) {
|
||||
mIsSelectingText = !mIsSelectingText;
|
||||
if (!mIsSelectingText) mSelX1 = mSelY1 = mSelX2 = mSelY2 = -1;
|
||||
mOnKeyListener.copyModeChanged(mIsSelectingText);
|
||||
|
||||
if (mIsSelectingText) {
|
||||
if (mLeftSelectionHandle == null) {
|
||||
mLeftSelectionHandle = (BitmapDrawable) getContext().getDrawable(R.drawable.text_select_handle_left_material);
|
||||
mRightSelectionHandle = (BitmapDrawable) getContext().getDrawable(R.drawable.text_select_handle_right_material);
|
||||
}
|
||||
|
||||
int cx = (int) (ev.getX() / mRenderer.mFontWidth);
|
||||
final boolean eventFromMouse = ev.isFromSource(InputDevice.SOURCE_MOUSE);
|
||||
// Offset for finger:
|
||||
final int SELECT_TEXT_OFFSET_Y = eventFromMouse ? 0 : -40;
|
||||
int cy = (int) ((ev.getY() + SELECT_TEXT_OFFSET_Y) / mRenderer.mFontLineSpacing) + mTopRow;
|
||||
|
||||
mSelX1 = mSelX2 = cx;
|
||||
mSelY1 = mSelY2 = cy;
|
||||
|
||||
TerminalBuffer screen = mEmulator.getScreen();
|
||||
if (!" ".equals(screen.getSelectedText(mSelX1, mSelY1, mSelX1, mSelY1))) {
|
||||
// Selecting something other than whitespace. Expand to word.
|
||||
while (mSelX1 > 0 && !"".equals(screen.getSelectedText(mSelX1-1, mSelY1, mSelX1-1, mSelY1))) {
|
||||
mSelX1--;
|
||||
}
|
||||
while (mSelX2 < mEmulator.mColumns-1 && !"".equals(screen.getSelectedText(mSelX2+1, mSelY1, mSelX2+1, mSelY1))) {
|
||||
mSelX2++;
|
||||
}
|
||||
}
|
||||
|
||||
mInitialTextSelection = true;
|
||||
mIsDraggingLeftSelection = true;
|
||||
mSelectionDownX = ev.getX();
|
||||
mSelectionDownY = ev.getY();
|
||||
|
||||
final ActionMode.Callback callback = new ActionMode.Callback() {
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
final int[] ACTION_MODE_ATTRS = { android.R.attr.actionModeCopyDrawable, android.R.attr.actionModePasteDrawable, };
|
||||
TypedArray styledAttributes = getContext().obtainStyledAttributes(ACTION_MODE_ATTRS);
|
||||
try {
|
||||
int show = MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT;
|
||||
|
||||
ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
menu.add(Menu.NONE, 1, Menu.NONE, R.string.copy_text).setIcon(styledAttributes.getResourceId(0, 0)).setShowAsAction(show);
|
||||
menu.add(Menu.NONE, 2, Menu.NONE, R.string.paste_text).setIcon(styledAttributes.getResourceId(1, 0)).setEnabled(clipboard.hasPrimaryClip()).setShowAsAction(show);
|
||||
menu.add(Menu.NONE, 3, Menu.NONE, R.string.text_selection_more);
|
||||
} finally {
|
||||
styledAttributes.recycle();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case 1:
|
||||
String selectedText = mEmulator.getSelectedText(mSelX1, mSelY1, mSelX2, mSelY2).trim();
|
||||
mTermSession.clipboardText(selectedText);
|
||||
break;
|
||||
case 2:
|
||||
ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clipData = clipboard.getPrimaryClip();
|
||||
if (clipData != null) {
|
||||
CharSequence paste = clipData.getItemAt(0).coerceToText(getContext());
|
||||
if (!TextUtils.isEmpty(paste)) mEmulator.paste(paste.toString());
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
showContextMenu();
|
||||
break;
|
||||
}
|
||||
toggleSelectingText(null);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode) {
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
mActionMode = startActionMode(new ActionMode.Callback2() {
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
return callback.onCreateActionMode(mode, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||
return callback.onActionItemClicked(mode, item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode) {
|
||||
// Ignore.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
|
||||
int x1 = Math.round(mSelX1 * mRenderer.mFontWidth);
|
||||
int x2 = Math.round(mSelX2 * mRenderer.mFontWidth);
|
||||
int y1 = Math.round((mSelY1 - mTopRow) * mRenderer.mFontLineSpacing);
|
||||
int y2 = Math.round((mSelY2 + 1 - mTopRow) * mRenderer.mFontLineSpacing);
|
||||
outRect.set(Math.min(x1, x2), y1, Math.max(x1, x2), y2);
|
||||
}
|
||||
}, ActionMode.TYPE_FLOATING);
|
||||
} else {
|
||||
mActionMode = startActionMode(callback);
|
||||
}
|
||||
|
||||
|
||||
invalidate();
|
||||
} else {
|
||||
mActionMode.finish();
|
||||
mSelX1 = mSelY1 = mSelX2 = mSelY2 = -1;
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public TerminalSession getCurrentSession() {
|
||||
|
||||
@@ -22,7 +22,14 @@ static int throw_runtime_exception(JNIEnv* env, char const* message)
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int create_subprocess(JNIEnv* env, char const* cmd, char const* cwd, char* const argv[], char** envp, int* pProcessId)
|
||||
static int create_subprocess(JNIEnv* env,
|
||||
char const* cmd,
|
||||
char const* cwd,
|
||||
char* const argv[],
|
||||
char** envp,
|
||||
int* pProcessId,
|
||||
jint rows,
|
||||
jint columns)
|
||||
{
|
||||
int ptm = open("/dev/ptmx", O_RDWR | O_CLOEXEC);
|
||||
if (ptm < 0) return throw_runtime_exception(env, "Cannot open /dev/ptmx");
|
||||
@@ -49,8 +56,8 @@ static int create_subprocess(JNIEnv* env, char const* cmd, char const* cwd, char
|
||||
tios.c_iflag &= ~(IXON | IXOFF);
|
||||
tcsetattr(ptm, TCSANOW, &tios);
|
||||
|
||||
/** Set initial winsize (better too small than too large). */
|
||||
struct winsize sz = { .ws_row = 20, .ws_col = 20 };
|
||||
/** Set initial winsize. */
|
||||
struct winsize sz = { .ws_row = rows, .ws_col = columns };
|
||||
ioctl(ptm, TIOCSWINSZ, &sz);
|
||||
|
||||
pid_t pid = fork();
|
||||
@@ -105,7 +112,16 @@ static int create_subprocess(JNIEnv* env, char const* cmd, char const* cwd, char
|
||||
}
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL Java_com_termux_terminal_JNI_createSubprocess(JNIEnv* env, jclass TERMUX_UNUSED(clazz), jstring cmd, jstring cwd, jobjectArray args, jobjectArray envVars, jintArray processIdArray)
|
||||
JNIEXPORT jint JNICALL Java_com_termux_terminal_JNI_createSubprocess(
|
||||
JNIEnv* env,
|
||||
jclass TERMUX_UNUSED(clazz),
|
||||
jstring cmd,
|
||||
jstring cwd,
|
||||
jobjectArray args,
|
||||
jobjectArray envVars,
|
||||
jintArray processIdArray,
|
||||
jint rows,
|
||||
jint columns)
|
||||
{
|
||||
jsize size = args ? (*env)->GetArrayLength(env, args) : 0;
|
||||
char** argv = NULL;
|
||||
@@ -140,7 +156,7 @@ JNIEXPORT jint JNICALL Java_com_termux_terminal_JNI_createSubprocess(JNIEnv* env
|
||||
int procId = 0;
|
||||
char const* cmd_cwd = (*env)->GetStringUTFChars(env, cwd, NULL);
|
||||
char const* cmd_utf8 = (*env)->GetStringUTFChars(env, cmd, NULL);
|
||||
int ptm = create_subprocess(env, cmd_utf8, cmd_cwd, argv, envp, &procId);
|
||||
int ptm = create_subprocess(env, cmd_utf8, cmd_cwd, argv, envp, &procId, rows, columns);
|
||||
(*env)->ReleaseStringUTFChars(env, cmd, cmd_utf8);
|
||||
(*env)->ReleaseStringUTFChars(env, cmd, cmd_cwd);
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:src="@drawable/text_select_handle_left_mtrl_alpha"
|
||||
android:tint="#2196F3" />
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:src="@drawable/text_select_handle_right_mtrl_alpha"
|
||||
android:tint="#2196F3" />
|
||||
BIN
app/src/main/res/raw/bell.ogg
Normal file
BIN
app/src/main/res/raw/bell.ogg
Normal file
Binary file not shown.
@@ -1,11 +1,8 @@
|
||||
<?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>
|
||||
@@ -15,7 +12,7 @@
|
||||
<string name="help">Help</string>
|
||||
|
||||
<string name="welcome_dialog_title">Welcome to Termux</string>
|
||||
<string name="welcome_dialog_body">Long press anywhere on the terminal for a context menu where Help is available.\n\nExecute \'apt update\' to update the packages list before installing packages.</string>
|
||||
<string name="welcome_dialog_body">Long press and select <i>More…</i> to show a menu where <i>Help</i> is available.\n\nExecute <b>apt update</b> to update the packages list before installing packages.</string>
|
||||
<string name="welcome_dialog_dont_show_again_button">Do not show again</string>
|
||||
|
||||
<string name="bootstrap_installer_body">Installing…</string>
|
||||
@@ -30,18 +27,18 @@
|
||||
|
||||
<string name="reset_toast_notification">Terminal reset.</string>
|
||||
|
||||
<string name="select">Select…</string>
|
||||
<string name="select_text">Select text</string>
|
||||
<string name="select_url">Select URL</string>
|
||||
<string name="select_url_dialog_title">Click URL to copy or long press to open</string>
|
||||
<string name="select_all_and_share">Select all text and share</string>
|
||||
<string name="select_all_and_share">Share transcript</string>
|
||||
<string name="select_url_no_found">No URL found in the terminal.</string>
|
||||
<string name="select_url_copied_to_clipboard">URL copied to clipboard</string>
|
||||
<string name="share_transcript_chooser_title">Send text to:</string>
|
||||
|
||||
<string name="paste_text">Paste</string>
|
||||
<string name="kill_process">Hangup</string>
|
||||
<string name="copy_text">Copy</string>
|
||||
<string name="text_selection_more">More…</string>
|
||||
|
||||
<string name="kill_process">Hangup</string>
|
||||
<string name="confirm_kill_process">Close this process?</string>
|
||||
|
||||
<string name="session_rename_title">Set session name</string>
|
||||
@@ -58,4 +55,8 @@
|
||||
|
||||
<string name="empty_folder">Empty folder.</string>
|
||||
|
||||
<string name="file_received_title">Save file in ~/downloads/</string>
|
||||
<string name="file_received_edit_button">Edit</string>
|
||||
<string name="file_received_open_folder_button">Open folder</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -6,10 +6,13 @@
|
||||
<style name="Theme.Termux" parent="@android:style/Theme.Material.Light.NoActionBar">
|
||||
<item name="android:statusBarColor">#000000</item>
|
||||
<item name="android:windowBackground">@android:color/black</item>
|
||||
|
||||
|
||||
<!-- Seen in buttons on left drawer: -->
|
||||
<item name="android:colorAccent">#212121</item>
|
||||
<item name="android:alertDialogTheme">@style/TermuxAlertDialogStyle</item>
|
||||
<item name="android:alertDialogTheme">@style/TermuxAlertDialogStyle</item>
|
||||
<!-- Avoid action mode toolbar pushing down terminal content when
|
||||
selecting text on pre-6.0 (non-floating toolbar). -->
|
||||
<item name="android:windowActionModeOverlay">true</item>
|
||||
</style>
|
||||
|
||||
<style name="TermuxAlertDialogStyle" parent="@android:style/Theme.Material.Light.Dialog.Alert">
|
||||
@@ -17,4 +20,4 @@
|
||||
<item name="android:colorAccent">#212121</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
@@ -20,4 +20,13 @@ public class ControlSequenceIntroducerTest extends TerminalTestCase {
|
||||
withTerminalSized(3, 4).enterString("1\r\n2\r\n3\r\nhi\033[Sy").assertLinesAre("2 ", "3 ", "hi ", " y");
|
||||
}
|
||||
|
||||
/** CSI Ps X Erase Ps Character(s) (default = 1) (ECH). */
|
||||
public void testCsiX() {
|
||||
// See https://code.google.com/p/chromium/issues/detail?id=212712 where test was extraced from.
|
||||
withTerminalSized(13, 2).enterString("abcdefghijkl\b\b\b\b\b\033[X").assertLinesAre("abcdefg ijkl ", " ");
|
||||
withTerminalSized(13, 2).enterString("abcdefghijkl\b\b\b\b\b\033[1X").assertLinesAre("abcdefg ijkl ", " ");
|
||||
withTerminalSized(13, 2).enterString("abcdefghijkl\b\b\b\b\b\033[2X").assertLinesAre("abcdefg jkl ", " ");
|
||||
withTerminalSized(13, 2).enterString("abcdefghijkl\b\b\b\b\b\033[20X").assertLinesAre("abcdefg ", " ");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -163,7 +163,12 @@ public class CursorAndScreenTest extends TerminalTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
public void testHorizontalTabColorsBackground() {
|
||||
/**
|
||||
* See comments on horizontal tab handling in TerminalEmulator.java.
|
||||
*
|
||||
* We do not want to color already written cells when tabbing over them.
|
||||
*/
|
||||
public void DISABLED_testHorizontalTabColorsBackground() {
|
||||
withTerminalSized(10, 3).enterString("\033[48;5;15m").enterString("\t");
|
||||
assertCursorAt(0, 8);
|
||||
for (int i = 0; i < 10; i++) {
|
||||
@@ -172,4 +177,54 @@ public class CursorAndScreenTest extends TerminalTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test interactions between the cursor overflow bit and various escape sequences.
|
||||
* <p/>
|
||||
* Adapted from hterm:
|
||||
* https://chromium.googlesource.com/chromiumos/platform/assets/+/2337afa5c063127d5ce40ec7fec9b602d096df86%5E%21/#F2
|
||||
*/
|
||||
public void testClearingOfAutowrap() {
|
||||
// Fill a row with the last hyphen wrong, then run a command that
|
||||
// modifies the screen, then add a hyphen. The wrap bit should be
|
||||
// cleared, so the extra hyphen can fix the row.
|
||||
withTerminalSized(15, 6);
|
||||
|
||||
enterString("----- 1 ----X");
|
||||
enterString("\033[K-"); // EL
|
||||
|
||||
enterString("----- 2 ----X");
|
||||
enterString("\033[J-"); // ED
|
||||
|
||||
enterString("----- 3 ----X");
|
||||
enterString("\033[@-"); // ICH
|
||||
|
||||
enterString("----- 4 ----X");
|
||||
enterString("\033[P-"); // DCH
|
||||
|
||||
enterString("----- 5 ----X");
|
||||
enterString("\033[X-"); // ECH
|
||||
|
||||
// DL will delete the entire line but clear the wrap bit, so we
|
||||
// expect a hyphen at the end and nothing else.
|
||||
enterString("XXXXXXXXXXXXXXX");
|
||||
enterString("\033[M-"); // DL
|
||||
|
||||
assertLinesAre(
|
||||
"----- 1 -----",
|
||||
"----- 2 -----",
|
||||
"----- 3 -----",
|
||||
"----- 4 -----",
|
||||
"----- 5 -----",
|
||||
" -");
|
||||
}
|
||||
|
||||
public void testBackspaceAcrossWrappedLines() {
|
||||
// Backspace should not go to previous line if not auto-wrapped:
|
||||
withTerminalSized(3, 3).enterString("hi\r\n\b\byou").assertLinesAre("hi ", "you", " ");
|
||||
// Backspace should go to previous line if auto-wrapped:
|
||||
withTerminalSized(3, 3).enterString("hi y").assertLinesAre("hi ", "y ", " ").enterString("\b\b#").assertLinesAre("hi#", "y ", " ");
|
||||
// Initial backspace should do nothing:
|
||||
withTerminalSized(3, 3).enterString("\b\b\b\bhi").assertLinesAre("hi ", " ", " ");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -28,6 +28,11 @@ public class DecSetTest extends TerminalTestCase {
|
||||
assertFalse(mTerminal.isShowingCursor());
|
||||
mTerminal.reset();
|
||||
assertTrue("Resetting the terminal should show the cursor", mTerminal.isShowingCursor());
|
||||
|
||||
enterString("\033[?25l");
|
||||
assertFalse(mTerminal.isShowingCursor());
|
||||
enterString("\033c"); // RIS resetting should reveal cursor.
|
||||
assertTrue(mTerminal.isShowingCursor());
|
||||
}
|
||||
|
||||
/** DECSET 2004, controls bracketed paste mode. */
|
||||
|
||||
@@ -111,6 +111,10 @@ public class KeyHandlerTest extends TestCase {
|
||||
// Backspace.
|
||||
assertKeysEquals("\u007f", KeyHandler.getCode(KeyEvent.KEYCODE_DEL, 0, false, false));
|
||||
|
||||
// Space.
|
||||
assertNull(KeyHandler.getCode(KeyEvent.KEYCODE_SPACE, 0, false, false));
|
||||
assertKeysEquals("\u0000", KeyHandler.getCode(KeyEvent.KEYCODE_SPACE, KeyHandler.KEYMOD_CTRL, false, false));
|
||||
|
||||
// Back tab.
|
||||
assertKeysEquals("\033[Z", KeyHandler.getCode(KeyEvent.KEYCODE_TAB, KeyHandler.KEYMOD_SHIFT, false, false));
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ public class OperatingSystemControlTest extends TerminalTestCase {
|
||||
withTerminalSized(10, 10);
|
||||
enterString("\033]0;Hello, world\007");
|
||||
assertEquals("Hello, world", mTerminal.getTitle());
|
||||
expectedTitleChanges.add(new ChangedTitle((String) null, "Hello, world"));
|
||||
expectedTitleChanges.add(new ChangedTitle(null, "Hello, world"));
|
||||
assertEquals(expectedTitleChanges, mOutput.titleChanges);
|
||||
|
||||
enterString("\033]0;Goodbye, world\007");
|
||||
|
||||
@@ -94,4 +94,8 @@ public class ScrollRegionTest extends TerminalTestCase {
|
||||
withTerminalSized(3, 3).enterString("\033[?69h\033[0;2sABCD\0339").assertLinesAre("B ", "D ", " ");
|
||||
}
|
||||
|
||||
public void testScrollDownWithScrollRegion() {
|
||||
withTerminalSized(2, 5).enterString("1\r\n2\r\n3\r\n4\r\n5").assertLinesAre("1 ", "2 ", "3 ", "4 ", "5 ");
|
||||
enterString("\033[3r").enterString("\033[2T").assertLinesAre("1 ", "2 ", " ", " ", "3 ");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,6 +92,7 @@ public class TerminalTest extends TerminalTestCase {
|
||||
assertEnteringStringGivesResponse("\033[6n", "\033[2;1R");
|
||||
}
|
||||
|
||||
/** Test the cursor shape changes using DECSCUSR. */
|
||||
public void testSetCursorStyle() throws Exception {
|
||||
withTerminalSized(5, 5);
|
||||
assertEquals(TerminalEmulator.CURSOR_STYLE_BLOCK, mTerminal.getCursorStyle());
|
||||
@@ -257,4 +258,9 @@ public class TerminalTest extends TerminalTestCase {
|
||||
withTerminalSized(3, 3).enterString("abc\r ").assertLinesAre(" bc", " ", " ").assertCursorAt(0, 1);
|
||||
}
|
||||
|
||||
public void testTab() {
|
||||
withTerminalSized(11, 2).enterString("01234567890\r\tXX").assertLinesAre("01234567XX0", " ");
|
||||
withTerminalSized(11, 2).enterString("01234567890\033[44m\r\tXX").assertLinesAre("01234567XX0", " ");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -12,6 +12,47 @@ public class UnicodeInputTest extends TerminalTestCase {
|
||||
withTerminalSized(5, 5);
|
||||
mTerminal.append(new byte[]{(byte) 0b11101111, (byte) 'a'}, 2);
|
||||
assertLineIs(0, ((char) TerminalEmulator.UNICODE_REPLACEMENT_CHAR) + "a ");
|
||||
|
||||
// https://code.google.com/p/chromium/issues/detail?id=212704
|
||||
byte[] input = new byte[]{
|
||||
(byte) 0x61, (byte) 0xF1,
|
||||
(byte) 0x80, (byte) 0x80,
|
||||
(byte) 0xe1, (byte) 0x80,
|
||||
(byte) 0xc2, (byte) 0x62,
|
||||
(byte) 0x80, (byte) 0x63,
|
||||
(byte) 0x80, (byte) 0xbf,
|
||||
(byte) 0x64
|
||||
};
|
||||
withTerminalSized(10, 2);
|
||||
mTerminal.append(input, input.length);
|
||||
assertLinesAre("a\uFFFD\uFFFD\uFFFDb\uFFFDc\uFFFD\uFFFDd", " ");
|
||||
|
||||
// Surrogate pairs.
|
||||
withTerminalSized(5, 2);
|
||||
input = new byte[]{
|
||||
(byte) 0xed, (byte) 0xa0,
|
||||
(byte) 0x80, (byte) 0xed,
|
||||
(byte) 0xad, (byte) 0xbf,
|
||||
(byte) 0xed, (byte) 0xae,
|
||||
(byte) 0x80, (byte) 0xed,
|
||||
(byte) 0xbf, (byte) 0xbf
|
||||
};
|
||||
mTerminal.append(input, input.length);
|
||||
assertLinesAre("\uFFFD\uFFFD\uFFFD\uFFFD ", " ");
|
||||
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=746900: "with this patch 0xe0 0x80 is decoded as two U+FFFDs,
|
||||
// but 0xe0 0xa0 is decoded as a single U+FFFD, and this is correct according to the "Best Practices", but IE
|
||||
// and Chrome (Version 22.0.1229.94) decode both of them as two U+FFFDs. Opera 12.11 decodes both of them as
|
||||
// one U+FFFD".
|
||||
withTerminalSized(5, 2);
|
||||
input = new byte[]{(byte) 0xe0, (byte) 0xa0, ' '};
|
||||
mTerminal.append(input, input.length);
|
||||
assertLinesAre("\uFFFD ", " ");
|
||||
|
||||
// withTerminalSized(5, 2);
|
||||
// input = new byte[]{(byte) 0xe0, (byte) 0x80, 'a'};
|
||||
// mTerminal.append(input, input.length);
|
||||
// assertLinesAre("\uFFFD\uFFFDa ", " ");
|
||||
}
|
||||
|
||||
public void testUnassignedCodePoint() throws UnsupportedEncodingException {
|
||||
|
||||
@@ -39,6 +39,7 @@ public class WcWidthTest extends TestCase {
|
||||
public void testCombining() {
|
||||
assertWidthIs(0, 0x0302);
|
||||
assertWidthIs(0, 0x0308);
|
||||
assertWidthIs(0, 0x2060);
|
||||
}
|
||||
|
||||
public void testWatch() {
|
||||
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
||||
#Sat Nov 28 23:59:10 CET 2015
|
||||
#Tue Mar 15 00:24:33 CET 2016
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-2.9-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-2.12-bin.zip
|
||||
|
||||
90
gradlew.bat
vendored
90
gradlew.bat
vendored
@@ -1,90 +0,0 @@
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS=
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windowz variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
if "%@eval[2+2]" == "4" goto 4NT_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
goto execute
|
||||
|
||||
:4NT_args
|
||||
@rem Get arguments from the 4NT Shell from JP Software
|
||||
set CMD_LINE_ARGS=%$
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
BIN
travis.keystore
BIN
travis.keystore
Binary file not shown.
Reference in New Issue
Block a user