Compare commits

...

138 Commits
v0.17 ... v0.34

Author SHA1 Message Date
Fredrik Fornwall
746dc750df Bump version to 0.34 2016-04-22 03:48:28 +02:00
Fredrik Fornwall
7db1f6c5a1 Add explicit handling of switch constant 2016-04-22 03:30:09 +02:00
Fredrik Fornwall
b7f3fdf528 Update android-annotations version 2016-04-22 03:28:50 +02:00
Fredrik Fornwall
6e7f777d04 Remove support for ACTION_GET_CONTENT
As we have a document provider now we can remove ACTION_GET_CONTENT.

"The ACTION_OPEN_DOCUMENT intent is only available on devices running
Android 4.4 and higher. If you want your application to support
ACTION_GET_CONTENT to accommodate devices that are running Android 4.3
and lower, you should disable the ACTION_GET_CONTENT intent filter in
your manifest for devices running Android 4.4 or higher. A document
provider and ACTION_GET_CONTENT should be considered mutually exclusive.
If you support both of them simultaneously, your app will appear twice
in the system picker UI, offering two different ways of accessing your
stored data. This would be confusing for users."

- http://developer.android.com/guide/topics/providers/document-provider.html#43
2016-04-22 03:26:23 +02:00
Fredrik Fornwall
fc15bd2355 Documents provider: Remove FLAG_SUPPORTS_RECENTS 2016-04-22 02:51:13 +02:00
Fredrik Fornwall
a87cbdd70c Make VolumeUp+x send Alt+x 2016-04-22 02:20:10 +02:00
Fredrik Fornwall
fb7f7d249e Ignore warnings about setExecutable(true) failing 2016-04-22 02:17:41 +02:00
Fredrik Fornwall
026d0b495e Expose files through the Storage Access Framework
This allows e.g. external editors to edit files in the Termux home
folder. Fixes #79.
2016-04-22 02:13:50 +02:00
Fredrik Fornwall
533fa60516 Remove unused imports 2016-04-22 02:00:36 +02:00
Fredrik Fornwall
dc086a1e0b Tweak button ordering on the file received dialog 2016-04-16 23:02:20 +02:00
Fredrik Fornwall
2a056aeb2e Match less intents for receiving files
Be liberal when accepting SEND intents, but restrict to text files
for VIEW intents.
2016-04-16 23:01:19 +02:00
Fredrik Fornwall
9e70ebc2a6 Build native libraries for 64-bit arm 2016-04-16 21:22:04 +02:00
Fredrik Fornwall
9686127f81 Fix installer to check supported abi:s
This fixes installation on e.g. the Samsung Galaxy S5 Neo which has
a 64-bit cpu but no 64-bit runtime available (closes #69).
2016-04-16 21:18:21 +02:00
Fredrik Fornwall
395c36ee83 Fix typo 2016-04-11 14:13:02 +02:00
Fredrik Fornwall
906ff24e76 Avoid repetition of home path constant 2016-04-11 14:12:21 +02:00
Fredrik Fornwall
c8af974852 Bump version in preparation for 0.33 2016-04-11 14:06:23 +02:00
Fredrik Fornwall
481339e2f5 Do not force chooser when opening url 2016-04-11 14:05:31 +02:00
Fredrik Fornwall
b2ecae63a8 Update to Android Studio 2.0 2016-04-11 13:50:14 +02:00
Fredrik Fornwall
a67f798f2f Update the android gradle plugin 2016-04-11 13:48:24 +02:00
Fredrik Fornwall
d69485b70b Match less files for file receiving (fixes #66) 2016-03-25 00:22:49 +01:00
Fredrik Fornwall
421dfcca39 Do not fail with NPE when scheme is null
Also remove some debug logging left by mistake.
2016-03-23 18:31:03 +01:00
Fredrik Fornwall
3aaa0ab267 Remove unused imports 2016-03-21 15:24:02 +01:00
Fredrik Fornwall
e7f9647beb Remove unused variable 2016-03-21 15:19:08 +01:00
Fredrik Fornwall
5558f371b4 Bump version to 0.31 2016-03-21 00:01:59 +01:00
Fredrik Fornwall
0882ed6470 Keep the EXTERNAL_STORAGE environment variable
The EXTERNAL_STORAGE environment variable is needed on at least the
Samsung Galaxy S7 for /system/bin/am to function.
2016-03-20 23:57:34 +01:00
Fredrik Fornwall
5c02448521 Install 64-bit arm packages on capable devices
This will only affect new installations. Existing users wishing to
install 64-bit packages can re-install the app completely, or just
'rm -Rf $PREFIX' and exit all sessions, which will cause Termux to
re-install all packages at next startup.
2016-03-20 22:24:05 +01:00
Fredrik Fornwall
17382fb190 Do not have /system/bin in the PATH
By appending the old system PATH environment variable to the paths
setup by Termux system binaries are found as a fallback.

This causes problems with system binaries not working (due to
LD_LIBRARY_PATH) and causing a lot of confusion for new users when
e.g. an Android system provides a system version of e.g. curl, ssh
and other programs. It's better for these users to be prompted to
install the proper Termux package, and advanced users can still
add /system/bin to the PATH themselves.

Certain programs such as 'am' and 'pm' are already setup in
$PREFIX/bin to clear LD_LIBRARY_PATH and launch the binaries in
/system/bin - if there are some more popular ones they could be
added in the same way.
2016-03-20 22:17:21 +01:00
Fredrik Fornwall
d6eea83bfc Make it possible to receive files
The files are saved to $HOME/downloads/, after which the user
may choose to open the downloads/ folder or edit the file with
the $HOME/bin/termux-file-editor program.

It's also possible to receive URL:s, in which case the
$HOME/bin/termux-url-opener program will be called.
2016-03-19 00:17:38 +01:00
Fredrik Fornwall
51181c2d49 Fix method reference in javadoc 2016-03-17 12:31:45 +01:00
Fredrik Fornwall
480b8a4f7e Recycle a TypedArray after usage
Also add two suppress lint annotations.
2016-03-17 12:29:30 +01:00
Fredrik Fornwall
f989157f10 Extract constants 2016-03-16 23:10:44 +01:00
Fredrik Fornwall
0e942f90a6 Update gradle from 2.10 to 2.12 2016-03-15 00:26:36 +01:00
Fredrik Fornwall
5b8eca46a1 Set 4 space indentation in .editorconfig 2016-03-15 00:09:10 +01:00
Fredrik Fornwall
493900d60b Add PATH environemnt variable in failsafe mode 2016-03-15 00:08:31 +01:00
Fredrik Fornwall
c6d6a63637 Extract variable for clarity 2016-03-11 01:20:54 +01:00
Fredrik Fornwall
ca71265f23 Handle backspace across wrapped lines (closes #59) 2016-03-07 23:45:02 +01:00
Fredrik Fornwall
46c9c4b80e Catch IllegalArgumentException from startActivity 2016-03-01 16:33:22 +01:00
Fredrik Fornwall
6ca055bb25 Fix tabs to not overwrite cells 2016-02-25 16:33:00 +01:00
Fredrik Fornwall
ce7ad530cd TermuxFilePickerProvider: Small improvements
1. Return true from onCreate().
2. Implement getType().
2016-02-14 00:49:27 +01:00
Fredrik Fornwall
d0015cbe82 Bump version to 0.29 2016-02-14 00:45:53 +01:00
Fredrik Fornwall
9e19217f8f Force refresh when returning in onStart()
This makes sure that terminal session changes that has happened
while away are visible when returning.
2016-02-14 00:44:33 +01:00
Fredrik Fornwall
048af64093 Map everything starting with x86 to i686
This fixes CPU detection for ARC welder which reports x86.
2016-02-14 00:42:34 +01:00
Fredrik Fornwall
a8f7bf1b6e Clarify how to find the Help menu entry 2016-02-13 22:01:18 +01:00
Fredrik Fornwall
62e229e184 Fix LOG_KEY_EVENTS=true committed by mistake 2016-02-13 00:27:58 +01:00
Fredrik Fornwall
36e4d94093 Add *.so to .gitignore 2016-02-09 11:33:57 +01:00
Fredrik Fornwall
d2c9c5a0f0 Bump version to 0.28 2016-02-09 11:25:58 +01:00
Fredrik Fornwall
6405180cb8 Wait for terminal size before starting process
This fixes https://github.com/termux/termux-widget/issues/2, which
was caused by the terminal launching the terminal session process
before the terminal size was known.

Also remove the built JNI libraries from source control.
2016-02-09 11:24:05 +01:00
Fredrik Fornwall
1b6919bb23 Add test comment 2016-01-28 16:45:45 +01:00
Fredrik Fornwall
e52cd2dd41 Bump version to 0.27 2016-01-21 11:53:37 +01:00
Fredrik Fornwall
54857d5fd4 Replace surrogate chars with U+FFFD
Also add some more unicode input tests.
2016-01-19 23:11:20 +01:00
Fredrik Fornwall
38dd99e827 Add wcwidth test for U+2060 2016-01-19 23:08:28 +01:00
Fredrik Fornwall
7256b04317 Clear autowrap bit at some escape sequences
Add test adapted from chromiums hterm.
2016-01-19 17:24:18 +01:00
Fredrik Fornwall
01a1c6de0f Change default behaviour of back key to back
It's still possible to set it to escape using configuration
2016-01-19 17:22:57 +01:00
Fredrik Fornwall
497fc3ecd0 Add VolumeUp+V to show volume control 2016-01-19 17:21:27 +01:00
Fredrik Fornwall
b2b39abacd Recognize '\033c' - RIS, reset terminal state 2016-01-19 12:05:38 +01:00
Fredrik Fornwall
bee305e53f Whitespace/tabs consistency in AndroidManifest.xml 2016-01-19 12:01:35 +01:00
Fredrik Fornwall
c8d2f28ed8 Terminal emulation: Test "CSI X"/ECH processing 2016-01-18 15:15:34 +01:00
Fredrik Fornwall
19eb371d23 Do not force soft keyboard visible when hw exists 2016-01-13 13:17:53 +01:00
Fredrik Fornwall
6caaae4fd6 Do not start text selection directly on LMB 2016-01-13 13:17:36 +01:00
Fredrik Fornwall
ed544102bc Show icons for copy and paste menu items 2016-01-13 12:28:31 +01:00
Fredrik Fornwall
8f1ab1bc17 Use action mode overlay on pre-6.0 devices
This avoids the terminal content from being pushed down when starting
text selection. The drawback is that one cannot select text at the
top rows without scrolling - something to fix for the future.
2016-01-13 12:27:15 +01:00
Fredrik Fornwall
50337cbf9d Fix gesture handling while selecting text
Also remove stray debug logging.
2016-01-13 10:52:23 +01:00
Fredrik Fornwall
fa9ea2db5c Do not auto scroll when selecting text 2016-01-13 04:20:36 +01:00
Fredrik Fornwall
7a659ebd21 Improve session name dialog
- Show keyboard directly.
- Let return create the session.
2016-01-13 03:44:03 +01:00
Fredrik Fornwall
54bc1ed791 Do not recognize gestures while selecting text 2016-01-13 03:42:46 +01:00
Fredrik Fornwall
fe4365c94b Simplify long press on new session button 2016-01-13 03:18:22 +01:00
Fredrik Fornwall
0c13ea1bd4 Bump version to 0.26 2016-01-13 03:02:09 +01:00
Fredrik Fornwall
862b461a07 Improve text selection functionality
- Make text selection easier and quicker by selecting text directly on long press, and using standard grip bars for changing the selection.
- Disable the drawer while selecting text.
- Fix problem with selecting snippets of text with wide unicode characters at start and end.
- Remove the "tap-screen" configuration option for a more common show keyboard behaviour when tapping the terminal.
- Do no longer map the back key to escape by default - but it's still possible to do by configuration.
- Add new hardware keyboard shortcut Ctrl+Shift+K for toggling soft keyboard visibility.
2016-01-13 03:01:29 +01:00
Fredrik Fornwall
845976be0f Update README.md 2016-01-13 01:29:57 +01:00
Fredrik Fornwall
60bdaa3bf6 Add test for space handling 2016-01-05 03:14:18 +01:00
Fredrik Fornwall
eeb873f4e4 Do not save instance state in DrawerLayout
This was not needed, and due to missing CREATOR field caused a crash
after returning to the activity after it was evicted
2016-01-05 01:00:25 +01:00
Fredrik Fornwall
207ddf9fdc Change member to local variable 2016-01-05 00:48:16 +01:00
Fredrik Fornwall
5ca82ea095 Bump version to 0.25 2015-12-30 00:49:35 +01:00
Fredrik Fornwall
913c474d32 Input normal ^ even on other unicode char input
Some bluetooth keyboards [1] input U+02C6, the unicode character
MODIFIER LETTER CIRCUMFLEX ACCENT instead of the more common ^
(U+005E CIRCUMFLEX ACCENT). Remap it to the common caret since
that is what terminal programs expect.

[1] https://plus.google.com/100972300636796512022/posts/f7PKpXWesgG
2015-12-30 00:46:08 +01:00
Fredrik Fornwall
657c270d97 Fix error message 2015-12-28 20:49:46 +01:00
Fredrik Fornwall
7763931035 Bump app version to 0.24 2015-12-28 20:48:13 +01:00
Fredrik Fornwall
50005bc794 Fix problem with font and color loading at startup
Using View#post() does not work in onCreate().
2015-12-28 20:47:34 +01:00
Fredrik Fornwall
96f5ed985a Remove unused files 2015-12-28 02:30:28 +01:00
Fredrik Fornwall
c3aa9d9662 Updated storage handling
Let the user run termux-setup-storage, which will ensure that storage
permission has been granted and setup $HOME/storage/ folder with
symlinks to storage folders.
2015-12-28 01:37:00 +01:00
Fredrik Fornwall
47634ca237 Update AS configuration 2015-12-28 01:17:40 +01:00
Fredrik Fornwall
d9ec1bf40b Add .idea/dictionaries/ to .gitignore 2015-12-28 00:41:21 +01:00
Fredrik Fornwall
bc1b742a36 Remove bundled help in favour of online help 2015-12-28 00:40:51 +01:00
Fredrik Fornwall
86e2945069 Make it possible to reload settings at runtime 2015-12-27 15:35:03 +01:00
Fredrik Fornwall
961e06379b Avoid prompting for storage permission for now 2015-12-27 13:46:20 +01:00
Fredrik Fornwall
468185efb3 Transition to https://termux.net for bootstrap
The initial bootstrap zip was previously downloaded from
http://apt.termux.com, which lacked security and was not behind a CDN.

By moving to https://termux.net we improve security (as it's https)
and reliability (as it's using a CDN).

Fixes https://github.com/termux/termux-packages/issues/89.
2015-12-27 08:17:29 +01:00
Fredrik Fornwall
e006e36dd0 Do not eat escape key events in onKeyPreIme()
The original reason for intercepting the escape key in onKeyPreIme()
was to prevent the escape key as being treated as the Back key before
reaching onKeyDown().

This seems no long necessary, and may mess up handling the ...+1
combination for escape on the Google Pixel C keyboard (see #27).
2015-12-25 21:10:48 +01:00
Fredrik Fornwall
dd38965c46 Use $PREFIX/storage for symlinks. Rix null check. 2015-12-24 09:29:14 +01:00
Fredrik Fornwall
79d56b778d Update gradle to 2.10 2015-12-23 01:44:51 +01:00
Fredrik Fornwall
2326c52199 Implement support for termux.properties
Also some symlink-to-storage improvements and experimenting with
requesting read storage permission.
2015-12-23 01:43:41 +01:00
Fredrik Fornwall
f907684ef2 Read support for customizing through properties
By using the file $HOME/.config/termux/termux.properties it will
be possible to configure the behaviour of:

- The bell character.
- Tapping the terminal.
- The back key.
2015-12-23 01:39:49 +01:00
Fredrik Fornwall
9d37461ac7 Fix lint warnings 2015-12-23 01:12:47 +01:00
Fredrik Fornwall
4de0f98fa4 Fix lint warning 2015-12-23 01:08:23 +01:00
Fredrik Fornwall
f153a72592 TerminalView: Make theming work for non-activities 2015-12-20 14:37:00 +01:00
Fredrik Fornwall
b0f4efb0bc Bump version to 0.23 2015-12-20 14:26:08 +01:00
Fredrik Fornwall
5c03c2d77e Change theme for file picker 2015-12-20 14:25:07 +01:00
Fredrik Fornwall
bce65f7db1 Setup $HOME/storage symlink for external symlink
At startup Termux now checks if there is external storage available,
and creates a private area on the external storage if one exists as
well as creating a symlink to the private are at $HOME/storage.
2015-12-12 03:02:06 +01:00
Fredrik Fornwall
e18579164f Bump version number to 0.22 2015-12-12 00:10:43 +01:00
Fredrik Fornwall
16273a1981 Fix bug with scrolling down and top scroll margin
The Termux implementation of the ${CSI}${N}T escape sequence to scroll
down N lines (SD - Pan Up) did not take the top margin into account
when figuring out where to place the scrolled rows.

Fixes #28.
2015-12-12 00:07:55 +01:00
Fredrik Fornwall
ce82979e2b Update idea codeStyleSettings.xml 2015-12-05 20:16:20 +01:00
Fredrik Fornwall
625aeab398 Reformat code 2015-11-30 00:39:24 +01:00
Fredrik Fornwall
bad6712338 Improve motion event handling after long press
When mouse reporting is enabled, do not send mouse events on up
after a long press, since that causes e.g. the cursor to move in
vim when lifting the finger after long pressing for the menu.
2015-11-29 10:56:31 +01:00
Fredrik Fornwall
b54c7909bd Fix crash with wide character in last column
Ignore wide character outputs instead of crashing when the cursor
is in the last column with autowrap disabled.
2015-11-29 10:04:50 +01:00
Fredrik Fornwall
4ccc703fcf Remove dead code 2015-11-29 10:03:38 +01:00
Fredrik Fornwall
7348820caf Add screen wraparound test 2015-11-29 09:36:06 +01:00
Fredrik Fornwall
525985b1f2 Remove some dead code 2015-11-29 08:56:56 +01:00
Fredrik Fornwall
ab3852d2e4 Add missing null guard for no current session 2015-11-29 08:55:13 +01:00
Fredrik Fornwall
9928073e48 Fix bitwise operation issue 2015-11-29 02:20:37 +01:00
Fredrik Fornwall
3091da64bc Update travis config 2015-11-29 02:00:55 +01:00
Fredrik Fornwall
74dca95101 Fix inspect code warnings 2015-11-29 01:55:41 +01:00
Fredrik Fornwall
2a6a3b76b7 Update travis config 2015-11-29 01:45:11 +01:00
Fredrik Fornwall
0e4ea95d74 Revert to gradle-bin to check travis build failure 2015-11-29 01:27:13 +01:00
Fredrik Fornwall
7389dbb56f Fix two Android Studio inspect code warnings 2015-11-29 01:12:27 +01:00
Fredrik Fornwall
d982c71efe Url-regexp: Remove redundant escapes and add test 2015-11-29 01:08:18 +01:00
Fredrik Fornwall
1b36c684d6 Add a customized auto backup
Starting with Android 6.0 the system may automatically backup app
data when a users installs an app on a new device or reinstalls an
app on one. After this commit this only affects the $HOME/backup/
folder, so that the user may choose what to backup.

See https://developer.android.com/training/backup/autosyncapi.html
2015-11-29 00:55:05 +01:00
Fredrik Fornwall
a6a83b1fcd Tweak file picker layout 2015-11-29 00:42:47 +01:00
Fredrik Fornwall
d6f01bfe9a Update gradle config and add Support Annotations 2015-11-29 00:25:51 +01:00
Fredrik Fornwall
271dd7dcee Specify Android Studio "Inspect Code" profile 2015-11-29 00:23:18 +01:00
Fredrik Fornwall
95fbb810e2 Code simplifications 2015-11-29 00:20:45 +01:00
Fredrik Fornwall
1aa439311b Remove some warnings 2015-11-29 00:17:50 +01:00
Fredrik Fornwall
12ddaccaf7 Extract hardcoded string to resource 2015-11-29 00:09:25 +01:00
Fredrik Fornwall
09fe7e5941 Use the gradle source distribution for the wrapper 2015-11-29 00:08:07 +01:00
Fredrik Fornwall
36cc010a87 Show bold text in bright colors
Fixes 17. Could be made an option in the future if necessary.
2015-11-24 17:31:48 +01:00
Fredrik Fornwall
b1aaf5abe5 Add *.apk to .gitignore 2015-11-24 17:31:32 +01:00
Fredrik Fornwall
9b3dc57447 Remove junk from app/build.gradle 2015-11-20 23:29:44 +01:00
Fredrik Fornwall
f7ce206212 Bump version to 0.20 2015-11-20 00:40:52 +01:00
Fredrik Fornwall
0deacd8fc6 Show "-press Enter to close" at session exit
This makes it more clear how to close the session after finishing.
Fixes #15.
2015-11-20 00:38:58 +01:00
Fredrik Fornwall
65cfcffa6f Update to gradle 2.9 2015-11-17 21:53:40 +01:00
Fredrik Fornwall
463b927813 Bump version to 0.19 2015-11-13 00:22:18 +01:00
Fredrik Fornwall
09ecd14764 Fix crash when using some unicode characters
We should never try to look at characters in a row after those
actually used.
2015-11-13 00:14:35 +01:00
Fredrik Fornwall
89912be500 Fix crash on installation if activity destroyed
If the activity had been destroyed waiting for the installation to
finish the code would crash trying to display welcome dialogs (on
success) or error dialogs (on error).
2015-11-13 00:10:21 +01:00
Fredrik Fornwall
3b4e3b0e42 Update to Android Gradle Plugin version 1.5.0 2015-11-12 22:16:09 +01:00
Fredrik Fornwall
7a726c035c Clarify Apache License version (fixes #10) 2015-11-07 23:09:52 +01:00
Fredrik Fornwall
430a98e9ad Merge pull request #11 from RyDroid/minor
Minor changes in README and adding an EditorConfig file
2015-11-07 23:06:50 +01:00
Nicola Spanti (RyDroid)
01de6b4d18 Minor changes in README and adding an EditorConfig file 2015-11-07 19:39:13 +01:00
Fredrik Fornwall
1652c1dcf3 Update gradle wrapper to 2.8 2015-11-06 00:05:32 +01:00
Fredrik Fornwall
7db3200c13 Add a file picker to pick files from $HOME
This allows e.g. files from the $HOME folder to be attached to emails
or uploaded when in other apps.

Also bump version to 0.18.
2015-11-05 23:56:51 +01:00
Fredrik Fornwall
9e7ca8b689 Make build-jnilibs.sh always clear directories 2015-10-25 22:35:42 +01:00
Fredrik Fornwall
cb42c19d76 Remove <uses-sdk/> element in favour of gradle 2015-10-25 21:58:09 +01:00
60 changed files with 1819 additions and 981 deletions

16
.editorconfig Normal file
View File

@@ -0,0 +1,16 @@
# Copying and distribution of this file, with or without modification,
# are permitted in any medium without royalty provided this notice is
# preserved. This file is offered as-is, without any warranty.
# EditorConfig
# http://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
indent_style = space
indent_size = 4

3
.gitignore vendored
View File

@@ -4,6 +4,8 @@
# Built application files
build/
*.apk
*.so
# Crashlytics configuations
com_crashlytics_export_strings.xml
@@ -29,6 +31,7 @@ local.properties
.idea/modules.xml
.idea/scopes/scope_settings.xml
.idea/vcs.xml
.idea/dictionaries/
*.iml
# OS-specific files

7
.idea/gradle.xml generated
View File

@@ -5,13 +5,18 @@
<GradleProjectSettings>
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="1.8" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="myModules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>

View File

@@ -11,9 +11,9 @@ android:
components:
- platform-tools
- tools
- build-tools-23.0.1
- build-tools-23.0.2
- android-23
- sys-img-x86-android-23
- extra-android-m2repository
script:
- ./gradlew testDebugUnitTest

View File

@@ -1,11 +1,13 @@
Termux app
==========
[![Travis build status](https://travis-ci.org/termux/termux-app.svg?branch=master)](https://travis-ci.org/termux/termux-app)
[![Join the chat at https://gitter.im/termux/termux](https://badges.gitter.im/termux/termux.svg)](https://gitter.im/termux/termux)
Termux is an Android terminal app and Linux environment.
* [Termux on Google Play](http://play.google.com/store/apps/details?id=com.termux)
* [Termux on Google Play Store](https://play.google.com/store/apps/details?id=com.termux)
* [Termux on F-Droid](https://f-droid.org/repository/browse/?fdid=com.termux)
* [termux.com](http://termux.com)
* [Termux Help](http://termux.com/help/)
* [Termux app on GitHub](https://github.com/termux/termux-app)
@@ -14,11 +16,11 @@ Termux is an Android terminal app and Linux environment.
License
=======
Released under the GPLv3 license. Contains code from `Terminal Emulator for Android` which is released under the Apache License.
Released under [the GPLv3 license](https://www.gnu.org/licenses/gpl.html). Contains code from `Terminal Emulator for Android` which is released under [the Apache License 2.0](https://www.apache.org/licenses/).
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
==================
@@ -29,9 +31,9 @@ Terminal resources
Terminal emulators
==================
* VTE (libvte): Terminal emulator widget for GTK+, mainly used in gnome-terminal. [Source](https://github.com/GNOME/vte), [Open Issues](https://bugzilla.gnome.org/buglist.cgi?quicksearch=product%3A%22vte%22+), and [All (including closed) issues](https://bugzilla.gnome.org/buglist.cgi?bug_status=RESOLVED&bug_status=VERIFIED&chfield=resolution&chfieldfrom=-2000d&chfieldvalue=FIXED&product=vte&resolution=FIXED).
* iTerm 2: Mac terminal application. [Source](https://github.com/gnachman/iTerm2), [Issues](https://gitlab.com/gnachman/iterm2/issues) and [Documentation](http://www.iterm2.com/documentation.html) (which includes [iTerm2 proprietary escape codes](http://www.iterm2.com/documentation-escape-codes.html)).
* iTerm 2: OS X terminal application. [Source](https://github.com/gnachman/iTerm2), [Issues](https://gitlab.com/gnachman/iterm2/issues) and [Documentation](http://www.iterm2.com/documentation.html) (which includes [iTerm2 proprietary escape codes](http://www.iterm2.com/documentation-escape-codes.html)).
* Konsole: KDE terminal application. [Source](https://projects.kde.org/projects/kde/applications/konsole/repository), in particular [tests](https://projects.kde.org/projects/kde/applications/konsole/repository/revisions/master/show/tests), [Bugs](https://bugs.kde.org/buglist.cgi?bug_severity=critical&bug_severity=grave&bug_severity=major&bug_severity=crash&bug_severity=normal&bug_severity=minor&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&product=konsole) and [Wishes](https://bugs.kde.org/buglist.cgi?bug_severity=wishlist&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&product=konsole).
* hterm: Javascript terminal implementation from chromium. [Source](https://github.com/chromium/hterm), including [tests](https://github.com/chromium/hterm/blob/master/js/hterm_vt_tests.js), and [google group](https://groups.google.com/a/chromium.org/forum/#!forum/chromium-hterm).
* hterm: JavaScript terminal implementation from Chromium. [Source](https://github.com/chromium/hterm), including [tests](https://github.com/chromium/hterm/blob/master/js/hterm_vt_tests.js), and [Google group](https://groups.google.com/a/chromium.org/forum/#!forum/chromium-hterm).
* xterm: The grandfather of terminal emulators. [Source](http://invisible-island.net/datafiles/release/xterm.tar.gz).
* Connectbot: Android SSH client. [Source](https://github.com/connectbot/connectbot)
* Android Terminal Emulator: Android terminal app which Termux terminal handling is based on. Inactive. [Source](https://github.com/jackpal/Android-Terminal-Emulator).

View File

@@ -2,43 +2,30 @@ apply plugin: 'com.android.application'
android {
compileSdkVersion 23
buildToolsVersion "23.0.1"
buildToolsVersion "23.0.2"
dependencies {
compile 'com.android.support:support-annotations:23.3.0'
}
sourceSets {
main {
jni.srcDirs = []
}
main {
jni.srcDirs = []
}
}
defaultConfig {
applicationId "com.termux"
minSdkVersion 21
targetSdkVersion 22
versionCode 17
versionName "0.17"
}
signingConfigs {
release {
if (System.getenv("TRAVIS")) {
storeFile rootProject.file('travis.keystore')
storePassword 'abcdef'
keyAlias 'travis'
keyPassword 'abcdef'
} else {
storeFile new File(TERMUX_KEYSTORE_FILE)
storePassword TERMUX_KEYSTORE_PASSWORD
keyAlias TERMUX_KEYSTORE_ALIAS
keyPassword TERMUX_KEYSTORE_PASSWORD
}
}
targetSdkVersion 23
versionCode 34
versionName "0.34"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
}
}

View File

@@ -4,10 +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-sdk android:minSdkVersion="21" android:targetSdkVersion="22" />
<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" />
@@ -16,11 +14,13 @@
<application
android:allowBackup="true"
android:fullBackupContent="@xml/backupscheme"
android:icon="@drawable/ic_launcher"
android:banner="@drawable/banner"
android:label="@string/application_name"
android:theme="@style/Theme.Termux"
android:supportsRtl="false" >
<activity
android:name="com.termux.app.TermuxActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
@@ -30,18 +30,64 @@
<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">
<!-- Accept multiple file types when sending. -->
<intent-filter>
<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>
<!-- Be more restrictive for viewing files, restricting ourselves to text files. -->
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/*" />
<data android:mimeType="application/json" />
<data android:mimeType="application/*xml*" />
<data android:mimeType="application/*latex*" />
<data android:mimeType="application/javascript" />
</intent-filter>
</activity>
<provider
android:name=".filepicker.TermuxDocumentsProvider"
android:authorities="com.termux.documents"
android:grantUriPermissions="true"
android:exported="true"
android:permission="android.permission.MANAGE_DOCUMENTS">
<intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
</intent-filter>
</provider>
<service
android:name="com.termux.app.TermuxService"
android:exported="false" />
</application>
</manifest>

View File

@@ -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 &lt;query&gt;</dt><dd>Search among available packages.</dd>
<dt>apt install &lt;package&gt;</dt><dd>Install a new package.</dd>
<dt>apt upgrade</dt><dd>Upgrade outdated packages. For Apt to know about newer packages you will need to update the package index, so you will normally want to run <em>apt update</em> before upgrading.</dd>
<dt>apt show &lt;package&gt;</dt><dd>Show information about a package.</dd>
<dt>apt list</dt><dd>List all available packages.</dd>
<dt>apt list --installed</dt><dd>List all installed packages.</dd>
<dt>apt remove &lt;package&gt;</dt><dd>Remove an installed package.</dd>
</dl>
<p>Apt as a package manager uses a package format named <em>dpkg</em>. Normally direct use of dpkg is not necessary, but the
following two commands may be of use:</p>
<dl>
<dt>dpkg -L &lt;package&gt;</dt>
<dd>List installed files of a package.</dd>
<dt>dpkg --verify</dt>
<dd>Verify the integrity of installed packages.</dd>
</dl>
<p>View the apt manual page (execute <em>apt install man</em> to install a man page viewer first) for more information.</p>
<h2 id="text_editing">Text editing</h2>
<p>By default the busybox version of <em>vi</em> is available. This is a barebone and somewhat unfriendly editor -
install <a href="http://www.nano-editor.org/dist/v2.2/nano.html">nano</a> for a more straight-forward editor and
<a href="http://vimdoc.sourceforge.net/htmldoc/usr_toc.html">vim</a> for a more powerful one.</p>
<h2 id="using_ssh">Using SSH</h2>
<p>By installing the <strong>openssh</strong> package (by executing <em>apt install openssh</em>) you may SSH into remote systems,
optionally putting private keys or configuration under $HOME/.ssh/.</p>
<p>If you wish to use an SSH agent to avoid entering passwords, the Termux openssh package provides
a wrapper script named <strong>ssha</strong> (note the 'a' at the end) for ssh which:</p>
<ol>
<li>Starts the ssh agent if necessary (or connect to it if already running).</li>
<li>Runs <strong>ssh-add</strong> if necessary.</li>
<li>Runs <strong>ssh</strong> with the provided arguments.</li>
</ol>
<p>This means that the agent will prompt for a key password at first run, but remember the authorization for subsequent ones.</p>
<h2 id="interactive_shells">Interactive shells</h2>
<p>The base system that is installed when first starting Termux uses the <em>bash</em> shell while zsh is available as
an installable alternative:</p>
<ul>
<li>bash - the default shell on most Linux distributions, with resources such as
<a href="http://www.tldp.org/LDP/Bash-Beginners-Guide/html/">Bash Guide for Beginners</a>,
the <a href="https://www.gnu.org/software/bash/manual/bash.html">Bash Reference Manual</a>
or the <a href="http://www.tldp.org/LDP/abs/html/">Advanced Bash-Scripting Guide</a> available.</li>
<li>zsh - a powerful shell with information available at
<a href="http://zsh.sourceforge.net/Guide/zshguide.html">A User's Guide to the Z-Shell</a>, the
<a href="http://zsh.sourceforge.net/Doc/Release/zsh_toc.html">Z Shell Manual</a> or
<a href="http://www.rayninfo.co.uk/tips/zshtips.html">ZSH Tips by ZZapper</a>.
After installing zsh through <em>apt install zsh</em>, execute <em>chsh -s zsh</em> to set it as the default login shell when starting Termux
(and change back with <em>chsh -s bash</em> if necessary).</li>
</ul>
<h2 id="termux_android">Termux and Android</h2>
<p>Termux is designed to cope with the restrictions of running as an ordinary Android app without requiring root, which
leads to several differences between Termux and a traditional desktop system. The file system layout is drastically different:</p>
<ul>
<li>Common folders such as /bin, /usr/, /var and /etc does not exist.</li>
<li>The Android system provides a basic non-standard file system hierarchy, where e.g. /system/bin contains some system binaries.</li>
<li>The user folder $HOME is inside the private file area exposed to Termux as an ordinary Android app.
Uninstalling Termux will cause this file area to be wiped - so save important files outside this area such as in /sdcard
or use a version control system such as <em>git</em>.</li>
<li>Termux installs its packages in a folder exposed through the $PREFIX environment variable (with e.g. binaries in $PREFIX/bin,
and configuration in $PREFIX/etc).</li>
<li>Shared libraries are installed in $PREFIX/lib, which are available from binaries due to Termux setting the $LD_LIBRARY_PATH
environment variable. These may clash with Android system binaries in /system/bin, which may force LD_LIBRARY_PATH to be
cleared before running system binaries.</li>
</ul>
<p>Besides the file system being different, Termux is running as a single-user system without root - each Android app is running as
its own Linux user, so running commands inside Termux may not interfere with other installed applications.</p>
<p>Running as non-root implies that ports below 1024 cannot be bound to. Many packages have been configured to have compatible
default values - the ftpd, httpd, and sshd servers default to 8021, 8080 and 8022, respectively.</p>
<h2 id="add_on_api">Add-on: API</h2>
<p>The API add-on exposes Android system functionality such as SMS messages, GPS location or the Text-to-speech functionality through command line tools.</p>
<ul><li><a href="http://play.google.com/store/apps/details?id=com.termux.api">See more and install from Google Play</a></li></ul>
<h2 id="add_on_float">Add-on: Float</h2>
<p>The Float add-on consists of a floating terminal window visible while running other apps.</p>
<ul><li><a href="http://play.google.com/store/apps/details?id=com.termux.window">See more and install from Google Play</a></li></ul>
<h2 id="add_on_styling">Add-on: Styling</h2>
<p>The Styling add-on provides color schemes and fonts to beabeautify and customize the appearance of the Termux terminal.</p>
<ul><li><a href="http://play.google.com/store/apps/details?id=com.termux.styling">See more and install from Google Play</a></li></ul>
<h2 id="add_on_widget">Add-on: Widget</h2>
<p>The Widget add-on brings a widget to your homescreen, providing links to run scripts in your $HOME/.shortcuts/ folder.</p>
<ul><li><a href="http://play.google.com/store/apps/details?id=com.termux.widget">See more and install from Google Play</a></li></ul>
<h2 id="source_and_licenses">Source and licenses</h2>
<p>Termux uses terminal emulation code from <a href="https://github.com/jackpal/Android-Terminal-Emulator">Terminal Emulator for Android</a>
which is under the <a href="https://raw.githubusercontent.com/jackpal/Android-Terminal-Emulator/master/NOTICE">Apache License, Version 2.0</a>.
Packages available through Termux are distributed under their respective licenses with scripts and patches used to build them
<a href="https://github.com/termux/termux-packages">available on github</a>.</p>
</div>
</body>
</html>

View File

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

View File

@@ -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,14 +16,19 @@ import android.content.DialogInterface.OnShowListener;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.res.Resources;
import android.content.pm.PackageManager;
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;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextUtils;
@@ -52,6 +46,7 @@ import android.view.View.OnClickListener;
import android.view.View.OnKeyListener;
import android.view.View.OnLongClickListener;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
@@ -61,6 +56,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.
*
@@ -73,7 +81,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;
@@ -83,10 +92,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. */
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);
@@ -111,17 +122,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);
@@ -142,6 +178,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
if (event.getAction() != KeyEvent.ACTION_DOWN) return false;
final TerminalSession currentSession = getCurrentTermSession();
if (currentSession == null) return false;
if (keyCode == KeyEvent.KEYCODE_ENTER && !currentSession.isRunning()) {
// Return pressed with finished session - remove it.
@@ -181,6 +218,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 */) {
@@ -219,59 +259,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);
@@ -287,8 +322,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);
}
/**
@@ -338,14 +374,27 @@ 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;
case TermuxPreferences.BELL_IGNORE:
// Ignore the bell character.
break;
}
}
}
};
@@ -414,18 +463,23 @@ public final class TermuxActivity extends Activity implements ServiceConnection
TermuxInstaller.setupIfNeeded(TermuxActivity.this, new Runnable() {
@Override
public void run() {
if (TermuxPreferences.isShowWelcomeDialog(TermuxActivity.this)) {
new AlertDialog.Builder(TermuxActivity.this).setTitle(R.string.welcome_dialog_title).setMessage(R.string.welcome_dialog_body)
.setCancelable(false).setPositiveButton(android.R.string.ok, null)
.setNegativeButton(R.string.welcome_dialog_dont_show_again_button, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
TermuxPreferences.disableWelcomeDialog(TermuxActivity.this);
dialog.dismiss();
}
}).show();
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)
.setCancelable(false).setPositiveButton(android.R.string.ok, null)
.setNegativeButton(R.string.welcome_dialog_dont_show_again_button, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
TermuxPreferences.disableWelcomeDialog(TermuxActivity.this);
dialog.dismiss();
}
}).show();
}
addNewSession(false, null);
} catch (WindowManager.BadTokenException e) {
// Activity finished - ignore.
}
addNewSession(false, null);
}
});
} else {
@@ -439,14 +493,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) {
@@ -456,7 +509,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
}
}
TerminalSession getCurrentTermSession() {
@Nullable TerminalSession getCurrentTermSession() {
return mTerminalView.getCurrentSession();
}
@@ -472,6 +525,10 @@ public final class TermuxActivity extends Activity implements ServiceConnection
}
registerReceiver(mBroadcastReceiever, new IntentFilter(RELOAD_STYLE_ACTION));
// The current terminal session may have changed while being away, force
// a refresh of the displayed terminal:
mTerminalView.onScreenUpdated();
}
@Override
@@ -558,9 +615,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
TerminalSession currentSession = getCurrentTermSession();
if (currentSession == null) return;
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
menu.add(Menu.NONE, CONTEXTMENU_PASTE_ID, Menu.NONE, R.string.paste_text).setEnabled(clipboard.hasPrimaryClip());
menu.add(Menu.NONE, CONTEXTMENU_SELECT_ID, Menu.NONE, R.string.select);
menu.add(Menu.NONE, CONTEXTMENU_SELECT_URL_ID, Menu.NONE, R.string.select_url);
menu.add(Menu.NONE, CONTEXTMENU_SHARE_TRANSCRIPT_ID, Menu.NONE, R.string.select_all_and_share);
menu.add(Menu.NONE, CONTEXTMENU_RESET_TERMINAL_ID, Menu.NONE, R.string.reset_terminal);
menu.add(Menu.NONE, CONTEXTMENU_KILL_PROCESS_ID, Menu.NONE, R.string.kill_process).setEnabled(currentSession.isRunning());
menu.add(Menu.NONE, CONTEXTMENU_TOGGLE_FULLSCREEN_ID, Menu.NONE, R.string.toggle_fullscreen).setCheckable(true).setChecked(mSettings.isFullScreen());
@@ -575,12 +631,11 @@ public final class TermuxActivity extends Activity implements ServiceConnection
return false;
}
void showUrlSelection() {
String text = getCurrentTermSession().getEmulator().getScreen().getTranscriptText();
static LinkedHashSet<CharSequence> extractUrls(String text) {
// Pattern for recognizing a URL, based off RFC 3986
// http://stackoverflow.com/questions/5713558/detect-and-extract-url-from-a-string
final Pattern urlPattern = Pattern.compile(
"(?:^|[\\W])((ht|f)tp(s?):\\/\\/|www\\.)" + "(([\\w\\-]+\\.){1,}?([\\w\\-.~]+\\/?)*" + "[\\p{Alnum}.,%_=?&#\\-+()\\[\\]\\*$~@!:/{};']*)",
"(?:^|[\\W])((ht|f)tp(s?)://|www\\.)" + "(([\\w\\-]+\\.)+?([\\w\\-.~]+/?)*" + "[\\p{Alnum}.,%_=?&#\\-+()\\[\\]\\*$~@!:/{};']*)",
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
LinkedHashSet<CharSequence> urlSet = new LinkedHashSet<>();
Matcher matcher = urlPattern.matcher(text);
@@ -590,7 +645,12 @@ public final class TermuxActivity extends Activity implements ServiceConnection
String url = text.substring(matchStart, matchEnd);
urlSet.add(url);
}
return urlSet;
}
void showUrlSelection() {
String text = getCurrentTermSession().getEmulator().getScreen().getTranscriptText();
LinkedHashSet<CharSequence> urlSet = extractUrls(text);
if (urlSet.isEmpty()) {
new AlertDialog.Builder(this).setMessage(R.string.select_url_no_found).show();
return;
@@ -620,7 +680,13 @@ public final class TermuxActivity extends Activity implements ServiceConnection
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
dialog.dismiss();
String url = (String) urls[position];
startActivity(Intent.createChooser(new Intent(Intent.ACTION_VIEW, Uri.parse(url)), null));
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
try {
startActivity(i, null);
} catch (ActivityNotFoundException e) {
// If no applications match, Android displays a system message.
startActivity(Intent.createChooser(i, null));
}
return true;
}
});
@@ -632,84 +698,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);
}
}

View File

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

View File

@@ -1,16 +1,5 @@
package com.termux.app;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
@@ -18,13 +7,28 @@ import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.DialogInterface.OnDismissListener;
import android.os.Build;
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;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/**
* Install the Termux bootstrap packages if necessary by following the below steps:
*
@@ -138,27 +142,35 @@ final class TermuxInstaller {
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_body)
.setNegativeButton(R.string.bootstrap_error_abort, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
activity.finish();
}
}).setPositiveButton(R.string.bootstrap_error_try_again, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
TermuxInstaller.setupIfNeeded(activity, whenDone);
}
}).show();
try {
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_body)
.setNegativeButton(R.string.bootstrap_error_abort, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
activity.finish();
}
}).setPositiveButton(R.string.bootstrap_error_try_again, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
TermuxInstaller.setupIfNeeded(activity, whenDone);
}
}).show();
} catch (WindowManager.BadTokenException e) {
// Activity already dismissed - ignore.
}
}
});
} finally {
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
progress.dismiss();
try {
progress.dismiss();
} catch (RuntimeException e) {
// Activity already dismissed - ignore.
}
}
});
}
@@ -166,17 +178,28 @@ 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 termuxArch = null;
// Note that we cannot use System.getProperty("os.arch") since that may give e.g. "aarch64"
// while a 64-bit runtime may not be installed (like on the Samsung Galaxy S5 Neo).
// Instead we search through the supported abi:s on the device, see:
// http://developer.android.com/ndk/guides/abis.html
// Note that we search for abi:s in preferred order, and want to avoid installing arm on
// an x86 system where arm emulation is available.
final String[] androidArchNames = {"arm64-v8a", "x86", "armeabi-v7a"};
final String[] termuxArchNames = {"aarch64", "i686", "arm"};
final List<String> supportedArches = Arrays.asList(Build.SUPPORTED_ABIS);
for (int i = 0; i < termuxArchNames.length; i++) {
if (supportedArches.contains(androidArchNames[i])) {
termuxArch = termuxArchNames[i];
break;
}
}
return new URL("https://termux.net/bootstrap/bootstrap-" + termuxArch + ".zip");
}
/** Delete a folder and all its content or throw. */
static void deleteFolder(File fileOrDirectory) {
@@ -191,4 +214,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();
}
}

View File

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

View File

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

View File

@@ -16,8 +16,6 @@ package com.termux.drawer;
* limitations under the License.
*/
import java.util.List;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
@@ -27,8 +25,6 @@ import android.graphics.PixelFormat;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.SystemClock;
import android.util.AttributeSet;
import android.view.Gravity;
@@ -39,6 +35,8 @@ import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import java.util.List;
/**
* DrawerLayout acts as a top-level container for window content that allows for interactive "drawer" views to be pulled
* out from the edge of the window.
@@ -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;
@@ -768,16 +766,10 @@ public class DrawerLayout extends ViewGroup {
// or pick a magic number from thin air otherwise.
// TODO Better communication with tools of this bogus state.
// It will crash on a real device.
if (widthMode == MeasureSpec.AT_MOST) {
widthMode = MeasureSpec.EXACTLY;
} else if (widthMode == MeasureSpec.UNSPECIFIED) {
widthMode = MeasureSpec.EXACTLY;
if (widthMode == MeasureSpec.UNSPECIFIED) {
widthSize = 300;
}
if (heightMode == MeasureSpec.AT_MOST) {
heightMode = MeasureSpec.EXACTLY;
} else if (heightMode == MeasureSpec.UNSPECIFIED) {
heightMode = MeasureSpec.EXACTLY;
if (heightMode == MeasureSpec.UNSPECIFIED) {
heightSize = 300;
}
} else {
@@ -934,10 +926,7 @@ public class DrawerLayout extends ViewGroup {
private static boolean hasOpaqueBackground(View v) {
final Drawable bg = v.getBackground();
if (bg != null) {
return bg.getOpacity() == PixelFormat.OPAQUE;
}
return false;
return bg != null && bg.getOpacity() == PixelFormat.OPAQUE;
}
/**
@@ -1114,7 +1103,6 @@ public class DrawerLayout extends ViewGroup {
mRightDragger.processTouchEvent(ev);
final int action = ev.getAction();
boolean wantTouchEvents = true;
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN: {
@@ -1154,7 +1142,7 @@ public class DrawerLayout extends ViewGroup {
}
}
return wantTouchEvents;
return true;
}
@Override
@@ -1317,10 +1305,7 @@ public class DrawerLayout extends ViewGroup {
*/
public boolean isDrawerOpen(int drawerGravity) {
final View drawerView = findDrawerWithGravity(drawerGravity);
if (drawerView != null) {
return isDrawerOpen(drawerView);
}
return false;
return drawerView != null && isDrawerOpen(drawerView);
}
/**
@@ -1350,10 +1335,7 @@ public class DrawerLayout extends ViewGroup {
*/
public boolean isDrawerVisible(int drawerGravity) {
final View drawerView = findDrawerWithGravity(drawerGravity);
if (drawerView != null) {
return isDrawerVisible(drawerView);
}
return false;
return drawerView != null && isDrawerVisible(drawerView);
}
private boolean hasPeekingDrawer() {
@@ -1438,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);
@@ -1502,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;
@@ -1776,10 +1702,7 @@ public class DrawerLayout extends ViewGroup {
@Override
public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, AccessibilityEvent event) {
if (CAN_HIDE_DESCENDANTS || includeChildForAccessibility(child)) {
return super.onRequestSendAccessibilityEvent(host, child, event);
}
return false;
return (CAN_HIDE_DESCENDANTS || includeChildForAccessibility(child)) && super.onRequestSendAccessibilityEvent(host, child, event);
}
}
@@ -1796,4 +1719,4 @@ public class DrawerLayout extends ViewGroup {
}
}
}
}
}

View File

@@ -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
@@ -1485,10 +1485,7 @@ public class ViewDragHelper {
* @return true if the supplied view is under the given point, false otherwise
*/
public boolean isViewUnder(View view, int x, int y) {
if (view == null) {
return false;
}
return x >= view.getLeft() && x < view.getRight() && y >= view.getTop() && y < view.getBottom();
return view != null && x >= view.getLeft() && x < view.getRight() && y >= view.getTop() && y < view.getBottom();
}
/**
@@ -1522,4 +1519,4 @@ public class ViewDragHelper {
return result;
}
}
}

View File

@@ -0,0 +1,242 @@
package com.termux.filepicker;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.graphics.Point;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsContract.Root;
import android.provider.DocumentsProvider;
import android.webkit.MimeTypeMap;
import com.termux.R;
import com.termux.app.TermuxService;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedList;
/**
* A document provider for the Storage Access Framework which exposes the files in the
* $HOME/ folder to other apps.
* <p>
* Note that this replaces providing an activity matching the ACTION_GET_CONTENT intent:
* <p>
* "A document provider and ACTION_GET_CONTENT should be considered mutually exclusive. If you
* support both of them simultaneously, your app will appear twice in the system picker UI,
* offering two different ways of accessing your stored data. This would be confusing for users."
* - http://developer.android.com/guide/topics/providers/document-provider.html#43
*/
public class TermuxDocumentsProvider extends DocumentsProvider {
private static final String ALL_MIME_TYPES = "*/*";
private static final File BASE_DIR = new File(TermuxService.HOME_PATH);
// The default columns to return information about a root if no specific
// columns are requested in a query.
private static final String[] DEFAULT_ROOT_PROJECTION = new String[]{
Root.COLUMN_ROOT_ID,
Root.COLUMN_MIME_TYPES,
Root.COLUMN_FLAGS,
Root.COLUMN_ICON,
Root.COLUMN_TITLE,
Root.COLUMN_SUMMARY,
Root.COLUMN_DOCUMENT_ID,
Root.COLUMN_AVAILABLE_BYTES
};
// The default columns to return information about a document if no specific
// columns are requested in a query.
private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[]{
Document.COLUMN_DOCUMENT_ID,
Document.COLUMN_MIME_TYPE,
Document.COLUMN_DISPLAY_NAME,
Document.COLUMN_LAST_MODIFIED,
Document.COLUMN_FLAGS,
Document.COLUMN_SIZE
};
@Override
public Cursor queryRoots(String[] projection) throws FileNotFoundException {
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_ROOT_PROJECTION);
@SuppressWarnings("ConstantConditions") final String applicationName = getContext().getString(R.string.application_name);
final MatrixCursor.RowBuilder row = result.newRow();
row.add(Root.COLUMN_ROOT_ID, getDocIdForFile(BASE_DIR));
row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(BASE_DIR));
row.add(Root.COLUMN_SUMMARY, null);
row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_SEARCH);
row.add(Root.COLUMN_TITLE, applicationName);
row.add(Root.COLUMN_MIME_TYPES, ALL_MIME_TYPES);
row.add(Root.COLUMN_AVAILABLE_BYTES, BASE_DIR.getFreeSpace());
row.add(Root.COLUMN_ICON, R.drawable.ic_launcher);
return result;
}
@Override
public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException {
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
includeFile(result, documentId, null);
return result;
}
@Override
public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException {
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
final File parent = getFileForDocId(parentDocumentId);
for (File file : parent.listFiles()) {
if (!file.getName().startsWith(".")) {
includeFile(result, null, file);
}
}
return result;
}
@Override
public ParcelFileDescriptor openDocument(final String documentId, String mode, CancellationSignal signal) throws FileNotFoundException {
final File file = getFileForDocId(documentId);
final int accessMode = ParcelFileDescriptor.parseMode(mode);
return ParcelFileDescriptor.open(file, accessMode);
}
@Override
public AssetFileDescriptor openDocumentThumbnail(String documentId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException {
final File file = getFileForDocId(documentId);
final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
return new AssetFileDescriptor(pfd, 0, file.length());
}
@Override
public boolean onCreate() {
return true;
}
@Override
public void deleteDocument(String documentId) throws FileNotFoundException {
File file = getFileForDocId(documentId);
if (!file.delete()) {
throw new FileNotFoundException("Failed to delete document with id " + documentId);
}
}
@Override
public String getDocumentType(String documentId) throws FileNotFoundException {
File file = getFileForDocId(documentId);
return getMimeType(file);
}
@Override
public Cursor querySearchDocuments(String rootId, String query, String[] projection) throws FileNotFoundException {
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
final File parent = getFileForDocId(rootId);
// This example implementation searches file names for the query and doesn't rank search
// results, so we can stop as soon as we find a sufficient number of matches. Other
// implementations might rank results and use other data about files, rather than the file
// name, to produce a match.
final LinkedList<File> pending = new LinkedList<>();
pending.add(parent);
final int MAX_SEARCH_RESULTS = 50;
while (!pending.isEmpty() && result.getCount() < MAX_SEARCH_RESULTS) {
final File file = pending.removeFirst();
// Avoid folders outside the $HOME folders linked in to symlinks (to avoid e.g. search
// through the whole SD card).
boolean isInsideHome;
try {
isInsideHome = file.getCanonicalPath().startsWith(TermuxService.HOME_PATH);
} catch (IOException e) {
isInsideHome = true;
}
final boolean isHidden = file.getName().startsWith(".");
if (isInsideHome && !isHidden) {
if (file.isDirectory()) {
Collections.addAll(pending, file.listFiles());
} else {
if (file.getName().toLowerCase().contains(query)) {
includeFile(result, null, file);
}
}
}
}
return result;
}
/**
* Get the document id given a file. This document id must be consistent across time as other
* applications may save the ID and use it to reference documents later.
* <p>
* The reverse of @{link #getFileForDocId}.
*/
private static String getDocIdForFile(File file) {
return file.getAbsolutePath();
}
/**
* Get the file given a document id (the reverse of {@link #getDocIdForFile(File)}).
*/
private static File getFileForDocId(String docId) throws FileNotFoundException {
final File f = new File(docId);
if (!f.exists()) throw new FileNotFoundException(f.getAbsolutePath() + " not found");
return f;
}
private static String getMimeType(File file) {
if (file.isDirectory()) {
return Document.MIME_TYPE_DIR;
} else {
final String name = file.getName();
final int lastDot = name.lastIndexOf('.');
if (lastDot >= 0) {
final String extension = name.substring(lastDot + 1);
final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
if (mime != null) return mime;
}
return "application/octet-stream";
}
}
/**
* Add a representation of a file to a cursor.
*
* @param result the cursor to modify
* @param docId the document ID representing the desired file (may be null if given file)
* @param file the File object representing the desired file (may be null if given docID)
*/
private void includeFile(MatrixCursor result, String docId, File file)
throws FileNotFoundException {
if (docId == null) {
docId = getDocIdForFile(file);
} else {
file = getFileForDocId(docId);
}
int flags = 0;
if (file.isDirectory()) {
if (file.isDirectory() && file.canWrite()) flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
} else if (file.canWrite()) {
flags |= Document.FLAG_SUPPORTS_WRITE | Document.FLAG_SUPPORTS_DELETE;
}
final String displayName = file.getName();
final String mimeType = getMimeType(file);
if (mimeType.startsWith("image/")) flags |= Document.FLAG_SUPPORTS_THUMBNAIL;
final MatrixCursor.RowBuilder row = result.newRow();
row.add(Document.COLUMN_DOCUMENT_ID, docId);
row.add(Document.COLUMN_DISPLAY_NAME, displayName);
row.add(Document.COLUMN_SIZE, file.length());
row.add(Document.COLUMN_MIME_TYPE, mimeType);
row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified());
row.add(Document.COLUMN_FLAGS, flags);
row.add(Document.COLUMN_ICON, R.drawable.ic_launcher);
}
}

View File

@@ -0,0 +1,217 @@
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, 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:
//noinspection ResultOfMethodCallIgnored
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();
}
},
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();
}
},
android.R.string.cancel, new DialogUtils.TextSetListener() {
@Override
public void onTextSet(final String text) {
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:
//noinspection ResultOfMethodCallIgnored
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();
}
}

View File

@@ -28,7 +28,7 @@ final class JNI {
* @return the file descriptor resulting from opening /dev/ptmx master device. The sub process will have opened the
* slave device counterpart (/dev/pts/$N) and have it as stdint, stdout and stderr.
*/
public static native int createSubprocess(String cmd, String cwd, String[] args, String[] envVars, int[] processId);
public static native int createSubprocess(String cmd, String cwd, String[] args, String[] envVars, int[] processId, int rows, int columns);
/** Set the window size for a given pty, which allows connected programs to learn how large their screen is. */
public static native void setPtyWindowSize(int fd, int rows, int cols);

View File

@@ -61,6 +61,10 @@ public final class TerminalBuffer {
TerminalRow lineObject = mLines[externalToInternalRow(row)];
int x1Index = lineObject.findStartOfColumn(x1);
int x2Index = (x2 < mColumns) ? lineObject.findStartOfColumn(x2) : lineObject.getSpaceUsed();
if (x2Index == x1Index) {
// Selected the start of a wide character.
x2Index = lineObject.findStartOfColumn(x2+1);
}
char[] line = lineObject.mText;
int lastPrintingCharIndex = -1;
int i;
@@ -71,7 +75,7 @@ public final class TerminalBuffer {
} else {
for (i = x1Index; i < x2Index; ++i) {
char c = line[i];
if (c != ' ' && !Character.isLowSurrogate(c)) lastPrintingCharIndex = i;
if (c != ' ') lastPrintingCharIndex = i;
}
}
if (lastPrintingCharIndex != -1) builder.append(line, x1Index, lastPrintingCharIndex - x1Index + 1);
@@ -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).

View File

@@ -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.
@@ -2156,15 +2181,22 @@ public final class TerminalEmulator {
final boolean autoWrap = isDecsetInternalBitSet(DECSET_BIT_AUTOWRAP);
final int displayWidth = WcWidth.width(codePoint);
final boolean cursorInLastColumn = mCursorCol == mRightMargin - 1;
if (autoWrap && (mCursorCol == mRightMargin - 1 && ((mAboutToAutoWrap && displayWidth == 1) || displayWidth == 2))) {
mScreen.setLineWrap(mCursorRow);
mCursorCol = mLeftMargin;
if (mCursorRow + 1 < mBottomMargin) {
mCursorRow++;
} else {
scrollDownOneLine();
if (autoWrap) {
if (cursorInLastColumn && ((mAboutToAutoWrap && displayWidth == 1) || displayWidth == 2)) {
mScreen.setLineWrap(mCursorRow);
mCursorCol = mLeftMargin;
if (mCursorRow + 1 < mBottomMargin) {
mCursorRow++;
} else {
scrollDownOneLine();
}
}
} else if (cursorInLastColumn && displayWidth == 2) {
// The behaviour when a wide character is output with cursor in the last column when
// autowrap is disabled is not obvious - it's ignored here.
return;
}
if (mInsertMode && displayWidth > 0) {

View File

@@ -184,6 +184,7 @@ public final class TerminalRow {
mSpaceUsed += javaCharDifference;
// Store char. A combining character is stored at the end of the existing contents so that it modifies them:
//noinspection ResultOfMethodCallIgnored - since we already now how many java chars is used.
Character.toChars(codePoint, text, oldStartOfColumnIndex + (newIsCombining ? oldCharactersUsedForColumn : 0));
if (oldCodePointDisplayWidth == 2 && newCodePointDisplayWidth == 1) {

View File

@@ -82,14 +82,17 @@ public final class TerminalSession extends TerminalOutput {
/** Callback which gets notified when a session finishes or changes title. */
final SessionChangedCallback mChangeCallback;
/** The pid of the shell process or -1 if not running. */
/** The pid of the shell process. 0 if not started and -1 if finished running. */
int mShellPid;
int mShellExitStatus = -1;
/** The exit status of the shell process. Only valid if ${@link #mShellPid} is -1. */
int mShellExitStatus;
/**
* The file descriptor referencing the master half of a pseudo-terminal pair, resulting from calling
* {@link JNI#createSubprocess(String, String, String[], String[], int[])}.
* {@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;
@@ -119,7 +122,7 @@ public final class TerminalSession extends TerminalOutput {
// Negated signal.
exitDescription += " with signal " + (-exitCode);
}
exitDescription += "]";
exitDescription += " - press Enter to close]";
byte[] bytesToWrite = exitDescription.getBytes(StandardCharsets.UTF_8);
mEmulator.append(bytesToWrite, bytesToWrite.length);
@@ -128,20 +131,26 @@ public final class TerminalSession extends TerminalOutput {
}
};
private final String mShellPath;
private final String mCwd;
private final String[] mArgs;
private final String[] mEnv;
public TerminalSession(String shellPath, String cwd, String[] args, String[] env, SessionChangedCallback changeCallback) {
mChangeCallback = changeCallback;
int[] processId = new int[1];
mTerminalFileDescriptor = JNI.createSubprocess(shellPath, cwd, args, env, processId);
mShellPid = processId[0];
this.mShellPath = shellPath;
this.mCwd = cwd;
this.mArgs = args;
this.mEnv = env;
}
/** Inform the attached pty of the new size and reflow or initialize the emulator. */
public void updateSize(int columns, int rows) {
JNI.setPtyWindowSize(mTerminalFileDescriptor, rows, columns);
if (mEmulator == null) {
initializeEmulator(columns, rows);
} else {
JNI.setPtyWindowSize(mTerminalFileDescriptor, rows, columns);
mEmulator.resize(columns, rows);
}
}
@@ -161,6 +170,11 @@ public final class TerminalSession extends TerminalOutput {
*/
public void initializeEmulator(int columns, int rows) {
mEmulator = new TerminalEmulator(this, columns, rows, /* transcript= */5000);
int[] processId = new int[1];
mTerminalFileDescriptor = JNI.createSubprocess(mShellPath, mCwd, mArgs, mEnv, processId, rows, columns);
mShellPid = processId[0];
final FileDescriptor terminalFileDescriptorWrapped = wrapFileDescriptor(mTerminalFileDescriptor);
new Thread("TermSessionInputReader[pid=" + mShellPid + "]") {
@@ -204,7 +218,7 @@ public final class TerminalSession extends TerminalOutput {
/** Write data to the shell process. */
@Override
public void write(byte[] data, int offset, int count) {
mTerminalToProcessIOQueue.write(data, offset, count);
if (mShellPid > 0) mTerminalToProcessIOQueue.write(data, offset, count);
}
/** Write the Unicode code point to the terminal encoded in UTF-8. */

View File

@@ -6,7 +6,7 @@ import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
/** A combination of {@link GestureDetector} and {@link ScaleGestureDetector}. */
public class GestureAndScaleRecognizer {
public final class GestureAndScaleRecognizer {
public interface Listener {
boolean onSingleTapUp(MotionEvent e);
@@ -29,6 +29,7 @@ public class GestureAndScaleRecognizer {
private final GestureDetector mGestureDetector;
private final ScaleGestureDetector mScaleDetector;
final Listener mListener;
boolean isAfterLongPress;
public GestureAndScaleRecognizer(Context context, Listener listener) {
mListener = listener;
@@ -52,6 +53,7 @@ public class GestureAndScaleRecognizer {
@Override
public void onLongPress(MotionEvent e) {
mListener.onLongPress(e);
isAfterLongPress = true;
}
}, null, true /* ignoreMultitouch */);
@@ -88,8 +90,17 @@ public class GestureAndScaleRecognizer {
public void onTouchEvent(MotionEvent event) {
mGestureDetector.onTouchEvent(event);
mScaleDetector.onTouchEvent(event);
if (event.getAction() == MotionEvent.ACTION_UP) {
mListener.onUp(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
isAfterLongPress = false;
break;
case MotionEvent.ACTION_UP:
if (!isAfterLongPress) {
// This behaviour is desired when in e.g. vim with mouse events, where we do not
// want to move the cursor when lifting finger after a long press.
mListener.onUp(event);
}
break;
}
}
@@ -97,4 +108,4 @@ public class GestureAndScaleRecognizer {
return mScaleDetector.isInProgress();
}
}
}

View File

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

View File

@@ -80,6 +80,7 @@ final class TerminalRenderer {
TerminalRow lineObject = screen.allocateFullLineIfNecessary(screen.externalToInternalRow(row));
final char[] line = lineObject.mText;
final int charsUsedInLine = lineObject.getSpaceUsed();
int lastRunStyle = 0;
boolean lastRunInsideCursor = false;
@@ -125,7 +126,7 @@ final class TerminalRenderer {
measuredWidthForRun += measuredCodePointWidth;
column += codePointWcWidth;
currentCharIndex += charsForCodePoint;
while (WcWidth.width(line, currentCharIndex) <= 0) {
while (currentCharIndex < charsUsedInLine && WcWidth.width(line, currentCharIndex) <= 0) {
// Eat combining chars so that they are treated as part of the last non-combining code point,
// instead of e.g. being considered inside the cursor in the next run.
currentCharIndex += Character.isHighSurrogate(line[currentCharIndex]) ? 2 : 1;
@@ -204,7 +205,10 @@ final class TerminalRenderer {
final boolean strikeThrough = (effect & TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH) != 0;
final boolean dim = (effect & TextStyle.CHARACTER_ATTRIBUTE_DIM) != 0;
int foreColorARGB = palette[foreColor];
// Let bold have bright colors if applicable (one of the first 8):
final int actualForeColor = foreColor + (bold && foreColor < 8 ? 8 : 0);
int foreColorARGB = palette[actualForeColor];
if (dim) {
int red = (0xFF & (foreColorARGB >> 16));
int green = (0xFF & (foreColorARGB >> 8));

View File

@@ -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);
}
@@ -598,61 +652,75 @@ public final class TerminalView extends View {
resultingKeyCode = KeyEvent.KEYCODE_F12;
}
} else if (mVirtualFnKeyDown) {
if (codePoint == 'w' || codePoint == 'W') {
resultingKeyCode = KeyEvent.KEYCODE_DPAD_UP;
} else if (codePoint == 'a' || codePoint == 'A') {
resultingKeyCode = KeyEvent.KEYCODE_DPAD_LEFT;
} else if (codePoint == 's' || codePoint == 'S') {
resultingKeyCode = KeyEvent.KEYCODE_DPAD_DOWN;
} else if (codePoint == 'd' || codePoint == 'D') {
resultingKeyCode = KeyEvent.KEYCODE_DPAD_RIGHT;
} else if (codePoint == 'p' || codePoint == 'P') {
resultingKeyCode = KeyEvent.KEYCODE_PAGE_UP;
} else if (codePoint == 'n' || codePoint == 'N') {
resultingKeyCode = KeyEvent.KEYCODE_PAGE_DOWN;
} else if (codePoint == 't' || codePoint == 'T') {
resultingKeyCode = KeyEvent.KEYCODE_TAB;
} else if (codePoint == 'l' || codePoint == 'L') {
codePoint = '|';
} else if (codePoint == 'u' || codePoint == 'U') {
codePoint = '_';
} else if (codePoint == 'e' || codePoint == 'E') {
codePoint = 27; // ^[ (Esc)
} else if (codePoint == '.') {
codePoint = 28; // ^\
} else if (codePoint > '0' && codePoint <= '9') {
// F1-F9
resultingKeyCode = (codePoint - '1') + KeyEvent.KEYCODE_F1;
} else if (codePoint == '0') {
resultingKeyCode = KeyEvent.KEYCODE_F10;
} else if (codePoint == 'i' || codePoint == 'I') {
resultingKeyCode = KeyEvent.KEYCODE_INSERT;
} else if (codePoint == 'x' || codePoint == 'X') {
resultingKeyCode = KeyEvent.KEYCODE_FORWARD_DEL;
} else if (codePoint == 'h' || codePoint == 'H') {
resultingKeyCode = KeyEvent.KEYCODE_MOVE_HOME;
} else if (codePoint == 'f' || codePoint == 'F') {
// As left alt+f, jumping forward in readline:
codePoint = 'f';
leftAltDownFromEvent = true;
} else if (codePoint == 'b' || codePoint == 'B') {
// As left alt+b, jumping forward in readline:
codePoint = 'b';
leftAltDownFromEvent = true;
}
int lowerCase = Character.toLowerCase(codePoint);
switch (lowerCase) {
// Arrow keys.
case 'w': resultingKeyCode = KeyEvent.KEYCODE_DPAD_UP; break;
case 'a': resultingKeyCode = KeyEvent.KEYCODE_DPAD_LEFT; break;
case 's': resultingKeyCode = KeyEvent.KEYCODE_DPAD_DOWN; break;
case 'd': resultingKeyCode = KeyEvent.KEYCODE_DPAD_RIGHT; break;
// Page up and down.
case 'p': resultingKeyCode = KeyEvent.KEYCODE_PAGE_UP; break;
case 'n': resultingKeyCode = KeyEvent.KEYCODE_PAGE_DOWN; break;
// Some special keys:
case 't': resultingKeyCode = KeyEvent.KEYCODE_TAB; break;
case 'i': resultingKeyCode = KeyEvent.KEYCODE_INSERT; break;
case 'h': resultingKeyCode = KeyEvent.KEYCODE_MOVE_HOME; break;
// Special characters to input.
case 'u': codePoint = '_'; break;
case 'l': codePoint = '|'; break;
// Function keys.
case '1': case '2': case '3':
case '4': case '5': case '6':
case '7': case '8': case '9':
resultingKeyCode = (codePoint - '1') + KeyEvent.KEYCODE_F1;
break;
case '0':
resultingKeyCode = KeyEvent.KEYCODE_F10;
break;
// Other special keys.
case 'e': codePoint = /*Escape*/ 27; break;
case '.': codePoint = /*^.*/ 28; break;
case 'b': // alt+b, jumping backward in readline.
case 'f': // alf+f, jumping forward in readline.
case 'x': // alt+x, common in emacs.
codePoint = lowerCase;
leftAltDownFromEvent = true;
break;
// Volume control.
case '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);
break;
}
}
if (codePoint > -1) {
if (resultingKeyCode > -1) {
handleKeyCode(resultingKeyCode, 0);
} else {
// The below two workarounds are needed on at least Logitech Keyboard k810 on Samsung Galaxy Tab Pro
// (Android 4.4) with the stock Samsung Keyboard. They should be harmless when not used since the need
// to input the original characters instead of the new ones using the keyboard should be low.
// Rewrite U+02DC 'SMALL TILDE' to U+007E 'TILDE' for ~ to work in shells:
if (codePoint == 0x02DC) codePoint = 0x07E;
// Rewrite U+02CB 'MODIFIER LETTER GRAVE ACCENT' to U+0060 'GRAVE ACCENT' for ` (backticks) to work:
if (codePoint == 0x02CB) codePoint = 0x60;
// 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 +780,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 +840,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() {

View File

@@ -1,4 +1,4 @@
APP_ABI := armeabi-v7a x86
APP_ABI := arm64-v8a armeabi-v7a x86
APP_PLATFORM := android-21
NDK_TOOLCHAIN_VERSION := 4.9
APP_CFLAGS := -std=c11 -Wall -Wextra -Os -fno-stack-protector

View File

@@ -22,7 +22,14 @@ static int throw_runtime_exception(JNIEnv* env, char const* message)
return -1;
}
static int create_subprocess(JNIEnv* env, char const* cmd, char const* cwd, char* const argv[], char** envp, int* pProcessId)
static int create_subprocess(JNIEnv* env,
char const* cmd,
char const* cwd,
char* const argv[],
char** envp,
int* pProcessId,
jint rows,
jint columns)
{
int ptm = open("/dev/ptmx", O_RDWR | O_CLOEXEC);
if (ptm < 0) return throw_runtime_exception(env, "Cannot open /dev/ptmx");
@@ -49,8 +56,8 @@ static int create_subprocess(JNIEnv* env, char const* cmd, char const* cwd, char
tios.c_iflag &= ~(IXON | IXOFF);
tcsetattr(ptm, TCSANOW, &tios);
/** Set initial winsize (better too small than too large). */
struct winsize sz = { .ws_row = 20, .ws_col = 20 };
/** Set initial winsize. */
struct winsize sz = { .ws_row = rows, .ws_col = columns };
ioctl(ptm, TIOCSWINSZ, &sz);
pid_t pid = fork();
@@ -105,7 +112,16 @@ static int create_subprocess(JNIEnv* env, char const* cmd, char const* cwd, char
}
}
JNIEXPORT jint JNICALL Java_com_termux_terminal_JNI_createSubprocess(JNIEnv* env, jclass TERMUX_UNUSED(clazz), jstring cmd, jstring cwd, jobjectArray args, jobjectArray envVars, jintArray processIdArray)
JNIEXPORT jint JNICALL Java_com_termux_terminal_JNI_createSubprocess(
JNIEnv* env,
jclass TERMUX_UNUSED(clazz),
jstring cmd,
jstring cwd,
jobjectArray args,
jobjectArray envVars,
jintArray processIdArray,
jint rows,
jint columns)
{
jsize size = args ? (*env)->GetArrayLength(env, args) : 0;
char** argv = NULL;
@@ -140,7 +156,7 @@ JNIEXPORT jint JNICALL Java_com_termux_terminal_JNI_createSubprocess(JNIEnv* env
int procId = 0;
char const* cmd_cwd = (*env)->GetStringUTFChars(env, cwd, NULL);
char const* cmd_utf8 = (*env)->GetStringUTFChars(env, cmd, NULL);
int ptm = create_subprocess(env, cmd_utf8, cmd_cwd, argv, envp, &procId);
int ptm = create_subprocess(env, cmd_utf8, cmd_cwd, argv, envp, &procId, rows, columns);
(*env)->ReleaseStringUTFChars(env, cmd, cmd_utf8);
(*env)->ReleaseStringUTFChars(env, cmd, cmd_cwd);

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

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

View File

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

Binary file not shown.

View File

@@ -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>
@@ -56,4 +53,8 @@
<string name="notification_action_wakelock">Wake</string>
<string name="notification_action_wifilock">Wifi</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>

View File

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

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<!-- See https://developer.android.com/training/backup/autosyncapi.html -->
<include domain="file" path="home/backup" />
</full-backup-content>

View File

@@ -0,0 +1,25 @@
package com.termux.app;
import junit.framework.TestCase;
import java.util.Collections;
import java.util.LinkedHashSet;
public class TermuxActivityTest extends TestCase {
private void assertUrlsAre(String text, String... urls) {
LinkedHashSet<String> expected = new LinkedHashSet<>();
Collections.addAll(expected, urls);
assertEquals(expected, TermuxActivity.extractUrls(text));
}
public void testExtractUrls() {
assertUrlsAre("hello http://example.com world", "http://example.com");
assertUrlsAre("http://example.com\nhttp://another.com", "http://example.com", "http://another.com");
assertUrlsAre("hello http://example.com world and http://more.example.com with secure https://more.example.com",
"http://example.com", "http://more.example.com", "https://more.example.com");
}
}

View File

@@ -17,21 +17,21 @@ public class ByteQueueTest extends TestCase {
public void testCompleteWrites() throws Exception {
ByteQueue q = new ByteQueue(10);
assertEquals(true, q.write(new byte[] { 1, 2, 3 }, 0, 3));
assertEquals(true, q.write(new byte[]{1, 2, 3}, 0, 3));
byte[] arr = new byte[10];
assertEquals(3, q.read(arr, true));
assertArrayEquals(new byte[] { 1, 2, 3 }, new byte[] { arr[0], arr[1], arr[2] });
assertArrayEquals(new byte[]{1, 2, 3}, new byte[]{arr[0], arr[1], arr[2]});
assertEquals(true, q.write(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }, 0, 10));
assertEquals(true, q.write(new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, 0, 10));
assertEquals(10, q.read(arr, true));
assertArrayEquals(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }, arr);
assertArrayEquals(new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, arr);
}
public void testQueueWraparound() throws Exception {
ByteQueue q = new ByteQueue(10);
byte[] origArray = new byte[] { 1, 2, 3, 4, 5, 6 };
byte[] origArray = new byte[]{1, 2, 3, 4, 5, 6};
byte[] readArray = new byte[origArray.length];
for (int i = 0; i < 20; i++) {
q.write(origArray, 0, origArray.length);
@@ -43,7 +43,7 @@ public class ByteQueueTest extends TestCase {
public void testWriteNotesClosing() throws Exception {
ByteQueue q = new ByteQueue(10);
q.close();
assertEquals(false, q.write(new byte[] { 1, 2, 3 }, 0, 3));
assertEquals(false, q.write(new byte[]{1, 2, 3}, 0, 3));
}
public void testReadNonBlocking() throws Exception {

View File

@@ -20,4 +20,13 @@ public class ControlSequenceIntroducerTest extends TerminalTestCase {
withTerminalSized(3, 4).enterString("1\r\n2\r\n3\r\nhi\033[Sy").assertLinesAre("2 ", "3 ", "hi ", " y");
}
/** CSI Ps X Erase Ps Character(s) (default = 1) (ECH). */
public void testCsiX() {
// See https://code.google.com/p/chromium/issues/detail?id=212712 where test was extraced from.
withTerminalSized(13, 2).enterString("abcdefghijkl\b\b\b\b\b\033[X").assertLinesAre("abcdefg ijkl ", " ");
withTerminalSized(13, 2).enterString("abcdefghijkl\b\b\b\b\b\033[1X").assertLinesAre("abcdefg ijkl ", " ");
withTerminalSized(13, 2).enterString("abcdefghijkl\b\b\b\b\b\033[2X").assertLinesAre("abcdefg jkl ", " ");
withTerminalSized(13, 2).enterString("abcdefghijkl\b\b\b\b\b\033[20X").assertLinesAre("abcdefg ", " ");
}
}

View File

@@ -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 ", " ", " ");
}
}

View File

@@ -4,13 +4,13 @@ package com.termux.terminal;
* <pre>
* "CSI ? Pm h", DEC Private Mode Set (DECSET)
* </pre>
*
* <p/>
* and
*
* <p/>
* <pre>
* "CSI ? Pm l", DEC Private Mode Reset (DECRST)
* </pre>
*
* <p/>
* controls various aspects of the terminal
*/
public class DecSetTest extends TerminalTestCase {
@@ -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. */
@@ -59,4 +64,15 @@ public class DecSetTest extends TerminalTestCase {
assertEquals("Terminal reset() should disable bracketed paste mode", "a", mOutput.getOutputAndClear());
}
/** DECSET 7, DECAWM, controls wraparound mode. */
public void testWrapAroundMode() {
// Default with wraparound:
withTerminalSized(3, 3).enterString("abcd").assertLinesAre("abc", "d ", " ");
// With wraparound disabled:
withTerminalSized(3, 3).enterString("\033[?7labcd").assertLinesAre("abd", " ", " ");
enterString("efg").assertLinesAre("abg", " ", " ");
// Re-enabling wraparound:
enterString("\033[?7hhij").assertLinesAre("abh", "ij ", " ");
}
}

View File

@@ -1,6 +1,7 @@
package com.termux.terminal;
import android.view.KeyEvent;
import junit.framework.TestCase;
public class KeyHandlerTest extends TestCase {
@@ -110,6 +111,10 @@ public class KeyHandlerTest extends TestCase {
// Backspace.
assertKeysEquals("\u007f", KeyHandler.getCode(KeyEvent.KEYCODE_DEL, 0, false, false));
// Space.
assertNull(KeyHandler.getCode(KeyEvent.KEYCODE_SPACE, 0, false, false));
assertKeysEquals("\u0000", KeyHandler.getCode(KeyEvent.KEYCODE_SPACE, KeyHandler.KEYMOD_CTRL, false, false));
// Back tab.
assertKeysEquals("\033[Z", KeyHandler.getCode(KeyEvent.KEYCODE_TAB, KeyHandler.KEYMOD_SHIFT, false, false));

View File

@@ -1,11 +1,11 @@
package com.termux.terminal;
import android.util.Base64;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import android.util.Base64;
/** "ESC ]" is the Operating System Command. */
public class OperatingSystemControlTest extends TerminalTestCase {
@@ -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");

View File

@@ -2,7 +2,7 @@ package com.termux.terminal;
/**
* ${CSI}${top};${bottom}r" - set Scrolling Region [top;bottom] (default = full size of window) (DECSTBM).
*
* <p/>
* "DECSTBM moves the cursor to column 1, line 1 of the page" (http://www.vt100.net/docs/vt510-rm/DECSTBM).
*/
public class ScrollRegionTest extends TerminalTestCase {
@@ -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 ");
}
}

View File

@@ -1,10 +1,10 @@
package com.termux.terminal;
import junit.framework.TestCase;
import java.util.Arrays;
import java.util.Random;
import junit.framework.TestCase;
public class TerminalRowTest extends TestCase {
/** The properties of these code points are validated in {@link #testStaticConstants()}. */
@@ -96,7 +96,7 @@ public class TerminalRowTest extends TestCase {
assertEquals(80, row.getSpaceUsed());
assertColumnCharIndicesStartsWith(0, 1, 2, 3);
char[] someChars = new char[] { 'a', 'c', 'e', '4', '5', '6', '7', '8' };
char[] someChars = new char[]{'a', 'c', 'e', '4', '5', '6', '7', '8'};
char[] rawLine = new char[80];
Arrays.fill(rawLine, ' ');
@@ -373,13 +373,13 @@ public class TerminalRowTest extends TestCase {
assertEquals(0, WcWidth.width(0x009F));
assertEquals(1, Character.charCount(0x009F));
int[] points = new int[] { 0xC2541, 'a', '8', 0x73EE, 0x009F, 0x881F, 0x8324, 0xD4C9, 0xFFFD, 'B', 0x009B, 0x61C9, 'Z' };
int[] points = new int[]{0xC2541, 'a', '8', 0x73EE, 0x009F, 0x881F, 0x8324, 0xD4C9, 0xFFFD, 'B', 0x009B, 0x61C9, 'Z'};
// int[] expected = new int[] { TerminalEmulator.UNICODE_REPLACEMENT_CHAR, 'a', '8', 0x73EE, 0x009F, 0x881F, 0x8324, 0xD4C9, 0xFFFD,
// 'B', 0x009B, 0x61C9, 'Z' };
int currentColumn = 0;
for (int i = 0; i < points.length; i++) {
row.setChar(currentColumn, points[i], 0);
currentColumn += WcWidth.width(points[i]);
for (int point : points) {
row.setChar(currentColumn, point, 0);
currentColumn += WcWidth.width(point);
}
// assertLineStartsWith(points);
// assertEquals(Character.highSurrogate(0xC2541), line.mText[0]);

View File

@@ -92,6 +92,7 @@ public class TerminalTest extends TerminalTestCase {
assertEnteringStringGivesResponse("\033[6n", "\033[2;1R");
}
/** Test the cursor shape changes using DECSCUSR. */
public void testSetCursorStyle() throws Exception {
withTerminalSized(5, 5);
assertEquals(TerminalEmulator.CURSOR_STYLE_BLOCK, mTerminal.getCursorStyle());
@@ -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", " ");
}
}

View File

@@ -1,5 +1,8 @@
package com.termux.terminal;
import junit.framework.AssertionFailedError;
import junit.framework.TestCase;
import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
@@ -9,16 +12,10 @@ import java.util.List;
import java.util.Objects;
import java.util.Set;
import junit.framework.AssertionFailedError;
import junit.framework.TestCase;
import com.termux.terminal.TerminalEmulator;
import com.termux.terminal.TerminalOutput;
public abstract class TerminalTestCase extends TestCase {
public static class MockTerminalOutput extends TerminalOutput {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
public final List<ChangedTitle> titleChanges = new ArrayList<>();
public final List<String> clipboardPuts = new ArrayList<>();
public int bellsRung = 0;
@@ -57,7 +54,7 @@ public abstract class TerminalTestCase extends TestCase {
public TerminalEmulator mTerminal;
public MockTerminalOutput mOutput;
public static class ChangedTitle {
public static final class ChangedTitle {
final String oldTitle;
final String newTitle;
@@ -68,6 +65,7 @@ public abstract class TerminalTestCase extends TestCase {
@Override
public boolean equals(Object o) {
if (!(o instanceof ChangedTitle)) return false;
ChangedTitle other = (ChangedTitle) o;
return Objects.equals(oldTitle, other.oldTitle) && Objects.equals(newTitle, other.newTitle);
}
@@ -115,8 +113,8 @@ public abstract class TerminalTestCase extends TestCase {
}
}
private static class LineWrapper {
TerminalRow mLine;
private static final class LineWrapper {
final TerminalRow mLine;
public LineWrapper(TerminalRow line) {
mLine = line;
@@ -129,7 +127,7 @@ public abstract class TerminalTestCase extends TestCase {
@Override
public boolean equals(Object o) {
return ((LineWrapper) o).mLine == mLine;
return o instanceof LineWrapper && ((LineWrapper) o).mLine == mLine;
}
}

View File

@@ -4,10 +4,10 @@ import junit.framework.TestCase;
public class TextStyleTest extends TestCase {
private static final int[] ALL_EFFECTS = new int[] { 0, TextStyle.CHARACTER_ATTRIBUTE_BOLD, TextStyle.CHARACTER_ATTRIBUTE_ITALIC,
private static final int[] ALL_EFFECTS = new int[]{0, TextStyle.CHARACTER_ATTRIBUTE_BOLD, TextStyle.CHARACTER_ATTRIBUTE_ITALIC,
TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE, TextStyle.CHARACTER_ATTRIBUTE_BLINK, TextStyle.CHARACTER_ATTRIBUTE_INVERSE,
TextStyle.CHARACTER_ATTRIBUTE_INVISIBLE, TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH, TextStyle.CHARACTER_ATTRIBUTE_PROTECTED,
TextStyle.CHARACTER_ATTRIBUTE_DIM };
TextStyle.CHARACTER_ATTRIBUTE_DIM};
public void testEncodingSingle() {
for (int fx : ALL_EFFECTS) {
@@ -34,7 +34,7 @@ public class TextStyleTest extends TestCase {
public void testEncodingStrikeThrough() {
int encoded = TextStyle.encode(TextStyle.COLOR_INDEX_FOREGROUND, TextStyle.COLOR_INDEX_BACKGROUND,
TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH);
assertTrue((TextStyle.decodeEffect(encoded) | TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH) != 0);
assertTrue((TextStyle.decodeEffect(encoded) & TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH) != 0);
}
public void testEncodingProtected() {

View File

@@ -10,14 +10,55 @@ public class UnicodeInputTest extends TerminalTestCase {
// continue with valid successor bytes (see Table 3-7), it must not consume the successor bytes as part of the ill-formed
// subsequence whenever those successor bytes themselves constitute part of a well-formed UTF-8 code unit subsequence."
withTerminalSized(5, 5);
mTerminal.append(new byte[] { (byte) 0b11101111, (byte) 'a' }, 2);
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 {
withTerminalSized(3, 3);
// UTF-8 for U+C2541, an unassigned code point:
byte[] b = new byte[] { (byte) 0xf3, (byte) 0x82, (byte) 0x95, (byte) 0x81 };
byte[] b = new byte[]{(byte) 0xf3, (byte) 0x82, (byte) 0x95, (byte) 0x81};
mTerminal.append(b, b.length);
enterString("Y");
assertEquals(1, Character.charCount(TerminalEmulator.UNICODE_REPLACEMENT_CHAR));
@@ -26,10 +67,10 @@ public class UnicodeInputTest extends TerminalTestCase {
public void testStuff() {
withTerminalSized(80, 24);
byte[] b = new byte[] { (byte) 0xf3, (byte) 0x82, (byte) 0x95, (byte) 0x81, (byte) 0x61, (byte) 0x38, (byte) 0xe7, (byte) 0x8f,
byte[] b = new byte[]{(byte) 0xf3, (byte) 0x82, (byte) 0x95, (byte) 0x81, (byte) 0x61, (byte) 0x38, (byte) 0xe7, (byte) 0x8f,
(byte) 0xae, (byte) 0xc2, (byte) 0x9f, (byte) 0xe8, (byte) 0xa0, (byte) 0x9f, (byte) 0xe8, (byte) 0x8c, (byte) 0xa4,
(byte) 0xed, (byte) 0x93, (byte) 0x89, (byte) 0xef, (byte) 0xbf, (byte) 0xbd, (byte) 0x42, (byte) 0xc2, (byte) 0x9b,
(byte) 0xe6, (byte) 0x87, (byte) 0x89, (byte) 0x5a };
(byte) 0xe6, (byte) 0x87, (byte) 0x89, (byte) 0x5a};
mTerminal.append(b, b.length);
}
@@ -80,8 +121,16 @@ public class UnicodeInputTest extends TerminalTestCase {
public void testOverlongUtf8Encoding() throws Exception {
// U+0020 should be encoded as 0x20, 0xc0 0xa0 is an overlong encoding
// so should be replaced with the replacement char U+FFFD.
withTerminalSized(5, 5).mTerminal.append(new byte[] { (byte) 0xc0, (byte) 0xa0, 'Y' }, 3);
withTerminalSized(5, 5).mTerminal.append(new byte[]{(byte) 0xc0, (byte) 0xa0, 'Y'}, 3);
assertLineIs(0, "\uFFFDY ");
}
public void testWideCharacterWithoutWrapping() throws Exception {
// With wraparound disabled. The behaviour when a wide character is output with cursor in
// the last column when autowrap is disabled is not obvious, but we expect the wide
// character to be ignored here.
withTerminalSized(3, 3).enterString("\033[?7l").enterString("枝枝枝").assertLinesAre("", " ", " ");
enterString("a枝").assertLinesAre("枝a", " ", " ");
}
}

View File

@@ -39,6 +39,7 @@ public class WcWidthTest extends TestCase {
public void testCombining() {
assertWidthIs(0, 0x0302);
assertWidthIs(0, 0x0308);
assertWidthIs(0, 0x2060);
}
public void testWatch() {

View File

@@ -4,6 +4,10 @@
set -e -u
SRC_JNILIBS=app/src/main/jniLibs/
rm -Rf $SRC_JNILIBS
mkdir -p $SRC_JNILIBS
PROJECTDIR=`mktemp -d`
JNIDIR=$PROJECTDIR/jni
LIBSDIR=$PROJECTDIR/libs
@@ -12,6 +16,6 @@ mkdir $JNIDIR
cp app/src/main/jni/* $JNIDIR/
ndk-build NDK_PROJECT_PATH=$PROJECTDIR
cp -Rf $LIBSDIR/* app/src/main/jniLibs/
cp -Rf $LIBSDIR/* $SRC_JNILIBS
rm -Rf $PROJECTDIR

View File

@@ -5,7 +5,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.3.1'
classpath 'com.android.tools.build:gradle:2.0.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files

Binary file not shown.

View File

@@ -1,6 +1,6 @@
#Thu Oct 22 22:50:58 CEST 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.7-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-2.12-bin.zip

4
gradlew vendored
View File

@@ -56,9 +56,9 @@ while [ -h "$PRG" ] ; do
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >&-
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >&-
cd "$SAVED" >/dev/null
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar

90
gradlew.bat vendored
View File

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

Binary file not shown.