Compare commits

..

290 Commits
v0.48 ... v0.77

Author SHA1 Message Date
Fredrik Fornwall
3b969a0077 Bump version to 0.77 2019-10-28 01:54:50 +01:00
Leonid Plyushch
ae717d8f5f do not allow to backup data by Google
Why:

 * During backup process Termux is being killed in most cases.
 * Backup data is limited to 25 MB.
 * Backup may not be performed/updated in certain cases.
2019-10-27 20:33:12 +01:00
Leonid Plyushch
6c55c3c1be bootstraps: update to v18 2019-10-20 22:06:18 +03:00
Fredrik Fornwall
c0a0822843 Do not set LD_LIBRARY_PATH by default
See https://github.com/termux/termux-app/issues/1286
2019-10-20 20:48:10 +02:00
Fredrik Fornwall
08b08d1c47 Bump version to 0.76 2019-10-20 20:16:48 +02:00
Fredrik Fornwall
c76cec186a Include bootstrap zips as shared libraries 2019-10-20 20:15:50 +02:00
Leon Omelan
5ba3f7cf6d Made Black UI an option to configure 2019-10-20 20:05:48 +02:00
Leon Omelan
468f878a38 Unified UI colors across the app. Dark sidebar and dark app theme for
dark Alert Dialogs
2019-10-20 20:05:48 +02:00
Fredrik Fornwall
c50a367063 Add .cxx folder to gitignore 2019-10-13 20:48:24 +02:00
Leonid Plyushch
4189f598b9 add permission ACCESS_NETWORK_STATE
Seems to be required by some Android TV devices.
2019-10-13 20:05:55 +02:00
Leonid Plyushch
cb67e805de update readme 2019-10-07 15:32:21 +03:00
Leonid Plyushch
3f83368f42 CI: update .cirrus.yml 2019-10-07 15:01:22 +03:00
Leonid Plyushch
8c7462678f update readme 2019-10-07 00:21:57 +03:00
Leonid Plyushch
6f11390cc4 update readme 2019-10-07 00:20:33 +03:00
Leonid Plyushch
33d390a228 CI: development builds are now signed with publically-shared key
This commit adds keystore which is shared with community. People freely can
use it for creating own Termux app and plugin builds. However, builds signed
with such keystore cannot be proven as official.

Signed-off-by: Leonid Plyushch <leonid.plyushch@gmail.com>
2019-10-06 23:24:15 +03:00
Leonid Plyushch
0332779d6a update readme 2019-10-04 23:50:46 +03:00
Leonid Plyushch
f19575b942 readme: update status badge url 2019-10-04 02:16:22 +03:00
Leonid Plyushch
6a95809da3 CI: add test keystore 2019-10-04 02:08:44 +03:00
Leonid Plyushch
ac8dab70ef CI: add initial configuration for Cirrus 2019-10-04 01:59:49 +03:00
Leonid Plyushch
243285f7c2 CI: remove Travis setup 2019-10-04 01:59:49 +03:00
Fredrik Fornwall
b6182216d5 Update android gradle plug-in 2019-10-02 21:37:23 +02:00
Fredrik Fornwall
e206121bbc Bump version to 0.75 2019-09-08 21:43:52 +02:00
Fredrik Fornwall
deceffad00 Update gradle configuration 2019-09-08 21:43:07 +02:00
Leonid Plyushch
c19909cef1 improve url matching regex
Now it support complex URLs and some other schemes beyond just http/ftp.
2019-09-08 20:10:26 +02:00
Leonid Plyushch
5b7e40638c handle possible ActivityNotFoundException when requesting to disable battery optimizations
Related issue: https://github.com/termux/termux-app/issues/1224
2019-09-08 20:10:15 +02:00
Leonid Plyushch
a3673d1af5 extra keys: follow DnD mode and haptic feedback Android settings
Do not vibrate when:

 * Do not disturb mode is on.

 * Haptic feedback is disabled in Android settings.
2019-09-08 20:08:16 +02:00
Fredrik Fornwall
d5f9cf85c9 Bump version to 0.74 2019-08-18 22:11:21 +02:00
hungphd
549f09573f Change method name 'with' to 'getInstance' 2019-08-18 20:30:34 +02:00
Daniil Gentili
94e5bc86fb Ignore battery optimization settings to prevent suspension 2019-08-18 20:23:00 +02:00
Fredrik Fornwall
370ac2bd89 Avoid joining lines for shared transcript (#1166) 2019-08-04 19:42:36 +02:00
Fredrik Fornwall
7041f41981 Bump version to 0.73 2019-08-04 17:03:41 +02:00
Fredrik Fornwall
dd6a21257b Update the Android Gradle plugin 2019-08-04 16:44:58 +02:00
Fredrik Fornwall
445da0c4ea Fix problem with sharing large terminal transcript
Fixes #1166.
2019-07-14 20:26:16 +02:00
Sacha Chua
92980824b1 Add function keys F1 to F12 as keyboard options 2019-07-04 23:27:13 +02:00
Michal Bednarski
7d42b07a32 Also export ANDROID_TZDATA_ROOT
Since Android Q Beta 2 ANDROID_TZDATA_ROOT is required
in addition to ANDROID_RUNTIME_ROOT in order to run
`am` and `dalvikvm`
2019-07-04 23:26:55 +02:00
Fredrik Fornwall
e502b9169f Respect failsafe sessions in ACTION_EXECUTE 2019-06-09 20:00:17 +02:00
Leonid Plyushch
9f2d887ca0 minor fix for log message while removing tmpdir 2019-06-09 14:31:09 +03:00
Fredrik Fornwall
e6aacc5f08 Update Android Gradle plug-in 2019-05-27 14:07:50 +02:00
Fredrik Fornwall
ff935be54a Add versionName gradle task 2019-05-27 00:07:41 +02:00
Fredrik Fornwall
4e76162346 Bump version to 0.72 2019-05-27 00:03:42 +02:00
Leonid Plyushch
5fa4f2647b set BOOTCLASSPATH environment variable
Required on at least Android 5. Otherwise utilities that use dalvikvm may fail.

Issue happens on Nexus 7 with official 5.0/5.1 firmware and possibly other
AOSP ROMs.
2019-05-26 20:05:03 +02:00
Fredrik Fornwall
81b5889a26 Bump version to 0.71 2019-05-21 21:52:39 +02:00
Leonid Plyushch
514f59258a let $PREFIX/tmp be cleaned when TermuxService is going to be stopped 2019-05-21 21:52:14 +02:00
Leonid Plyushch
9f79393aa5 Revert "clean /tmp directory on cold start"
This reverts commit beb8a004e6.
2019-05-21 21:52:14 +02:00
Leonid Plyushch
e49d514236 Revert "sessions: do not clear TMPDIR if application was not started"
This reverts commit bd45837d93.
2019-05-21 21:52:14 +02:00
Leonid Plyushch
4e1462326c remove intent category "default"
It was needed only when Termux had separate icon for failsafe activity.
2019-05-21 21:52:14 +02:00
Fredrik Fornwall
b0f1773a92 Bump version to 0.70 2019-05-20 13:32:35 +02:00
Fredrik Fornwall
af9f28c010 Remove the failsafe activity
The failsafe activity were infrequently used while confusing users.

Replace it with an app shortcut on Android 7.1+ and a separate app on
earlier versions.
2019-05-20 13:32:03 +02:00
Nishith Khanna
fef0c66868 Tweak notification icon color to make it work with dark notifcations
As the notification icon and text is set to black, ROMs like Samsung OneUI and themes that change notifications to have a dark background will have visibility issues with Termux notifications. This commit sets a neutral color which will be visible on both white and dark backgrounds.
2019-05-20 13:27:58 +02:00
Fredrik Fornwall
7a5da83ce2 Bump version to 0.69 2019-05-12 00:18:02 +02:00
Fredrik Fornwall
97f01387b9 Update Android Gradle plugin 2019-05-12 00:17:16 +02:00
Jonas L
012ff83a06 Add limit for the bell (vibration)
This fixes https://github.com/termux/termux-app/issues/442
2019-05-12 00:13:54 +02:00
Michal Bednarski
a082c63849 Make am work on Android Q 2019-05-12 00:10:27 +02:00
Fredrik Fornwall
8c27084086 Bump version to 0.68 2019-04-22 00:54:27 +02:00
Fredrik Fornwall
f4fb45cbd6 Add the label attribute on TermuxActivity
Should fix the F-Droid naming of the app, see
https://github.com/termux/termux-app/issues/1107
2019-04-21 20:19:34 +02:00
Leonid Plyushch
0605fc4843 reformat res/values/strings.xml 2019-04-18 20:22:40 +03:00
Leonid Plyushch
3bbd61f9d7 extra keys view: fix crash in some cases
AndroidRuntime: FATAL EXCEPTION: main
AndroidRuntime: Process: com.termux, PID: 15799
AndroidRuntime: java.lang.NullPointerException: Attempt to invoke virtual method 'boolean android.widget.ToggleButton.isPressed()' on a null object reference
AndroidRuntime:        at com.termux.app.ExtraKeysView.readSpecialButton(ExtraKeysView.java:119)
AndroidRuntime:        at com.termux.app.TermuxViewClient.readControlKey(TermuxViewClient.java:116)
AndroidRuntime:        at com.termux.view.TerminalView.inputCodePoint(TerminalView.java:655)
AndroidRuntime:        at com.termux.view.TerminalView$2.sendTextToTerminal(TerminalView.java:336)
AndroidRuntime:        at com.termux.view.TerminalView$2.commitText(TerminalView.java:273)
AndroidRuntime:        at com.android.internal.view.IInputConnectionWrapper.executeMessage(IInputConnectionWrapper.java:341)
AndroidRuntime:        at com.android.internal.view.IInputConnectionWrapper$MyHandler.handleMessage(IInputConnectionWrapper.java:85)
AndroidRuntime:        at android.os.Handler.dispatchMessage(Handler.java:105)
AndroidRuntime:        at android.os.Looper.loop(Looper.java:164)
AndroidRuntime:        at android.app.ActivityThread.main(ActivityThread.java:6944)
AndroidRuntime:        at java.lang.reflect.Method.invoke(Native Method)
AndroidRuntime:        at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:327)
AndroidRuntime:        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1374)
2019-04-17 03:13:32 +03:00
Fredrik Fornwall
42fc0ea1cd Bump version to 0.67 2019-04-14 20:55:52 +02:00
Fredrik Fornwall
6218d08037 Use https://termux.org/bootstrap-${ARCH}.zip
This will redirect to the desired version of the bootstrap zip in
bintray.
2019-04-08 19:06:57 +02:00
Leonid Plyushch
10fe238ddb termux service: make sure that styling is always applied whenever session starts 2019-04-07 20:53:38 +02:00
Leonid Plyushch
fe41cd486f sessions: failsafe session is now accessible via separate launcher icon
Also enables session autoclosing so no more "annoying" messages
about "process completed - press enter". There autoclosing will be
performed on exit codes '0' and '130'.

On Android TV devices old behaviour will be used - auto close enabled for all
sessions when amount of running sessions >1.
2019-04-07 20:51:25 +02:00
Fredrik Fornwall
bda80547ad Do not export LD_LIBRARY_PATH if bintray is used 2019-04-03 23:20:08 +02:00
Fredrik Fornwall
70a786613d Install from bintray on Android 7 and later 2019-04-03 23:02:31 +02:00
Fredrik Fornwall
e4220a7ab1 Update the Android gradle plugin 2019-04-03 23:02:31 +02:00
Leonid Plyushch
bd45837d93 sessions: do not clear TMPDIR if application was not started 2019-02-24 22:01:31 +02:00
Leonid Plyushch
ddf5341b86 native text input: make text selection visible 2019-02-24 22:01:22 +02:00
Yuvraj Saxena
85a48a79a3 termux-app: Fix a typo in TermuxPreferences 2019-02-19 20:22:26 +02:00
Tom Yan
e3512c957d fix termux/termux-app#995 on the java side 2019-02-13 22:50:29 +01:00
Fredrik Fornwall
330301899a Send \r instead of \n from native input text view (fixes #1020) 2019-02-09 23:14:44 +01:00
iamahuman
2a36b915cb Implement CSI 3 J - Clear transcript 2019-02-05 23:27:25 +01:00
Leonid Plyushch
c986a46fb9 extra keys: fix crash when clicking on "empty" (undefined) key 2019-02-05 23:26:08 +01:00
Fredrik Fornwall
ed8bd4b569 Update gradle wrapper 2019-02-03 22:38:55 +01:00
Fredrik Fornwall
7f4df8328c Toggle the extra keys view with VolumeUp+K
Using K as a shortcut for toggling extra Keyboard probably makes more
sense than VolumeUp+Q, although we support both for now.
2019-01-21 00:52:24 +01:00
Fredrik Fornwall
ad1ce585d9 Bump version to 0.66 2019-01-21 00:37:39 +01:00
Fredrik Fornwall
158908a3bf Keep help context menu item as last item 2019-01-21 00:34:02 +01:00
Fredrik Fornwall
d8f066dec4 Minor restructuring of TermuxPreferences 2019-01-21 00:33:03 +01:00
Leonid Plyushch
743225ab6a feature: allow to keep screen on 2019-01-21 00:24:49 +01:00
Fredrik Fornwall
b38ae24908 Make termux-reload-settings reload the extra keys directly 2019-01-21 00:12:07 +01:00
Fredrik Fornwall
70c1bddae0 Use constant for utf-8 encoding 2019-01-20 23:58:04 +01:00
Fredrik Fornwall
4df285013e Use constant for utf-8 encoding 2019-01-20 23:50:49 +01:00
Fredrik Fornwall
1e4fa8c045 Update the android gradle plugin 2019-01-20 23:49:03 +01:00
Fredrik Fornwall
31f2dc35bc Fix Android Studio lint warning 2019-01-20 23:48:23 +01:00
Henrik Grimler
fbb048ce56 Add issue templates
Hopefully gives less package bug reports opened in termux-app
2019-01-20 23:46:20 +01:00
Fredrik Fornwall
8c3a30027e Migrate to AndroidX 2019-01-11 21:19:06 +01:00
Fredrik Fornwall
a8c270f3c1 Use size in icon name in big icon 2019-01-11 00:36:47 +01:00
Trygve Aaberge
829414ac19 Reduce the height of viewpager/ExtraKeysView to previous height
Commit f0eeb47 introduced configurable extra keys and thus changed the
height of the viewpager to be multipled by the number of rows of extra
keys. However, the extra keys had two rows at the time, so the initial
height should have been changed to half of what it was now that it is
multiplied. This was not done, so the viewpager was twice the height it
should be after that change.
2019-01-11 00:32:22 +01:00
Fredrik Fornwall
8e0b8623bc Update art/copy-to-other-apps.sh 2019-01-10 23:58:18 +01:00
Fredrik Fornwall
e2bda2bb52 Add art/generate-big-icon.sh script 2018-11-23 02:22:38 +01:00
Fredrik Fornwall
8c7b1a60d2 Minor icon tweaks 2018-11-23 02:12:57 +01:00
Fredrik Fornwall
cb0710bb83 Use a solid color as background for adaptive icon 2018-11-17 23:34:14 +01:00
Fredrik Fornwall
b54c8276b4 Add .swp to gitignore 2018-11-17 23:26:26 +01:00
Fredrik Fornwall
7056945f5d Move to vector icon also on pre-26 2018-11-17 23:19:22 +01:00
Fredrik Fornwall
063ef02f2b Let the installer create directories when necessary
By creating directories when necessary before trying to install files we
depend on less details in how the bootstrap zip is constructed.
2018-11-17 22:50:27 +01:00
Fredrik Fornwall
6e6e4fd212 Use vector drawables for the launcher icon 2018-11-14 00:21:59 +01:00
Fredrik Fornwall
406438e391 Fix Android Studio lint warnings 2018-11-10 23:16:54 +01:00
Fredrik Fornwall
d1977479bd Update the Android Gradle plugin 2018-11-10 23:16:27 +01:00
Fredrik Fornwall
b4563023f6 Add casts in termux.c to silence lint warning 2018-11-08 22:31:06 +01:00
Fredrik Fornwall
095ed8b54f Use jint as return value from native function
This silences an error from the Android Studio lint.
2018-11-08 22:28:37 +01:00
Markus Gräb
af445f9618 Improve config parsing 2018-10-25 23:31:38 +02:00
Fredrik Fornwall
82f977fbf1 Remove unused function 2018-10-07 20:08:41 +02:00
Fredrik Fornwall
4b52cfbb93 Drop the round launcher icon
It's better to focus on the adaptive icon for the future. See #830.
2018-10-02 21:54:03 +02:00
Fredrik Fornwall
b61da23be7 Enable java 8 2018-09-29 00:49:05 +02:00
Fredrik Fornwall
35df3f72c9 Revert to java 8 for travis build 2018-09-29 00:32:43 +02:00
Fredrik Fornwall
d584502fef Update .travis.yml 2018-09-29 00:27:25 +02:00
Fredrik Fornwall
a691333c5e Update the gradle wrapper 2018-09-29 00:25:24 +02:00
Fredrik Fornwall
e053cf82ca Update the Android Gradle plugin 2018-09-29 00:22:44 +02:00
Fredrik Fornwall
076176a5f6 Add whole of .idea/ to .gitignore 2018-09-29 00:22:22 +02:00
Fredrik Fornwall
08c72f9a65 Remove .idea/ folder 2018-09-29 00:17:25 +02:00
Fredrik Fornwall
7593ddde38 Update README.md 2018-09-23 16:57:58 +02:00
Matthew Klein
6dce32aece Use 2 spaces for yaml files (.travis.yml) 2018-08-30 13:51:24 +02:00
Matthew Klein
5ff801c914 Use sdkmanager to download ndk-bundle 2018-08-29 13:54:09 +02:00
Matthew Klein
221046732b Update travis to android 28
This bumps build-tools from 27.0.3 to 28.0.2.
2018-08-29 13:54:09 +02:00
Fredrik Fornwall
07d6c9bd5e Remove unused import 2018-08-28 02:51:11 +02:00
Fredrik Fornwall
a9eddce672 compileSdkVersion = targetSdkVersion = 28 2018-08-28 02:50:52 +02:00
Leonid Plyushch
26c95f1397 reduce padding to 3dp (5 dp seems too many) 2018-08-28 02:39:41 +02:00
Leonid Plyushch
4eca639212 add some padding for TerminalView
Should fix https://github.com/termux/termux-app/issues/516
2018-08-28 02:39:41 +02:00
Leonid Plyushch
beb8a004e6 clean /tmp directory on cold start 2018-08-28 02:37:03 +02:00
Robert Vanden Eynde
3693e3c1b6 Default extra-keys has TAB after ESC and UP/DOWN arrow keys 2018-08-28 02:34:36 +02:00
Robert Vanden Eynde
99e8ffcf90 Add aliases for BACKSLASH QUOTE and APOSTROPHE 2018-08-28 02:34:36 +02:00
Henrik Grimler
0807600a2d ExtraKeys: use ' instead of \" in default key string 2018-08-28 02:34:36 +02:00
Robert Vanden Eynde
f74293e8fb Use extra-keys spelling and correct default setting 2018-08-28 02:34:36 +02:00
Henrik Grimler
b99d092305 ExtraKeys: Prevent app crash if user specifies different row lengths 2018-08-28 02:34:36 +02:00
Robert Vanden Eynde
af7515247b Fix refactoring, Ctrl, Alt, Fn keys work again 2018-08-28 02:34:36 +02:00
Henrik Grimler
ec77be00dc state -> SpecialButtonState state 2018-08-28 02:34:36 +02:00
Henrik Grimler
a854960476 Declare buttonText final String, dont't change value inside reload 2018-08-28 02:34:36 +02:00
Henrik Grimler
9db8948f23 Fix typos and build errors 2018-08-28 02:34:36 +02:00
Robert Vanden Eynde
c24167f6a5 Use utf-8 for config, Activate defaultCharDisplay, Fix Typo 2018-08-28 02:34:36 +02:00
Robert Vanden Eynde
49c051c8b7 Fix Typo (KeyEvent.KEYCODE_DEL and others) 2018-08-28 02:34:36 +02:00
Robert Vanden Eynde
55efdb2f56 Refactor Ctrl, Alt, Fn code 2018-08-28 02:34:36 +02:00
Robert Vanden Eynde
d03e420e75 Use LEFT instead of arrow keys in config, and include arrows characters for special keys (will be easily changed in the properties after) 2018-08-28 02:34:36 +02:00
Henrik Grimler
06968a9295 Extrakeys: fix typo 2018-08-28 02:34:36 +02:00
Henrik Grimler
244248b1a0 Extrakeys: make ― the same as - in some more places 2018-08-28 02:34:36 +02:00
Henrik Grimler
7187ed6950 Extrakeys: fix rebase error 2018-08-28 02:34:36 +02:00
Henrik Grimler
b3eabd9bad ExtraKeys: fix so app doesn't crash if ctrl/alt aren't in extrakeys
Otherwise we get:
AndroidRuntime: java.lang.NullPointerException: Attempt to invoke virtual method 'boolean android.widget.CompoundButton.isChecked()' on a null object reference
AndroidRuntime:        at com.termux.app.ExtraKeysView.b(SourceFile:128)
2018-08-28 02:34:36 +02:00
Henrik Grimler
b51dd4f558 Extrakeys: show extra key row per default
Might help new users.
2018-08-28 02:34:36 +02:00
Henrik Grimler
f88b9c4629 ExtraKeys: add possibility for insert, delete, enter and - 2018-08-28 02:34:36 +02:00
neverwin
f0eeb4781b add support of configurable extra keys 2018-08-28 02:34:36 +02:00
Fredrik Fornwall
57a3a9b111 Request the FOREGROUND_SERVICE permission
From https://developer.android.com/guide/components/services:

"Apps that target Android 9 (API level 28) or higher and use foreground
services must request the FOREGROUND_SERVICE permission. This is a
normal permission, so the system automatically grants it to the
requesting app.

If an app that targets API level 28 or higher attempts to create a
foreground service without requesting FOREGROUND_SERVICE, the system
throws a SecurityException."
2018-08-26 01:07:33 +02:00
Fredrik Fornwall
8df73a3006 Set targetSdkVersion 28 2018-08-14 01:07:51 +02:00
Leonid Plyushch
963663e0cd close sessions without waiting for user input except the last one
An attempt to deal with these issues:

 * https://github.com/termux/termux-app/issues/627
 * https://github.com/termux/termux-app/issues/56
2018-08-11 23:09:16 +02:00
Lokesh Krishna
68a83ccf37 replaces foreground and background layers with nord color variants 2018-08-06 21:53:30 +02:00
Lokesh Krishna
db13ea02b6 Adds foreground and background layers 2018-08-02 00:41:18 +02:00
Lokesh Krishna
61a44dbfa8 Adds alternative drawable resource 2018-08-02 00:41:18 +02:00
Fredrik Fornwall
aaa92279ca Bump version to 0.65 2018-08-02 00:38:21 +02:00
Ico Doornekamp
365f9723cc - in onKeyDown() ignore keys when the Fn key is pressed
- in onKeyDown() return false when the key is not handled.

The above two changes fix the handling of Fn-key combo's on devices with
a physical keyboard, allowing the android system defined fallbacks from
`/system/usr/keychars/Generic.kcm` to be properly handled.

Fixes #731.

Original diagnosis and fix by Konehaltia.
2018-08-02 00:36:44 +02:00
Fredrik Fornwall
fdae272214 Bump version to 0.64 2018-07-02 00:05:04 +02:00
Leonid Plyushch
b7864d6ac2 deleteFolder(): check if passed argument is a symlink
Prevents possible data loss when user replaced directory '~/storage' with
a symlink.
2018-07-01 18:00:54 +02:00
Peter Vágner
d1f0c76db3 TerminalView: only use accessibility features when accessibility is
enabled when starting the view
2018-06-29 12:31:15 +02:00
Peter Vágner
5652624fc2 Call setContentDescription in onScreenUpdated rather than in onDraw.
That will be much less expensive.
2018-06-29 12:31:15 +02:00
Peter Vágner
35a9101f84 terminalview: add contentDescription to the view so accessibility
services can get the text currently being shown.
2018-06-29 12:31:15 +02:00
David xu
2b6a10712b fix button background and row height bug in api 21 2018-06-29 12:30:02 +02:00
Matthew Klein
c80e126b8f Add a .gitattributes file 2018-06-28 11:57:12 +02:00
Fredrik Fornwall
89048274dd Bump version to 0.63 2018-06-25 14:08:50 +02:00
Leonid Plyushch
d3b4d35b2a deleteFolder(): don't treat symlinks as directory 2018-06-25 14:08:28 +02:00
Fredrik Fornwall
4080f41cc7 Bump version to 0.62 2018-06-24 01:56:43 +02:00
Tom Yan
b4e2c99d46 Stop exporting PS1
PS1 is not supposed to be an env var and should be shell-specific.
We will set it appropriately with the init files of the shells.
2018-06-22 01:11:51 +02:00
Quasic
c5923201a4 fix indent with vim on termux
web interface would have worked, if I had deleted the tabs, first
This vim is adding tabs instead of matching the previous line's
indentation. Looking into fixing that now...
2018-06-22 00:33:35 +02:00
Quasic
bafd21bb39 fmt indentation 2018-06-22 00:33:35 +02:00
Quasic
0f20fab02c I think this is it 2018-06-22 00:33:35 +02:00
Quasic
c5ae5bb06a oops, should have local Java 2018-06-22 00:33:35 +02:00
Quasic
bbd46a763c Fix for #572 using recursive delete
Also see #578
2018-06-22 00:33:35 +02:00
David xu
dc145d65f8 minify long press interval 2018-06-22 00:29:51 +02:00
David xu
ce2d1c0d88 restore terminalview 2018-06-22 00:29:51 +02:00
David xu
f2f7f963e6 feat for - ― 2018-06-22 00:29:51 +02:00
David xu
2e53ef038e fix popup bug 2018-06-22 00:29:51 +02:00
xqdoo00o
8c82f43dce delete unnecss code 2018-06-22 00:29:51 +02:00
david
3a16f461e7 add symbol popup 2018-06-22 00:29:51 +02:00
xqdoo00o
594a5dfe25 comment unnecss code 2018-06-22 00:29:51 +02:00
david
d5e007dbb3 feat button color 2018-06-22 00:29:51 +02:00
david
f4335d3824 add some functional keys 2018-06-22 00:29:51 +02:00
Michał Bednarski
90b6f93697 Add fast path for TerminalRow.setChar
termux/termux-app#603
2018-06-22 00:21:31 +02:00
Fredrik Fornwall
17c4a45212 Bump gradle dependencies 2018-06-15 00:22:52 +02:00
Fredrik Fornwall
6d9ffb6922 Bump version to 0.61 2018-06-15 00:21:27 +02:00
Fredrik Fornwall
8472fce8ba Use lower when looking up mime type from extension
Fixes #721.
2018-06-15 00:19:47 +02:00
Fredrik Fornwall
69d954a583 Update gradle configuration 2018-06-14 23:58:41 +02:00
easyaspi314 (Devin)
ec1087d56f Replace CRLF with CR as well.
This should replace both \r\n and \n with \r now.

"\r?\n" matches 0 or 1 \r and one \n, which should capture both
escape sequences.
2018-06-13 22:45:37 +02:00
easyaspi314 (Devin)
be6a73d862 Replace \n with \r when pasting (Fixes #678)
Termux will now properly send \r to the terminal instead of \n when pasting
multiline strings.

This fixes cat not repeating back lines and nano accidentally justifying
text (because \n maps to ^J), as well as other potential issues.

This matches the behavior of other terminals, such as iTerm2 which explicitly
does it here:
https://github.com/gnachman/iTerm2/blob/f8a5930/sources/iTermPasteHelper.m#L113

Signed-off-by: easyaspi314 (Devin) <easyaspi314@users.noreply.github.com>
2018-06-13 22:45:37 +02:00
l-jonas
80c81b274d Update build.gradle 2018-06-11 12:26:35 +02:00
Jonas L
cd9dbac548 Add Google Repository
The Google-Repository is required for compilation because some dependencies were removed from jcenter()
2018-06-11 12:26:35 +02:00
Fredrik Fornwall
0c837796f0 Update Android plugin for Gradle 2018-06-11 12:23:52 +02:00
Fredrik Fornwall
3417e37e8d Update gitignore for Android Studio 3.1 2018-04-02 01:38:47 +02:00
Fredrik Fornwall
31ba36e0fa Remove explicit buildToolsVersion
https://developer.android.com/studio/releases/gradle-plugin.html#behavior_changes
2018-04-02 00:58:02 +02:00
Fredrik Fornwall
c444f1fd28 Update gradle configuration 2018-04-02 00:40:16 +02:00
RangerMauve
9f36ed06b8 Make TerminalRenderer public
I want to use the renderer with a custom canvas without having to render to an Android View which requires a context and all sorts of stuff.

Can't currently do that because the renderer is package-private
2018-03-10 22:55:10 +01:00
Fredrik Fornwall
c2ab5bcd50 Bump some dependency versions 2018-02-06 23:30:28 +01:00
Fredrik Fornwall
48fab33b79 Bump version to 0.60 2018-01-26 01:00:37 +01:00
Marcel Dopita
8d7a67645b Fix notification icon color
Icons used in notifications/status bar are monochrome and should use only white color and transparency. It is displayed fine on Android emulators but it's shown in black on Samsung Galaxy S7 running Android 7.0.

After the change, status bar icon is white and the small icon in notification still uses the black color set via setColor(). Termux doesn't support devices running Android older than 5.0 but using white color in vector drawables would be also necessary to generate proper (white) bitmap icons.
2018-01-01 23:17:22 +01:00
Fredrik Fornwall
186b49d429 Work around travis API 27 SDK checksum mismatch
See https://github.com/travis-ci/travis-ci/issues/8874.
2018-01-01 23:16:43 +01:00
Fredrik Fornwall
40a2775f52 Bump version to 0.59 2017-12-09 00:59:19 +01:00
Fredrik Fornwall
f802d6001d Do not send mouse clicks out of range (fixes #501) 2017-12-09 00:58:31 +01:00
Fredrik Fornwall
50f66a12da Bump version to 0.58 2017-12-08 23:32:44 +01:00
Fredrik Fornwall
d8e6fd21d1 Set android:resizeableActivity="true"
Setting android:resizeableActivity="true" should AFAIK not be needed
as https://developer.android.com/guide/topics/ui/multi-window.html
states that:

  If your app targets API level 24, but you do not specify a value
  for this attribute, the attribute's value defaults to true.

However, on a Galaxy S8 running Android 7.0 this attribute is needed
in order to have resizeable windows when in Samsung Dex mode.

Fixes #309.
2017-12-08 23:30:45 +01:00
Fredrik Fornwall
0964d83572 Bump com.android.support dependencies 2017-12-07 10:25:02 +01:00
Fredrik Fornwall
cee0466dd7 Bump version to 0.57 2017-12-07 01:48:51 +01:00
Fredrik Fornwall
de4f334e24 Remove the fullscreen setting
Trying to use fullscreen doesn't work well in a multi-windowed world
and makes using translucent navigation and status bars more complicated.
2017-12-07 01:46:16 +01:00
Fredrik Fornwall
ab205ae05b Update build tools in travis 2017-12-07 01:45:30 +01:00
Fredrik Fornwall
b29b24f507 Add android.max_aspect=10.0 metadata
This is needed to mark the app with

	"This app is optimized to run in full screen"

when run on a Samsung Galaxy S8.
2017-12-07 01:39:43 +01:00
Fredrik Fornwall
b3472e9e62 Disable the "quick scale" gesture
Disable the default android behaviour where a double tap followed by a
swipe is interpreted as a zoom gesture. Most people seem not to know
about it and hit it by mistake, see #495 for an example.
2017-12-07 01:36:29 +01:00
Fredrik Fornwall
ab59e08959 Update build tools 2017-12-07 01:12:39 +01:00
Fredrik Fornwall
8b566485e8 Do not clear the sessions on ACTION_STOP_SERVICE
There is no point doing so and it may cause problems with the list
adapter.
2017-11-26 00:56:13 +01:00
Fredrik Fornwall
231c02a0c7 Check for null mTermSession in inputCodePoint() 2017-11-26 00:50:13 +01:00
Fredrik Fornwall
65f30e77ba Add tools:ignore="AppLinkUrlError" 2017-11-25 22:59:03 +01:00
Fredrik Fornwall
686677ae45 Update android support dependencies 2017-11-25 22:58:46 +01:00
Fredrik Fornwall
85037a75a6 Remove redundant cast 2017-11-25 22:58:23 +01:00
Fredrik Fornwall
6025afc2c0 Update the Android Plugin for Gradle to 3.0.1 2017-11-24 23:11:55 +01:00
Fredrik Fornwall
a4e4f76775 Remove some Android Studio warnings 2017-11-22 01:27:14 +01:00
Fredrik Fornwall
02af113dda Bump app version to 0.56 2017-11-19 20:54:16 +01:00
Fredrik Fornwall
fc4ef838bf Configure proguard to keep line numbers 2017-11-19 20:53:56 +01:00
Fredrik Fornwall
86bd3ca21b Bump app version to 0.54 2017-11-19 15:27:17 +01:00
Fredrik Fornwall
367398bafb Update gradle configuration 2017-11-19 15:27:00 +01:00
Lauri Tirkkonen
dd502e55f8 set default notification priority to low
since we're a foreground service, in oreo we already get a higher
priority; see
https://developer.android.com/reference/android/app/NotificationManager.html#IMPORTANCE_MIN
2017-11-12 23:44:14 +01:00
Fredrik Fornwall
798125ef7a Remove outdated comment 2017-11-12 23:06:28 +01:00
Fredrik Fornwall
5126f06e06 Update .travis.yml to latest setup 2017-11-12 22:30:09 +01:00
Fredrik Fornwall
0bdbf314ef Update gradle configuration 2017-11-07 04:25:48 +01:00
Fredrik Fornwall
2deb9899bd Update buildToolsVersion to 27.0.1 2017-11-07 04:13:26 +01:00
Fredrik Fornwall
694ccc38c4 Remove WakefulBroadcastReceiver usage 2017-11-04 23:38:49 +01:00
Fredrik Fornwall
c949940374 Update support deps 2017-11-02 01:56:42 +01:00
Fredrik Fornwall
d09f70de1e Use a notification channel on Android O 2017-11-01 21:15:41 +01:00
Fredrik Fornwall
ac338ce2c5 Update gradle to 4.3 2017-10-31 19:07:00 +01:00
Fredrik Fornwall
092a83a688 Validate file path in TermuxOpenReceiver 2017-10-28 16:19:58 +02:00
Fredrik Fornwall
3533b13de8 Get rid of two android studio warnings 2017-10-28 11:05:43 +02:00
Fredrik Fornwall
d68a0f05be Update gradle configuration 2017-10-28 11:05:43 +02:00
Fredrik Fornwall
9e5293a08e Do not scroll when below bottom margin 2017-10-28 11:05:43 +02:00
Fredrik Fornwall
3f04a0a0d5 Bump build tools version in .travis.yml 2017-10-28 11:05:43 +02:00
Fredrik Fornwall
e558f824c5 Update gradle configuration 2017-10-28 11:05:43 +02:00
Auxilus
8ff72ddd8b replace help url with wiki url
HELP page wiki changed to https://wiki.termux.com/wiki/Main_Page as it now has more info than http://termux.com/help.html
2017-09-26 18:14:19 +02:00
Fredrik Fornwall
3e05356a69 Merge pull request #421 from sdrausty/master
Added two important links to README.md
2017-09-20 22:37:49 +02:00
Fredrik Fornwall
82673d3f82 Update gradle configuration 2017-09-17 11:47:12 +02:00
Fredrik Fornwall
f2757fdb76 Merge pull request #424 from EdwardBetts/patch-1
correct spelling mistake
2017-09-15 00:21:47 +02:00
Edward Betts
81fb669d0e correct spelling mistake 2017-09-13 16:16:08 +01:00
S D Rausty
4a45b1b617 Commit on 20170911 at 13:23:22 and at 1505150602 in Unix time. 2017-09-11 13:23:22 -04:00
S D Rausty
62b08b3cf1 Merge pull request #1 from termux/master
Hello
2017-09-10 13:24:32 -04:00
Fredrik Fornwall
1e83ea3151 Merge pull request #412 from Auxilus/patch-2
Added wiki link
2017-09-06 16:26:54 +02:00
Auxilus
9c42fdb3d6 Added wiki link 2017-09-05 10:43:30 +05:30
Fredrik Fornwall
42e3163e92 Update gradle to 4.1 2017-08-24 20:49:43 +02:00
Fredrik Fornwall
68912139f6 Additional REP test 2017-08-23 23:29:59 +02:00
Fredrik Fornwall
edc92bbcbb Start supporting the REP escape sequence 2017-08-13 16:14:31 +02:00
Fredrik Fornwall
d7520642de Update gradle 2017-07-29 03:05:48 +02:00
Fredrik Fornwall
adae111d5c Update gradle config 2017-07-09 21:07:18 +02:00
Fredrik Fornwall
dc9272790b Merge pull request #301 from kzlin129/android-things-auto-launch
[Android-things] Launch TermuxActivity automatically on boot
2017-06-26 02:04:03 +02:00
Fredrik Fornwall
05ea2ea238 Bump version to 0.53 2017-06-26 01:30:08 +02:00
Fredrik Fornwall
ad293562bc Fix possible crash in the copy/paste/more dialog 2017-06-26 01:29:05 +02:00
Fredrik Fornwall
f9a565d1e0 Ignore C1 control codes
C1 control codes are not used nowadays and just risks messing up
the terminal when they are used by accident.
2017-06-26 01:28:18 +02:00
Will Lin
5b3909cb73 Update AndroidManifest.xml with expanded comment 2017-06-17 15:39:23 +08:00
Fredrik Fornwall
7913e765d5 Bump version to 0.52 2017-06-13 16:44:33 +02:00
Fredrik Fornwall
ed3a3269d8 Merge pull request #339 from bumper314/master
Add Multi Window support for Samsung devices
2017-06-13 10:13:26 +02:00
Steven Audette
dc3994d2cf Add Multi Window support for Samsung devices
A single meta-data entry is enough to make Termux "Multi Window" aware on supported Samsung devices.
Tested and confirmed working on Samsung Note Pro 12.2 running Android 5.1.1.

This addition should not interfer with Android 6+'s split screen feature.
Tested and confirmed split screen functions as normal on a Nexus 6 running Android 7.0.

This addition should go unnoticed on non-Smasung Android 5 devices.
Tested and confirmed no functionality visible on Nexus 5 running Android 5.1.
2017-06-12 01:47:27 -06:00
Fredrik Fornwall
c972377bd1 Update gradle config 2017-06-11 22:47:59 +02:00
Fredrik Fornwall
d4be782c03 Bump version to 0.51 2017-06-05 22:49:11 +02:00
Fredrik Fornwall
c29909726c Bump terminal-emulator libraryVersion to 0.50 2017-06-05 22:45:44 +02:00
Fredrik Fornwall
45bac89298 Remove unused xmlns:android declaration 2017-06-05 22:45:15 +02:00
Fredrik Fornwall
c06770c353 Merge pull request #330 from whydoubt/cursor_preserve_color
Make cursor save/restore affect color attributes
2017-06-05 22:35:39 +02:00
Fredrik Fornwall
52a627efc8 Merge pull request #331 from whydoubt/fix_parameter_fc
Fix FC from too many control code parameters
2017-06-05 22:35:32 +02:00
Jeff Smith
2e9383720c Make cursor save/restore affect color attributes
SGR attributes are stored in three variables: mEffect, mForeColor, and
mBackColor.  Saving/restoring the cursor only preserves mEffect.

Change the cursor save/restore methods to additionally preserve
mForeColor and mBackColor.  This affects both 'explit' saving/restoring
the cursor and switching to/from the alternate screen buffer.
2017-06-03 17:45:36 -05:00
Jeff Smith
58f9f1be71 Add tests for cursor save/restore 2017-06-03 17:45:36 -05:00
Jeff Smith
058441dda6 Fix FC from too many control code parameters
When the number of parameters in a CSI control code exceeds the size of
the mArgs array, the code may attempt to read past the end of the array,
resulting in a force close of the app.

Stop the index from moving past the last element of the mArgs array.
2017-06-03 00:15:09 -05:00
Jeff Smith
888802a519 Add test for CSI followed by many parameters 2017-06-03 00:14:20 -05:00
Fredrik Fornwall
1a09b6d2a6 Change android:extractNativeLibs to false
We're trying this to see if that fixes an F-Droid build issue.

See #319.
2017-05-18 13:25:41 +02:00
Fredrik Fornwall
9d3f7734a1 Update android build tools 2017-05-16 11:11:57 +02:00
Fredrik Fornwall
61f766b59f Bump build tools in .travis.yml 2017-05-16 00:21:15 +02:00
Fredrik Fornwall
3f08376881 Bump version to 0.49 2017-05-15 23:42:55 +02:00
Fredrik Fornwall
cf06e70429 Update gradle configuration 2017-05-15 23:42:30 +02:00
Fredrik Fornwall
0714e435cb Do not show a Toast on clipboard text
Fixes https://github.com/termux/termux-app/issues/149.
2017-05-15 23:42:13 +02:00
Fredrik Fornwall
c26315185f Export TMPDIR to $PREFIX/tmp
Fixes https://github.com/termux/termux-packages/issues/1010.
Fixes https://github.com/termux/termux-app/issues/306.
2017-05-15 23:40:55 +02:00
Will Lin
1be5139253 [Android-things] Launch TermuxActivity automatically on boot 2017-04-18 20:13:47 +08:00
Fredrik Fornwall
adc43c40c5 Perform haptic feedback on extra keys
Fixes #269.
2017-04-16 12:18:13 +02:00
Fredrik Fornwall
779b1ca1f8 Update the Android gradle plugin to 2.3.1 2017-04-05 09:10:54 +02:00
Fredrik Fornwall
e375f99b0c Remove float/ module from .idea/gradle.xml 2017-04-05 09:09:48 +02:00
Fredrik Fornwall
0ac55cd77a Remove float/
The Termux:Float app has a new home at:

https://github.com/termux/termux-float
2017-04-04 23:39:05 +02:00
Fredrik Fornwall
4aefa5049b Add gradle setup to publish to jcenter 2017-04-04 23:31:00 +02:00
Fredrik Fornwall
5b14124258 Make GestureAndScaleRecognizer non-public 2017-04-04 20:56:19 +02:00
Fredrik Fornwall
1a9c38374c Fix some invalid html in javadoc 2017-04-02 15:11:56 +02:00
Fredrik Fornwall
eefd504f08 Use getLocationOnScreen() to calculate position
This avoids problem when the window has been prevented from scrolling
outside of the screen, or when the touch keyboard has made it move.
2017-04-02 14:46:47 +02:00
Fredrik Fornwall
19f838e3d1 Show window if launched when hidden 2017-04-02 14:46:00 +02:00
Fredrik Fornwall
63adb2b132 Rename TerminalKeyListener to TerminalViewClient 2017-04-02 14:25:34 +02:00
Fredrik Fornwall
7d3a988d2f Restructure library modules
terminal/ -> terminal-emulator/
view/ -> terminal-view/
2017-04-02 14:21:36 +02:00
Fredrik Fornwall
5cd705d6d1 Reformat all code in float/, getting rid of tabs 2017-04-02 14:10:26 +02:00
Fredrik Fornwall
af8b269b51 Work on Termux:Float input handling 2017-04-02 14:09:43 +02:00
Fredrik Fornwall
8c220eaaea Do not call long press listener when scaling 2017-04-02 14:07:19 +02:00
Fredrik Fornwall
0142e1ed0e Update Termux:Float launcher icon 2017-04-02 07:33:06 +02:00
Fredrik Fornwall
ac0a349ed9 Remove versionCode&versionName from libraries 2017-04-02 07:12:03 +02:00
Fredrik Fornwall
548b0916b6 Clean up {terminal,view}/build.gradle 2017-04-02 06:58:59 +02:00
Fredrik Fornwall
009de5a3ee Split up into modules and add float module
Split the app/ module into three modules

terminal/ - Terminal emulator library module.
view/ - Terminal view library module (depending on terminal/).
app/ - The main Termux app (depending on view/).

Also add the

float/ - The Termux:Float app (depending on view/).
2017-04-01 19:06:02 +02:00
Fredrik Fornwall
41d0d60017 Respect content type termux-open for url:s 2017-03-28 23:52:52 +02:00
Fredrik Fornwall
12ac0fa73c Add android:extractNativeLibs="false" to manifest 2017-03-12 20:55:58 +01:00
Fredrik Fornwall
835dfc0276 Avoid synthetic accessor method 2017-03-06 01:53:09 +01:00
Fredrik Fornwall
f60316835f Fix some android studio lint warnings 2017-03-06 01:47:08 +01:00
Fredrik Fornwall
f74a091be6 Remove useless continue statement 2017-03-06 01:44:16 +01:00
Fredrik Fornwall
dd6cb5221d Work around Android < 7.0 wifi manager leak
http://tools.android.com/tech-docs/lint-in-studio-2-3#TOC-WifiManager-Leak
2017-03-06 01:42:33 +01:00
Fredrik Fornwall
cab6df5c0e Update gradle config 2017-03-06 01:41:12 +01:00
122 changed files with 2126 additions and 1348 deletions

20
.cirrus.yml Normal file
View File

@@ -0,0 +1,20 @@
container:
image: cirrusci/android-sdk:28
cpu: 2
memory: 8G
task:
name: tests
script: ./gradlew test
task:
name: debug-build
depends_on:
- tests
script: |
./gradlew assembleDebug
output_artifacts:
path: "./app/build/outputs/apk/debug/*.apk"

View File

@@ -14,3 +14,7 @@ insert_final_newline = true
charset = utf-8 charset = utf-8
indent_style = space indent_style = space
indent_size = 4 indent_size = 4
[*.y{a,}ml]
indent_size = 2
indent_style = space

3
.gitattributes vendored Normal file
View File

@@ -0,0 +1,3 @@
* text=auto
*.bat eol=crlf
*.sh eol=lf

20
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,20 @@
---
name: Bug report
about: Create a report to help us improve termux-app
---
<!-- Important note: Refusing to provide needed information may result in issue closing. If you are having problems with a package in termux then a bug report should be filed in the termux-packages repo: https://github.com/termux/termux-packages -->
**Problem description**
A clear and concise description of what the problem with the termux app is. You may post screenshots in addition to description.
**Steps to reproduce**
Please post all steps that are needed to reproduce the issue.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Additional information**
Post output of command `termux-info`.
If you are rooted or have access to adb then capture a logcat with `logcat -d "*:W"`, from a adb or root shell.

View File

@@ -0,0 +1,12 @@
---
name: Feature request
about: Suggest a new feature in termux-app
---
**Feature description**
Describe the feature and why you want it.
**Reference implementation**
Does another app/terminal emulator have this feature?
Provide links to more background information

18
.gitignore vendored
View File

@@ -7,6 +7,8 @@ build/
*.apk *.apk
*.so *.so
.externalNativeBuild .externalNativeBuild
.cxx
*.zip
# Crashlytics configuations # Crashlytics configuations
com_crashlytics_export_strings.xml com_crashlytics_export_strings.xml
@@ -20,19 +22,8 @@ local.properties
# Signing files # Signing files
.signing/ .signing/
# User-specific configurations # Intellij
.idea/libraries/ .idea/
.idea/workspace.xml
.idea/tasks.xml
.idea/.name
.idea/compiler.xml
.idea/copyright/profiles_settings.xml
.idea/encodings.xml
.idea/misc.xml
.idea/modules.xml
.idea/scopes/scope_settings.xml
.idea/vcs.xml
.idea/dictionaries/
*.iml *.iml
# OS-specific files # OS-specific files
@@ -41,5 +32,6 @@ local.properties
._* ._*
.Spotlight-V100 .Spotlight-V100
.Trashes .Trashes
.swp
ehthumbs.db ehthumbs.db
Thumbs.db Thumbs.db

View File

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

19
.idea/gradle.xml generated
View File

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

View File

@@ -1,71 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="AndroidLintLogConditional" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AndroidLintNegativeMargin" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AssignmentUsedAsCondition" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="DeprecatedAPI" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="DuplicateSwitchCase" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="EmptyStatementBody" enabled="false" level="WARNING" enabled_by_default="false">
<option name="m_reportEmptyBlocks" value="true" />
</inspection_tool>
<inspection_tool class="EndlessLoop" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="EqualityInConditionalOperator" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="Finalize" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreTrivialFinalizers" value="true" />
</inspection_tool>
<inspection_tool class="FinalizeNotProtected" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="FormatSpecifiers" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="FunctionImplicitDeclarationInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="HidesUpperScope" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="HidingNonVirtualFunction" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="ImplicitIntegerAndEnumConversion" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="ImplicitPointerAndIntegerConversion" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="IncompatibleEnums" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="IncompatibleInitializers" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="IncompatiblePointers" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="InstanceofChain" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreInstanceofOnLibraryClasses" value="false" />
</inspection_tool>
<inspection_tool class="KRUnspecifiedParameters" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="LocalValueEscapesScope" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="LoggerInitializedWithForeignClass" enabled="false" level="WARNING" enabled_by_default="false">
<option name="loggerClassName" value="org.apache.log4j.Logger,org.slf4j.LoggerFactory,org.apache.commons.logging.LogFactory,java.util.logging.Logger" />
<option name="loggerFactoryMethodName" value="getLogger,getLogger,getLog,getLogger" />
</inspection_tool>
<inspection_tool class="MissingReturn" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="MissingSwitchCase" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="NotImplementedFunctions" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="NotInitializedVariable" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="NotSuperclass" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="OCDFAInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="OCLoopDoesntUseConditionVariableInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="OCSimplifyInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="OCUnusedGlobalDeclarationInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="OCUnusedMacroInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="OCUnusedStructInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="OCUnusedTemplateParameterInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="OnDemandImport" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="PrivateMemberAccessBetweenOuterAndInnerClass" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ResourceNotFoundInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SamePackageImport" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SignednessMismatch" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" />
<option name="processLiterals" value="true" />
<option name="processComments" value="true" />
</inspection_tool>
<inspection_tool class="UnnecessaryFullyQualifiedName" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_ignoreJavadoc" value="false" />
</inspection_tool>
<inspection_tool class="UnreachableCode" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="UnusedExpressionResult" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="UnusedImportStatement" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="UnusedLocalVariable" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="UnusedLocalization" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="UnusedParameter" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="UnusedValue" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="ValueMayNotFitIntoReceiver" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="VariableNotUsedInsideIf" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

View File

@@ -1,7 +0,0 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="PROJECT_PROFILE" value="Project Default" />
<option name="USE_PROJECT_PROFILE" value="true" />
<version value="1.0" />
</settings>
</component>

View File

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

View File

@@ -1,34 +0,0 @@
sudo: false
language: android
jdk: oraclejdk8
env:
global:
# The next declaration is the encrypted COVERITY_SCAN_TOKEN, created
# via the "travis encrypt" command using the project repo's public key
- secure: "LdajbHNfRlpnqzhX5KY2Vr7KtzU9vXDs1TCNn93J6Dt522f2AaiyUDJvISvz+uslk0WJiS5bB5vGwQmXginxz6Qi6uMgMbjWXulv1vfs6ZviKpUX348DOp1qKPa8WfVNB66F84SwGIfc8cRMAgCFw79l/DFgLErubF8vKo1wZ8Hmvrz//+RJ0BGMa3YRc4VyJhAL0P+0Wc1Q2Im7R9EovAxC5pZXBIMSgr6g5GzLWPisbNLXpMPGsDeYhcenO6XCtCCy+aNxUYM8vcrLDzlVXR5Hy7KEs/MGRTS0Yk13TWUEYa5wBpKelFTszdWYLVn5ANreh/aXRVfHpnW3epotMYguLx1kSvOhWEnc4F+qqv3nle2LpDg9Y9bcLyTTcYnPl9smqEVVjEDu0FoIr1V58xkG4Oc6BPIvLRjlMVU96PXh2HxMLuGsJ/xM+uAFU9oVMbC07xn42Eu5O4NHOHJNOwMWac4/lSKRK8W/7/vWuXj5vhkD9ZsGVpN70UtY5HAfNUGADnTeDblvjgFTNZ2mUN/u0o7Z8ZFURYllZ9YU+Vr2nPf9CAhVBjuwFWx8uRQpAg1aDmc1dVMJijRBeBeU/uWhYqsGp34wkNEl8VGzob4R4QTyI8+T7CndGqKVmbTK/SjqKhjjPpbXIAfOH+JtxvAnNmb8XeQSJ32uK2nexFo="
android:
components:
- platform-tools
- tools
- build-tools-25.0.2
- android-25
- extra-android-m2repository
before_install:
- git clone https://github.com/urho3d/android-ndk.git $HOME/android-ndk
- export ANDROID_NDK_HOME=$HOME/android-ndk
script:
- ./gradlew testDebugUnitTest
addons:
coverity_scan:
project:
name: "termux/termux-app"
description: "Terminal emulator and Linux environment for Android"
notification_email: fredrik@fornwall.net
build_command_prepend: "./gradlew clean"
build_command: "./gradlew build"
branch_pattern: master

3
LICENSE.md Normal file
View File

@@ -0,0 +1,3 @@
Released under [the GPLv3 license](https://www.gnu.org/licenses/gpl.html).
Contains code from `Terminal Emulator for Android` by which is released under [the Apache License 2.0](https://www.apache.org/licenses/).

View File

@@ -1,33 +1,63 @@
Termux app # Termux application
==========
[![Travis build status](https://travis-ci.org/termux/termux-app.svg?branch=master)](https://travis-ci.org/termux/termux-app) [![Build status](https://api.cirrus-ci.com/github/termux/termux-app.svg?branch=master)](https://cirrus-ci.com/termux/termux-app)
[![Join the chat at https://gitter.im/termux/termux](https://badges.gitter.im/termux/termux.svg)](https://gitter.im/termux/termux) [![Join the chat at https://gitter.im/termux/termux](https://badges.gitter.im/termux/termux.svg)](https://gitter.im/termux/termux)
[Termux](https://termux.com) is an Android terminal app and Linux environment. [Termux](https://termux.com) is an Android terminal application and Linux environment.
* [Termux on Google Play Store](https://play.google.com/store/apps/details?id=com.termux) - [Termux Reddit community](https://reddit.com/r/termux)
* [Termux on F-Droid](https://f-droid.org/repository/browse/?fdid=com.termux) - [Termux Wiki](https://wiki.termux.com/wiki/)
* [Termux Help](http://termux.com/help/) - [Termux Twitter](http://twitter.com/termux/)
* [Termux Google+ community](http://termux.com/community/)
Note that this repository is for the app itself (the user interface and the terminal emulation). For the packages installable inside the app, see [termux/termux-packages](https://github.com/termux/termux-packages) Note that this repository is for the app itself (the user interface and the
terminal emulation). For the packages installable inside the app, see
[termux/termux-packages](https://github.com/termux/termux-packages)
License ## Installation
=======
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/).
Terminal resources Termux:Widget application can be obtained from:
==================
* [XTerm control sequences](http://invisible-island.net/xterm/ctlseqs/ctlseqs.html)
* [vt100.net](http://vt100.net/)
* [Terminal codes (ANSI and terminfo equivalents)](http://wiki.bash-hackers.org/scripting/terminalcodes)
Terminal emulators - [Google Play](https://play.google.com/store/apps/details?id=com.termux)
================== - [F-Droid](https://f-droid.org/en/packages/com.termux/)
* 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). - [Kali Nethunter Store](https://store.nethunter.com/en/packages/com.termux/)
* 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). Additionally we offer development builds for those who want to try out latest
* 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). features ready to be included in future versions. Such build can be obtained
* xterm: The grandfather of terminal emulators. [Source](http://invisible-island.net/datafiles/release/xterm.tar.gz). directly from [Cirrus CI artifacts](https://api.cirrus-ci.com/v1/artifact/github/termux/termux-app/debug-build/output/app/build/outputs/apk/debug/app-debug.apk).
* 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). Signature keys of all offered builds are different. Before you switch the
installation source, you will have to uninstall the Termux application and
all currently installed plugins.
## Terminal resources
- [XTerm control sequences](http://invisible-island.net/xterm/ctlseqs/ctlseqs.html)
- [vt100.net](http://vt100.net/)
- [Terminal codes (ANSI and terminfo equivalents)](http://wiki.bash-hackers.org/scripting/terminalcodes)
## Terminal emulators
- VTE (libvte): Terminal emulator widget for GTK+, mainly used in gnome-terminal.
[Source](https://github.com/GNOME/vte), [Open Issues](https://bugzilla.gnome.org/buglist.cgi?quicksearch=product%3A%22vte%22+),
and [All (including closed) issues](https://bugzilla.gnome.org/buglist.cgi?bug_status=RESOLVED&bug_status=VERIFIED&chfield=resolution&chfieldfrom=-2000d&chfieldvalue=FIXED&product=vte&resolution=FIXED).
- iTerm 2: 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).
- 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

@@ -1,20 +1,23 @@
apply plugin: 'com.android.application' plugins {
id "com.android.application"
}
android { android {
compileSdkVersion 25 compileSdkVersion 28
buildToolsVersion "25.0.2"
dependencies { dependencies {
compile 'com.android.support:support-annotations:25.1.0' implementation "androidx.annotation:annotation:1.0.1"
compile "com.android.support:support-v4:25.1.0" implementation "androidx.viewpager:viewpager:1.0.0"
implementation "androidx.drawerlayout:drawerlayout:1.0.0"
implementation project(":terminal-view")
} }
defaultConfig { defaultConfig {
applicationId "com.termux" applicationId "com.termux"
minSdkVersion 21 minSdkVersion 24
targetSdkVersion 25 targetSdkVersion 28
versionCode 48 versionCode 77
versionName "0.48" versionName "0.77"
externalNativeBuild { externalNativeBuild {
ndkBuild { ndkBuild {
@@ -25,6 +28,16 @@ android {
ndk { ndk {
abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a' abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
} }
}
signingConfigs {
debug {
storeFile file('dev_keystore.jks')
keyAlias 'alias'
storePassword 'xrj45yWGLbsO7W0v'
keyPassword 'xrj45yWGLbsO7W0v'
}
} }
buildTypes { buildTypes {
@@ -33,15 +46,96 @@ android {
shrinkResources true shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
} }
debug {
signingConfig signingConfigs.debug
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
} }
externalNativeBuild { externalNativeBuild {
ndkBuild { ndkBuild {
path "src/main/jni/Android.mk" path "src/main/cpp/Android.mk"
} }
} }
} }
dependencies { dependencies {
testCompile 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
} }
task versionName {
doLast {
print android.defaultConfig.versionName
}
}
def downloadBootstrap(String arch, String expectedChecksum, int version) {
def digest = java.security.MessageDigest.getInstance("SHA-256")
def localUrl = "src/main/cpp/bootstrap-" + arch + ".zip"
def file = new File(projectDir, localUrl)
if (file.exists()) {
def buffer = new byte[8192]
def input = new FileInputStream(file)
while (true) {
def readBytes = input.read(buffer)
if (readBytes < 0) break
digest.update(buffer, 0, readBytes)
}
def checksum = new BigInteger(1, digest.digest()).toString(16)
if (checksum == expectedChecksum) {
return
} else {
logger.quiet("Deleting old local file with wrong hash: " + localUrl)
file.delete()
}
}
def remoteUrl = "https://bintray.com/termux/bootstrap/download_file?file_path=bootstrap-" + arch + "-v" + version + ".zip"
logger.quiet("Downloading " + remoteUrl + " ...")
file.parentFile.mkdirs()
def out = new BufferedOutputStream(new FileOutputStream(file))
def connection = new URL(remoteUrl).openConnection()
connection.setInstanceFollowRedirects(true)
def digestStream = new java.security.DigestInputStream(connection.inputStream, digest)
out << digestStream
out.close()
def checksum = new BigInteger(1, digest.digest()).toString(16)
if (checksum != expectedChecksum) {
file.delete()
throw new GradleException("Wrong checksum for " + remoteUrl + ": expected: " + expectedChecksum + ", actual: " + checksum)
}
}
clean {
doLast {
def tree = fileTree(new File(projectDir, 'src/main/cpp'))
tree.include 'bootstrap-*.zip'
tree.each { it.delete() }
}
}
task downloadBootstraps(){
doLast {
def version = 18
downloadBootstrap("aarch64", "1a4c08a696d452b58f69102428239ec0c07521c0ca9f48b23ef70ae0e5e3d4f8", version)
downloadBootstrap("arm", "bff11f2c7e9c1055a22fc5f20bb7507b75f6034e0f5d591ec6725b3407981b85", version)
downloadBootstrap("i686", "6fb93020db2807337d82a1537e24612400cacbd10cf4bccaeb0714d51e653da1", version)
downloadBootstrap("x86_64", "a6067e5decc486dcad190c1ed9e15366c798e5e7d9b9b9ee6b4b8231290524c3", version)
}
}
afterEvaluate {
android.applicationVariants.all { variant ->
variant.javaCompiler.dependsOn(downloadBootstraps)
}
}

BIN
app/dev_keystore.jks Normal file

Binary file not shown.

View File

@@ -7,11 +7,5 @@
# For more details, see # For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html # http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here: -renamesourcefileattribute SourceFile
-keepattributes SourceFile,LineNumberTable
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

View File

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

View File

@@ -1,4 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.termux" package="com.termux"
android:installLocation="internalOnly" android:installLocation="internalOnly"
android:sharedUserId="com.termux" android:sharedUserId="com.termux"
@@ -7,25 +8,33 @@
<uses-feature android:name="android.hardware.touchscreen" android:required="false" /> <uses-feature android:name="android.hardware.touchscreen" android:required="false" />
<uses-feature android:name="android.software.leanback" android:required="false" /> <uses-feature android:name="android.software.leanback" android:required="false" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission-sdk-23 android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<application <application
android:allowBackup="true" android:extractNativeLibs="true"
android:fullBackupContent="@xml/backupscheme" android:allowBackup="false"
android:icon="@mipmap/ic_launcher" android:icon="@drawable/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:banner="@drawable/banner" android:banner="@drawable/banner"
android:label="@string/application_name" android:label="@string/application_name"
android:theme="@style/Theme.Termux" android:theme="@style/Theme.Termux"
android:supportsRtl="false" > android:supportsRtl="false" >
<!-- This (or rather, value 2.1 or higher) is needed to make the Samsung Galaxy S8
mark the app with "This app is optimized to run in full screen." -->
<meta-data android:name="android.max_aspect" android:value="10.0" />
<activity <activity
android:name="com.termux.app.TermuxActivity" android:name="com.termux.app.TermuxActivity"
android:label="@string/application_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize" android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:launchMode="singleTask" android:launchMode="singleTask"
android:resizeableActivity="true"
android:windowSoftInputMode="adjustResize|stateAlwaysVisible" > android:windowSoftInputMode="adjustResize|stateAlwaysVisible" >
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@@ -43,6 +52,7 @@
android:exported="false" android:exported="false"
android:theme="@android:style/Theme.Material.Light.DarkActionBar" android:theme="@android:style/Theme.Material.Light.DarkActionBar"
android:parentActivityName=".app.TermuxActivity" android:parentActivityName=".app.TermuxActivity"
android:resizeableActivity="true"
android:label="@string/application_name" /> android:label="@string/application_name" />
<activity <activity
@@ -50,6 +60,7 @@
android:label="@string/application_name" android:label="@string/application_name"
android:taskAffinity="com.termux.filereceiver" android:taskAffinity="com.termux.filereceiver"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:resizeableActivity="true"
android:noHistory="true"> android:noHistory="true">
<!-- Accept multiple file types when sending. --> <!-- Accept multiple file types when sending. -->
<intent-filter> <intent-filter>
@@ -64,7 +75,7 @@
<data android:mimeType="video/*" /> <data android:mimeType="video/*" />
</intent-filter> </intent-filter>
<!-- Be more restrictive for viewing files, restricting ourselves to text files. --> <!-- Be more restrictive for viewing files, restricting ourselves to text files. -->
<intent-filter> <intent-filter tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.VIEW"/> <action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/*" /> <data android:mimeType="text/*" />
@@ -75,6 +86,18 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity-alias
android:name=".HomeActivity"
android:targetActivity="com.termux.app.TermuxActivity">
<!-- Launch activity automatically on boot on Android Things devices -->
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.IOT_LAUNCHER"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity-alias>
<provider <provider
android:name=".filepicker.TermuxDocumentsProvider" android:name=".filepicker.TermuxDocumentsProvider"
android:authorities="com.termux.documents" android:authorities="com.termux.documents"
@@ -98,6 +121,7 @@
android:grantUriPermissions="true" android:grantUriPermissions="true"
android:name="com.termux.app.TermuxOpenReceiver$ContentProvider" /> android:name="com.termux.app.TermuxOpenReceiver$ContentProvider" />
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
</application> </application>
</manifest> </manifest>

View File

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

View File

@@ -0,0 +1,18 @@
.global blob
.global blob_size
.section .rodata
blob:
#if defined __i686__
.incbin "bootstrap-i686.zip"
#elif defined __x86_64__
.incbin "bootstrap-x86_64.zip"
#elif defined __aarch64__
.incbin "bootstrap-aarch64.zip"
#elif defined __arm__
.incbin "bootstrap-arm.zip"
#else
# error Unsupported arch
#endif
1:
blob_size:
.int 1b - blob

View File

@@ -0,0 +1,11 @@
#include <jni.h>
extern jbyte blob[];
extern int blob_size;
JNIEXPORT jbyteArray JNICALL Java_com_termux_app_TermuxInstaller_getZip(JNIEnv *env, __attribute__((__unused__)) jobject This)
{
jbyteArray ret = (*env)->NewByteArray(env, blob_size);
(*env)->SetByteArrayRegion(env, ret, 0, blob_size, blob);
return ret;
}

View File

@@ -93,32 +93,60 @@ public final class BackgroundJob {
}; };
} }
public static String[] buildEnvironment(boolean failSafe, String cwd) { private static void addToEnvIfPresent(List<String> environment, String name) {
String value = System.getenv(name);
if (value != null) {
environment.add(name + "=" + value);
}
}
static String[] buildEnvironment(boolean failSafe, String cwd) {
new File(TermuxService.HOME_PATH).mkdirs(); new File(TermuxService.HOME_PATH).mkdirs();
if (cwd == null) cwd = TermuxService.HOME_PATH; if (cwd == null) cwd = TermuxService.HOME_PATH;
final String termEnv = "TERM=xterm-256color"; List<String> environment = new ArrayList<>();
final String homeEnv = "HOME=" + TermuxService.HOME_PATH;
final String prefixEnv = "PREFIX=" + TermuxService.PREFIX_PATH; environment.add("TERM=xterm-256color");
final String androidRootEnv = "ANDROID_ROOT=" + System.getenv("ANDROID_ROOT"); environment.add("HOME=" + TermuxService.HOME_PATH);
final String androidDataEnv = "ANDROID_DATA=" + System.getenv("ANDROID_DATA"); environment.add("PREFIX=" + TermuxService.PREFIX_PATH);
environment.add("BOOTCLASSPATH" + System.getenv("BOOTCLASSPATH"));
environment.add("ANDROID_ROOT=" + System.getenv("ANDROID_ROOT"));
environment.add("ANDROID_DATA=" + System.getenv("ANDROID_DATA"));
// EXTERNAL_STORAGE is needed for /system/bin/am to work on at least // EXTERNAL_STORAGE is needed for /system/bin/am to work on at least
// Samsung S7 - see https://plus.google.com/110070148244138185604/posts/gp8Lk3aCGp3. // Samsung S7 - see https://plus.google.com/110070148244138185604/posts/gp8Lk3aCGp3.
final String externalStorageEnv = "EXTERNAL_STORAGE=" + System.getenv("EXTERNAL_STORAGE"); environment.add("EXTERNAL_STORAGE=" + System.getenv("EXTERNAL_STORAGE"));
// ANDROID_RUNTIME_ROOT and ANDROID_TZDATA_ROOT are required for `am` to run on Android Q
addToEnvIfPresent(environment, "ANDROID_RUNTIME_ROOT");
addToEnvIfPresent(environment, "ANDROID_TZDATA_ROOT");
if (failSafe) { if (failSafe) {
// Keep the default path so that system binaries can be used in the failsafe session. // Keep the default path so that system binaries can be used in the failsafe session.
final String pathEnv = "PATH=" + System.getenv("PATH"); environment.add("PATH= " + System.getenv("PATH"));
return new String[]{termEnv, homeEnv, prefixEnv, androidRootEnv, androidDataEnv, pathEnv, externalStorageEnv};
} else { } else {
final String ps1Env = "PS1=$ "; if (shouldAddLdLibraryPath()) {
final String ldEnv = "LD_LIBRARY_PATH=" + TermuxService.PREFIX_PATH + "/lib"; environment.add("LD_LIBRARY_PATH=" + TermuxService.PREFIX_PATH + "/lib");
final String langEnv = "LANG=en_US.UTF-8"; }
final String pathEnv = "PATH=" + TermuxService.PREFIX_PATH + "/bin:" + TermuxService.PREFIX_PATH + "/bin/applets"; environment.add("LANG=en_US.UTF-8");
final String pwdEnv = "PWD=" + cwd; environment.add("PATH=" + TermuxService.PREFIX_PATH + "/bin:" + TermuxService.PREFIX_PATH + "/bin/applets");
environment.add("PWD=" + cwd);
return new String[]{termEnv, homeEnv, prefixEnv, ps1Env, ldEnv, langEnv, pathEnv, pwdEnv, androidRootEnv, androidDataEnv, externalStorageEnv}; environment.add("TMPDIR=" + TermuxService.PREFIX_PATH + "/tmp");
} }
return environment.toArray(new String[0]);
}
private static boolean shouldAddLdLibraryPath() {
try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(TermuxService.PREFIX_PATH + "/etc/apt/sources.list")))) {
String line;
while ((line = in.readLine()) != null) {
if (!line.startsWith("#") && line.contains("//termux.net stable")) {
return true;
}
}
} catch (IOException e) {
Log.e(LOG_TAG, "Error trying to read sources.list", e);
}
return false;
} }
public static int getPid(Process p) { public static int getPid(Process p) {
@@ -158,7 +186,6 @@ public final class BackgroundJob {
if (c == ' ' || c == '\n') { if (c == ' ' || c == '\n') {
if (builder.length() == 0) { if (builder.length() == 0) {
// Skip whitespace after shebang. // Skip whitespace after shebang.
continue;
} else { } else {
// End of shebang. // End of shebang.
String executable = builder.toString(); String executable = builder.toString();
@@ -187,7 +214,7 @@ public final class BackgroundJob {
if (interpreter != null) result.add(interpreter); if (interpreter != null) result.add(interpreter);
result.add(fileToExecute); result.add(fileToExecute);
if (args != null) Collections.addAll(result, args); if (args != null) Collections.addAll(result, args);
return result.toArray(new String[result.size()]); return result.toArray(new String[0]);
} }
} }

View File

@@ -0,0 +1,63 @@
package com.termux.app;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.os.Vibrator;
public class BellUtil {
private static BellUtil instance = null;
private static final Object lock = new Object();
public static BellUtil getInstance(Context context) {
if (instance == null) {
synchronized (lock) {
if (instance == null) {
instance = new BellUtil((Vibrator) context.getApplicationContext().getSystemService(Context.VIBRATOR_SERVICE));
}
}
}
return instance;
}
private static final long DURATION = 50;
private static final long MIN_PAUSE = 3 * DURATION;
private final Handler handler = new Handler(Looper.getMainLooper());
private long lastBell = 0;
private final Runnable bellRunnable;
private BellUtil(final Vibrator vibrator) {
bellRunnable = new Runnable() {
@Override
public void run() {
if (vibrator != null) {
vibrator.vibrate(DURATION);
}
}
};
}
public synchronized void doBell() {
long now = now();
long timeSinceLastBell = now - lastBell;
if (timeSinceLastBell < 0) {
// there is a next bell pending; don't schedule another one
} else if (timeSinceLastBell < MIN_PAUSE) {
// there was a bell recently, scheudle the next one
handler.postDelayed(bellRunnable, MIN_PAUSE - timeSinceLastBell);
lastBell = lastBell + MIN_PAUSE;
} else {
// the last bell was long ago, do it now
bellRunnable.run();
lastBell = now;
}
}
private long now() {
return SystemClock.uptimeMillis();
}
}

View File

@@ -9,7 +9,6 @@ import android.view.KeyEvent;
import android.view.ViewGroup.LayoutParams; import android.view.ViewGroup.LayoutParams;
import android.widget.EditText; import android.widget.EditText;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.TextView;
public final class DialogUtils { public final class DialogUtils {
@@ -31,13 +30,10 @@ public final class DialogUtils {
final AlertDialog[] dialogHolder = new AlertDialog[1]; final AlertDialog[] dialogHolder = new AlertDialog[1];
input.setImeActionLabel(activity.getResources().getString(positiveButtonText), KeyEvent.KEYCODE_ENTER); input.setImeActionLabel(activity.getResources().getString(positiveButtonText), KeyEvent.KEYCODE_ENTER);
input.setOnEditorActionListener(new TextView.OnEditorActionListener() { input.setOnEditorActionListener((v, actionId, event) -> {
@Override onPositive.onTextSet(input.getText().toString());
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { dialogHolder[0].dismiss();
onPositive.onTextSet(input.getText().toString()); return true;
dialogHolder[0].dismiss();
return true;
}
}); });
float dipInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, activity.getResources().getDisplayMetrics()); float dipInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, activity.getResources().getDisplayMetrics());
@@ -53,31 +49,16 @@ public final class DialogUtils {
AlertDialog.Builder builder = new AlertDialog.Builder(activity) AlertDialog.Builder builder = new AlertDialog.Builder(activity)
.setTitle(titleText).setView(layout) .setTitle(titleText).setView(layout)
.setPositiveButton(positiveButtonText, new DialogInterface.OnClickListener() { .setPositiveButton(positiveButtonText, (d, whichButton) -> onPositive.onTextSet(input.getText().toString()));
@Override
public void onClick(DialogInterface d, int whichButton) {
onPositive.onTextSet(input.getText().toString());
}
});
if (onNeutral != null) { if (onNeutral != null) {
builder.setNeutralButton(neutralButtonText, new DialogInterface.OnClickListener() { builder.setNeutralButton(neutralButtonText, (dialog, which) -> onNeutral.onTextSet(input.getText().toString()));
@Override
public void onClick(DialogInterface dialog, int which) {
onNeutral.onTextSet(input.getText().toString());
}
});
} }
if (onNegative == null) { if (onNegative == null) {
builder.setNegativeButton(android.R.string.cancel, null); builder.setNegativeButton(android.R.string.cancel, null);
} else { } else {
builder.setNegativeButton(negativeButtonText, new DialogInterface.OnClickListener() { builder.setNegativeButton(negativeButtonText, (dialog, which) -> onNegative.onTextSet(input.getText().toString()));
@Override
public void onClick(DialogInterface dialog, int which) {
onNegative.onTextSet(input.getText().toString());
}
});
} }
if (onDismiss != null) builder.setOnDismissListener(onDismiss); if (onDismiss != null) builder.setOnDismissListener(onDismiss);

View File

@@ -1,12 +1,25 @@
package com.termux.app; package com.termux.app;
import android.content.Context; import android.content.Context;
import android.os.Build;
import android.provider.Settings;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.Gravity;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ScheduledExecutorService;
import java.util.Map;
import java.util.HashMap;
import java.util.Arrays;
import android.view.HapticFeedbackConstants;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.widget.Button; import android.widget.Button;
import android.widget.GridLayout; import android.widget.GridLayout;
import android.widget.PopupWindow;
import android.widget.ToggleButton; import android.widget.ToggleButton;
import com.termux.R; import com.termux.R;
@@ -20,152 +33,407 @@ import com.termux.view.TerminalView;
public final class ExtraKeysView extends GridLayout { public final class ExtraKeysView extends GridLayout {
private static final int TEXT_COLOR = 0xFFFFFFFF; private static final int TEXT_COLOR = 0xFFFFFFFF;
private static final int BUTTON_COLOR = 0x00000000;
private static final int INTERESTING_COLOR = 0xFF80DEEA;
private static final int BUTTON_PRESSED_COLOR = 0x7FFFFFFF;
public ExtraKeysView(Context context, AttributeSet attrs) { public ExtraKeysView(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
reload();
} }
/**
* HashMap that implements Python dict.get(key, default) function.
* Default java.util .get(key) is then the same as .get(key, null);
*/
static class CleverMap<K,V> extends HashMap<K,V> {
V get(K key, V defaultValue) {
if(containsKey(key))
return get(key);
else
return defaultValue;
}
}
static class CharDisplayMap extends CleverMap<String, String> {}
/**
* Keys are displayed in a natural looking way, like "→" for "RIGHT"
*/
static final Map<String, Integer> keyCodesForString = new HashMap<String, Integer>() {{
put("ESC", KeyEvent.KEYCODE_ESCAPE);
put("TAB", KeyEvent.KEYCODE_TAB);
put("HOME", KeyEvent.KEYCODE_MOVE_HOME);
put("END", KeyEvent.KEYCODE_MOVE_END);
put("PGUP", KeyEvent.KEYCODE_PAGE_UP);
put("PGDN", KeyEvent.KEYCODE_PAGE_DOWN);
put("INS", KeyEvent.KEYCODE_INSERT);
put("DEL", KeyEvent.KEYCODE_FORWARD_DEL);
put("BKSP", KeyEvent.KEYCODE_DEL);
put("UP", KeyEvent.KEYCODE_DPAD_UP);
put("LEFT", KeyEvent.KEYCODE_DPAD_LEFT);
put("RIGHT", KeyEvent.KEYCODE_DPAD_RIGHT);
put("DOWN", KeyEvent.KEYCODE_DPAD_DOWN);
put("ENTER", KeyEvent.KEYCODE_ENTER);
put("F1", KeyEvent.KEYCODE_F1);
put("F2", KeyEvent.KEYCODE_F2);
put("F3", KeyEvent.KEYCODE_F3);
put("F4", KeyEvent.KEYCODE_F4);
put("F5", KeyEvent.KEYCODE_F5);
put("F6", KeyEvent.KEYCODE_F6);
put("F7", KeyEvent.KEYCODE_F7);
put("F8", KeyEvent.KEYCODE_F8);
put("F9", KeyEvent.KEYCODE_F9);
put("F10", KeyEvent.KEYCODE_F10);
put("F11", KeyEvent.KEYCODE_F11);
put("F12", KeyEvent.KEYCODE_F12);
}};
static void sendKey(View view, String keyName) { static void sendKey(View view, String keyName) {
int keyCode = 0; TerminalView terminalView = view.findViewById(R.id.terminal_view);
String chars = null; if (keyCodesForString.containsKey(keyName)) {
switch (keyName) { int keyCode = keyCodesForString.get(keyName);
case "ESC": terminalView.onKeyDown(keyCode, new KeyEvent(KeyEvent.ACTION_UP, keyCode));
keyCode = KeyEvent.KEYCODE_ESCAPE; // view.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode));
break;
case "TAB":
keyCode = KeyEvent.KEYCODE_TAB;
break;
case "":
keyCode = KeyEvent.KEYCODE_DPAD_UP;
break;
case "":
keyCode = KeyEvent.KEYCODE_DPAD_LEFT;
break;
case "":
keyCode = KeyEvent.KEYCODE_DPAD_RIGHT;
break;
case "":
keyCode = KeyEvent.KEYCODE_DPAD_DOWN;
break;
case "":
chars = "-";
break;
default:
chars = keyName;
}
if (keyCode > 0) {
view.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
view.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode));
} else { } else {
TerminalView terminalView = (TerminalView) view.findViewById(R.id.terminal_view); // not a control char
TerminalSession session = terminalView.getCurrentSession(); TerminalSession session = terminalView.getCurrentSession();
if (session != null) session.write(chars); if (session != null && keyName.length() > 0)
session.write(keyName);
} }
} }
public enum SpecialButton {
CTRL, ALT, FN
}
private static class SpecialButtonState {
boolean isOn = false;
ToggleButton button = null;
}
private Map<SpecialButton, SpecialButtonState> specialButtons = new HashMap<SpecialButton, SpecialButtonState>() {{
put(SpecialButton.CTRL, new SpecialButtonState());
put(SpecialButton.ALT, new SpecialButtonState());
put(SpecialButton.FN, new SpecialButtonState());
}};
private ScheduledExecutorService scheduledExecutor;
private PopupWindow popupWindow;
private int longPressCount;
public boolean readSpecialButton(SpecialButton name) {
SpecialButtonState state = specialButtons.get(name);
if (state == null)
throw new RuntimeException("Must be a valid special button (see source)");
if (! state.isOn)
return false;
private ToggleButton controlButton; if (state.button == null) {
private ToggleButton altButton; return false;
private ToggleButton fnButton;
public boolean readControlButton() {
if (controlButton.isPressed()) return true;
boolean result = controlButton.isChecked();
if (result) {
controlButton.setChecked(false);
controlButton.setTextColor(TEXT_COLOR);
} }
return result;
if (state.button.isPressed())
return true;
if (! state.button.isChecked())
return false;
state.button.setChecked(false);
state.button.setTextColor(TEXT_COLOR);
return true;
} }
public boolean readAltButton() { void popup(View view, String text) {
if (altButton.isPressed()) return true; int width = view.getMeasuredWidth();
boolean result = altButton.isChecked(); int height = view.getMeasuredHeight();
if (result) { Button button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
altButton.setChecked(false); button.setText(text);
altButton.setTextColor(TEXT_COLOR); button.setTextColor(TEXT_COLOR);
} button.setPadding(0, 0, 0, 0);
return result; button.setMinHeight(0);
button.setMinWidth(0);
button.setMinimumWidth(0);
button.setMinimumHeight(0);
button.setWidth(width);
button.setHeight(height);
button.setBackgroundColor(BUTTON_PRESSED_COLOR);
popupWindow = new PopupWindow(this);
popupWindow.setWidth(LayoutParams.WRAP_CONTENT);
popupWindow.setHeight(LayoutParams.WRAP_CONTENT);
popupWindow.setContentView(button);
popupWindow.setOutsideTouchable(true);
popupWindow.setFocusable(false);
popupWindow.showAsDropDown(view, 0, -2 * height);
} }
static final CharDisplayMap classicArrowsDisplay = new CharDisplayMap() {{
// classic arrow keys (for ◀ ▶ ▲ ▼ @see arrowVariationDisplay)
put("LEFT", ""); // U+2190 ← LEFTWARDS ARROW
put("RIGHT", ""); // U+2192 → RIGHTWARDS ARROW
put("UP", ""); // U+2191 ↑ UPWARDS ARROW
put("DOWN", ""); // U+2193 ↓ DOWNWARDS ARROW
}};
public boolean readFnButton() { static final CharDisplayMap wellKnownCharactersDisplay = new CharDisplayMap() {{
if (fnButton.isPressed()) return true; // well known characters // https://en.wikipedia.org/wiki/{Enter_key, Tab_key, Delete_key}
boolean result = fnButton.isChecked(); put("ENTER", ""); // U+21B2 ↲ DOWNWARDS ARROW WITH TIP LEFTWARDS
if (result) { put("TAB", ""); // U+21B9 ↹ LEFTWARDS ARROW TO BAR OVER RIGHTWARDS ARROW TO BAR
fnButton.setChecked(false); put("BKSP", ""); // U+232B ⌫ ERASE TO THE LEFT sometimes seen and easy to understand
fnButton.setTextColor(TEXT_COLOR); put("DEL", ""); // U+2326 ⌦ ERASE TO THE RIGHT not well known but easy to understand
} }};
return result;
static final CharDisplayMap lessKnownCharactersDisplay = new CharDisplayMap() {{
// https://en.wikipedia.org/wiki/{Home_key, End_key, Page_Up_and_Page_Down_keys}
// home key can mean "goto the beginning of line" or "goto first page" depending on context, hence the diagonal
put("HOME", ""); // from IEC 9995 // U+21F1 ⇱ NORTH WEST ARROW TO CORNER
put("END", ""); // from IEC 9995 // ⇲ // U+21F2 ⇲ SOUTH EAST ARROW TO CORNER
put("PGUP", ""); // no ISO character exists, U+21D1 ⇑ UPWARDS DOUBLE ARROW will do the trick
put("PGDN", ""); // no ISO character exists, U+21D3 ⇓ DOWNWARDS DOUBLE ARROW will do the trick
}};
static final CharDisplayMap arrowTriangleVariationDisplay = new CharDisplayMap() {{
// alternative to classic arrow keys
put("LEFT", ""); // U+25C0 ◀ BLACK LEFT-POINTING TRIANGLE
put("RIGHT", ""); // U+25B6 ▶ BLACK RIGHT-POINTING TRIANGLE
put("UP", ""); // U+25B2 ▲ BLACK UP-POINTING TRIANGLE
put("DOWN", ""); // U+25BC ▼ BLACK DOWN-POINTING TRIANGLE
}};
static final CharDisplayMap notKnownIsoCharacters = new CharDisplayMap() {{
// Control chars that are more clear as text // https://en.wikipedia.org/wiki/{Function_key, Alt_key, Control_key, Esc_key}
// put("FN", "FN"); // no ISO character exists
put("CTRL", ""); // ISO character "U+2388 ⎈ HELM SYMBOL" is unknown to people and never printed on computers, however "U+25C7 ◇ WHITE DIAMOND" is a nice presentation, and "^" for terminal app and mac is often used
put("ALT", ""); // ISO character "U+2387 ⎇ ALTERNATIVE KEY SYMBOL'" is unknown to people and only printed as the Option key "⌥" on Mac computer
put("ESC", ""); // ISO character "U+238B ⎋ BROKEN CIRCLE WITH NORTHWEST ARROW" is unknown to people and not often printed on computers
}};
static final CharDisplayMap nicerLookingDisplay = new CharDisplayMap() {{
// nicer looking for most cases
put("-", ""); // U+2015 ― HORIZONTAL BAR
}};
/**
* Keys are displayed in a natural looking way, like "→" for "RIGHT" or "↲" for ENTER
*/
public static final CharDisplayMap defaultCharDisplay = new CharDisplayMap() {{
putAll(classicArrowsDisplay);
putAll(wellKnownCharactersDisplay);
putAll(nicerLookingDisplay);
// all other characters are displayed as themselves
}};
public static final CharDisplayMap lotsOfArrowsCharDisplay = new CharDisplayMap() {{
putAll(classicArrowsDisplay);
putAll(wellKnownCharactersDisplay);
putAll(lessKnownCharactersDisplay); // NEW
putAll(nicerLookingDisplay);
}};
public static final CharDisplayMap arrowsOnlyCharDisplay = new CharDisplayMap() {{
putAll(classicArrowsDisplay);
// putAll(wellKnownCharactersDisplay); // REMOVED
// putAll(lessKnownCharactersDisplay); // REMOVED
putAll(nicerLookingDisplay);
}};
public static final CharDisplayMap fullIsoCharDisplay = new CharDisplayMap() {{
putAll(classicArrowsDisplay);
putAll(wellKnownCharactersDisplay);
putAll(lessKnownCharactersDisplay); // NEW
putAll(nicerLookingDisplay);
putAll(notKnownIsoCharacters); // NEW
}};
/**
* Some people might call our keys differently
*/
static final CharDisplayMap controlCharsAliases = new CharDisplayMap() {{
put("ESCAPE", "ESC");
put("CONTROL", "CTRL");
put("RETURN", "ENTER"); // Technically different keys, but most applications won't see the difference
put("FUNCTION", "FN");
// no alias for ALT
// Directions are sometimes written as first and last letter for brevety
put("LT", "LEFT");
put("RT", "RIGHT");
put("DN", "DOWN");
// put("UP", "UP"); well, "UP" is already two letters
put("PAGEUP", "PGUP");
put("PAGE_UP", "PGUP");
put("PAGE UP", "PGUP");
put("PAGE-UP", "PGUP");
// no alias for HOME
// no alias for END
put("PAGEDOWN", "PGDN");
put("PAGE_DOWN", "PGDN");
put("PAGE-DOWN", "PGDN");
put("DELETE", "DEL");
put("BACKSPACE", "BKSP");
// easier for writing in termux.properties
put("BACKSLASH", "\\");
put("QUOTE", "\"");
put("APOSTROPHE", "'");
}};
/**
* Applies the 'controlCharsAliases' mapping to all the strings in *buttons*
* Modifies the array, doesn't return a new one.
*/
void replaceAliases(String[][] buttons) {
for(int i = 0; i < buttons.length; i++)
for(int j = 0; j < buttons[i].length; j++)
buttons[i][j] = controlCharsAliases.get(buttons[i][j], buttons[i][j]);
} }
void reload() { /**
altButton = controlButton = null; * General util function to compute the longest column length in a matrix.
*/
static int maximumLength(String[][] matrix) {
int m = 0;
for (String[] aMatrix : matrix) m = Math.max(m, aMatrix.length);
return m;
}
/**
* Reload the view given parameters in termux.properties
*
* @param buttons matrix of String as defined in termux.properties extrakeys
* Can Contain The Strings CTRL ALT TAB FN ENTER LEFT RIGHT UP DOWN or normal strings
* Some aliases are possible like RETURN for ENTER, LT for LEFT and more (@see controlCharsAliases for the whole list).
* Any string of length > 1 in total Uppercase will print a warning
*
* Examples:
* "ENTER" will trigger the ENTER keycode
* "LEFT" will trigger the LEFT keycode and be displayed as "←"
* "→" will input a "→" character
* "" will input a "" character
* "-_-" will input the string "-_-"
*/
void reload(String[][] buttons, CharDisplayMap charDisplayMap) {
for(SpecialButtonState state : specialButtons.values())
state.button = null;
removeAllViews(); removeAllViews();
String[][] buttons = { replaceAliases(buttons); // modifies the array
{"ESC", "CTRL", "ALT", "TAB", "", "/", "|"}
};
final int rows = buttons.length; final int rows = buttons.length;
final int cols = buttons[0].length; final int cols = maximumLength(buttons);
setRowCount(rows); setRowCount(rows);
setColumnCount(cols); setColumnCount(cols);
for (int row = 0; row < rows; row++) { for (int row = 0; row < rows; row++) {
for (int col = 0; col < cols; col++) { for (int col = 0; col < buttons[row].length; col++) {
final String buttonText = buttons[row][col]; final String buttonText = buttons[row][col];
Button button; Button button;
switch (buttonText) { if(Arrays.asList("CTRL", "ALT", "FN").contains(buttonText)) {
case "CTRL": SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonText)); // for valueOf: https://stackoverflow.com/a/604426/1980630
button = controlButton = new ToggleButton(getContext(), null, android.R.attr.buttonBarButtonStyle); state.isOn = true;
button.setClickable(true); button = state.button = new ToggleButton(getContext(), null, android.R.attr.buttonBarButtonStyle);
break; button.setClickable(true);
case "ALT": } else {
button = altButton = new ToggleButton(getContext(), null, android.R.attr.buttonBarButtonStyle); button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
button.setClickable(true);
break;
case "FN":
button = fnButton = new ToggleButton(getContext(), null, android.R.attr.buttonBarButtonStyle);
button.setClickable(true);
break;
default:
button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
break;
} }
button.setText(buttonText); final String displayedText = charDisplayMap.get(buttonText, buttonText);
button.setText(displayedText);
button.setTextColor(TEXT_COLOR); button.setTextColor(TEXT_COLOR);
button.setPadding(0, 0, 0, 0);
final Button finalButton = button; final Button finalButton = button;
button.setOnClickListener(new OnClickListener() { button.setOnClickListener(v -> {
@Override if (Settings.System.getInt(getContext().getContentResolver(),
public void onClick(View v) { Settings.System.HAPTIC_FEEDBACK_ENABLED, 0) != 0) {
View root = getRootView();
switch (buttonText) { // Depending on DnD settings, value can be >1 but 0 means "disabled".
case "CTRL": if (Settings.Global.getInt(getContext().getContentResolver(), "zen_mode", 0) < 1) {
case "ALT": finalButton.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
case "FN":
ToggleButton self = (ToggleButton) finalButton;
self.setChecked(self.isChecked());
self.setTextColor(self.isChecked() ? 0xFF80DEEA : TEXT_COLOR);
break;
default:
sendKey(root, buttonText);
break;
} }
} }
View root = getRootView();
if(Arrays.asList("CTRL", "ALT", "FN").contains(buttonText)) {
ToggleButton self = (ToggleButton) finalButton;
self.setChecked(self.isChecked());
self.setTextColor(self.isChecked() ? INTERESTING_COLOR : TEXT_COLOR);
} else {
sendKey(root, buttonText);
}
}); });
GridLayout.LayoutParams param = new GridLayout.LayoutParams(); button.setOnTouchListener((v, event) -> {
param.height = param.width = 0; final View root = getRootView();
param.rightMargin = param.topMargin = 0; switch (event.getAction()) {
param.setGravity(Gravity.LEFT); case MotionEvent.ACTION_DOWN:
float weight = "▲▼◀▶".contains(buttonText) ? 0.7f : 1.f; longPressCount = 0;
param.columnSpec = GridLayout.spec(col, GridLayout.FILL, weight); v.setBackgroundColor(BUTTON_PRESSED_COLOR);
if (Arrays.asList("UP", "DOWN", "LEFT", "RIGHT").contains(buttonText)) {
scheduledExecutor = Executors.newSingleThreadScheduledExecutor();
scheduledExecutor.scheduleWithFixedDelay(() -> {
longPressCount++;
sendKey(root, buttonText);
}, 400, 80, TimeUnit.MILLISECONDS);
}
return true;
case MotionEvent.ACTION_MOVE:
// These two keys have a Move-Up button appearing
if (Arrays.asList("/", "-").contains(buttonText)) {
if (popupWindow == null && event.getY() < 0) {
v.setBackgroundColor(BUTTON_COLOR);
String text = "-".equals(buttonText) ? "|" : "\\";
popup(v, text);
}
if (popupWindow != null && event.getY() > 0) {
v.setBackgroundColor(BUTTON_PRESSED_COLOR);
popupWindow.dismiss();
popupWindow = null;
}
}
return true;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
v.setBackgroundColor(BUTTON_COLOR);
if (scheduledExecutor != null) {
scheduledExecutor.shutdownNow();
scheduledExecutor = null;
}
if (longPressCount == 0) {
if (popupWindow != null && Arrays.asList("/", "-").contains(buttonText)) {
popupWindow.setContentView(null);
popupWindow.dismiss();
popupWindow = null;
sendKey(root, "-".equals(buttonText) ? "|" : "\\");
} else {
v.performClick();
}
}
return true;
default:
return true;
}
});
LayoutParams param = new GridLayout.LayoutParams();
param.width = 0;
if(Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP) { // special handle api 21
param.height = (int)(37.5 * getResources().getDisplayMetrics().density + 0.5); // 37.5 equal to R.id.viewpager layout_height / rows in DP
} else {
param.height = 0;
}
param.setMargins(0, 0, 0, 0);
param.columnSpec = GridLayout.spec(col, GridLayout.FILL, 1.f);
param.rowSpec = GridLayout.spec(row, GridLayout.FILL, 1.f); param.rowSpec = GridLayout.spec(row, GridLayout.FILL, 1.f);
button.setLayoutParams(param); button.setLayoutParams(param);

View File

@@ -1,67 +0,0 @@
package com.termux.app;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.os.Build;
import android.view.View;
import com.termux.R;
/**
* Utility to manage full screen immersive mode.
* <p/>
* See https://code.google.com/p/android/issues/detail?id=5497
*/
final class FullScreenHelper {
private boolean mEnabled = false;
final TermuxActivity mActivity;
public FullScreenHelper(TermuxActivity activity) {
this.mActivity = activity;
}
public void setImmersive(boolean enabled) {
if (enabled == mEnabled) return;
mEnabled = enabled;
View decorView = mActivity.getWindow().getDecorView();
if (enabled) {
decorView.setOnSystemUiVisibilityChangeListener
(new View.OnSystemUiVisibilityChangeListener() {
@Override
public void onSystemUiVisibilityChange(int visibility) {
if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
if (mActivity.mSettings.isShowExtraKeys()) {
mActivity.findViewById(R.id.viewpager).setVisibility(View.VISIBLE);
}
setImmersiveMode();
} else {
mActivity.findViewById(R.id.viewpager).setVisibility(View.GONE);
}
}
});
setImmersiveMode();
} else {
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
decorView.setOnSystemUiVisibilityChangeListener(null);
}
}
private static boolean isColorLight(int color) {
double darkness = 1 - (0.299 * Color.red(color) + 0.587 * Color.green(color) + 0.114 * Color.blue(color)) / 255;
return darkness < 0.5;
}
void setImmersiveMode() {
int flags = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
| View.SYSTEM_UI_FLAG_FULLSCREEN;
int color = ((ColorDrawable) mActivity.getWindow().getDecorView().getBackground()).getColor();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && isColorLight(color))
flags |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
mActivity.getWindow().getDecorView().setSystemUiVisibility(flags);
}
}

View File

@@ -11,8 +11,6 @@ import android.content.ClipData;
import android.content.ClipboardManager; import android.content.ClipboardManager;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnShowListener;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.content.ServiceConnection; import android.content.ServiceConnection;
@@ -26,12 +24,6 @@ import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.IBinder; import android.os.IBinder;
import android.os.Vibrator;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v4.widget.DrawerLayout;
import android.text.SpannableString; import android.text.SpannableString;
import android.text.Spanned; import android.text.Spanned;
import android.text.TextUtils; import android.text.TextUtils;
@@ -40,21 +32,13 @@ import android.util.Log;
import android.view.ContextMenu; import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo; import android.view.ContextMenu.ContextMenuInfo;
import android.view.Gravity; import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.WindowManager; import android.view.WindowManager;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.AdapterView.OnItemLongClickListener;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.EditText; import android.widget.EditText;
import android.widget.ListView; import android.widget.ListView;
@@ -75,10 +59,17 @@ import java.io.InputStream;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List;
import java.util.Properties; import java.util.Properties;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;
/** /**
* A terminal emulator activity. * A terminal emulator activity.
* <p/> * <p/>
@@ -91,14 +82,16 @@ import java.util.regex.Pattern;
*/ */
public final class TermuxActivity extends Activity implements ServiceConnection { public final class TermuxActivity extends Activity implements ServiceConnection {
public static final String TERMUX_FAILSAFE_SESSION_ACTION = "com.termux.app.failsafe_session";
private static final int CONTEXTMENU_SELECT_URL_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_SHARE_TRANSCRIPT_ID = 1;
private static final int CONTEXTMENU_PASTE_ID = 3; private static final int CONTEXTMENU_PASTE_ID = 3;
private static final int CONTEXTMENU_KILL_PROCESS_ID = 4; private static final int CONTEXTMENU_KILL_PROCESS_ID = 4;
private static final int CONTEXTMENU_RESET_TERMINAL_ID = 5; private static final int CONTEXTMENU_RESET_TERMINAL_ID = 5;
private static final int CONTEXTMENU_STYLING_ID = 6; private static final int CONTEXTMENU_STYLING_ID = 6;
private static final int CONTEXTMENU_TOGGLE_FULLSCREEN_ID = 7;
private static final int CONTEXTMENU_HELP_ID = 8; private static final int CONTEXTMENU_HELP_ID = 8;
private static final int CONTEXTMENU_TOGGLE_KEEP_SCREEN_ON = 9;
private static final int MAX_SESSIONS = 8; private static final int MAX_SESSIONS = 8;
@@ -113,8 +106,6 @@ public final class TermuxActivity extends Activity implements ServiceConnection
ExtraKeysView mExtraKeysView; ExtraKeysView mExtraKeysView;
final FullScreenHelper mFullScreenHelper = new FullScreenHelper(this);
TermuxPreferences mSettings; TermuxPreferences mSettings;
/** /**
@@ -136,6 +127,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
*/ */
boolean mIsVisible; boolean mIsVisible;
boolean mIsUsingBlackUI;
final SoundPool mBellSoundPool = new SoundPool.Builder().setMaxStreams(1).setAudioAttributes( final SoundPool mBellSoundPool = new SoundPool.Builder().setMaxStreams(1).setAudioAttributes(
new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build()).build(); .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build()).build();
@@ -153,13 +146,16 @@ public final class TermuxActivity extends Activity implements ServiceConnection
} }
checkForFontAndColors(); checkForFontAndColors();
mSettings.reloadFromProperties(TermuxActivity.this); mSettings.reloadFromProperties(TermuxActivity.this);
if (mExtraKeysView != null) {
mExtraKeysView.reload(mSettings.mExtraKeys, ExtraKeysView.defaultCharDisplay);
}
} }
} }
}; };
void checkForFontAndColors() { void checkForFontAndColors() {
try { 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 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"); @SuppressLint("SdCardPath") File colorsFile = new File("/data/data/com.termux/files/home/.termux/colors.properties");
@@ -209,20 +205,38 @@ public final class TermuxActivity extends Activity implements ServiceConnection
@Override @Override
public void onCreate(Bundle bundle) { public void onCreate(Bundle bundle) {
mSettings = new TermuxPreferences(this);
mIsUsingBlackUI = mSettings.isUsingBlackUI();
if (mIsUsingBlackUI) {
this.setTheme(R.style.Theme_Termux_Black);
} else {
this.setTheme(R.style.Theme_Termux);
}
super.onCreate(bundle); super.onCreate(bundle);
mSettings = new TermuxPreferences(this);
setContentView(R.layout.drawer_layout); setContentView(R.layout.drawer_layout);
mTerminalView = (TerminalView) findViewById(R.id.terminal_view);
mTerminalView.setOnKeyListener(new TermuxKeyListener(this)); if (mIsUsingBlackUI) {
findViewById(R.id.left_drawer).setBackgroundColor(
getResources().getColor(android.R.color.background_dark)
);
}
mTerminalView = findViewById(R.id.terminal_view);
mTerminalView.setOnKeyListener(new TermuxViewClient(this));
mTerminalView.setTextSize(mSettings.getFontSize()); mTerminalView.setTextSize(mSettings.getFontSize());
mFullScreenHelper.setImmersive(mSettings.isFullScreen()); mTerminalView.setKeepScreenOn(mSettings.isScreenAlwaysOn());
mTerminalView.requestFocus(); mTerminalView.requestFocus();
final ViewPager viewPager = (ViewPager) findViewById(R.id.viewpager); final ViewPager viewPager = findViewById(R.id.viewpager);
if (mSettings.isShowExtraKeys()) viewPager.setVisibility(View.VISIBLE); if (mSettings.mShowExtraKeys) viewPager.setVisibility(View.VISIBLE);
ViewGroup.LayoutParams layoutParams = viewPager.getLayoutParams();
layoutParams.height = layoutParams.height * mSettings.mExtraKeys.length;
viewPager.setLayoutParams(layoutParams);
viewPager.setAdapter(new PagerAdapter() { viewPager.setAdapter(new PagerAdapter() {
@Override @Override
@@ -231,35 +245,34 @@ public final class TermuxActivity extends Activity implements ServiceConnection
} }
@Override @Override
public boolean isViewFromObject(View view, Object object) { public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
return view == object; return view == object;
} }
@NonNull
@Override @Override
public Object instantiateItem(ViewGroup collection, int position) { public Object instantiateItem(@NonNull ViewGroup collection, int position) {
LayoutInflater inflater = LayoutInflater.from(TermuxActivity.this); LayoutInflater inflater = LayoutInflater.from(TermuxActivity.this);
View layout; View layout;
if (position == 0) { if (position == 0) {
layout = mExtraKeysView = (ExtraKeysView) inflater.inflate(R.layout.extra_keys_main, collection, false); layout = mExtraKeysView = (ExtraKeysView) inflater.inflate(R.layout.extra_keys_main, collection, false);
mExtraKeysView.reload(mSettings.mExtraKeys, ExtraKeysView.defaultCharDisplay);
} else { } else {
layout = inflater.inflate(R.layout.extra_keys_right, collection, false); layout = inflater.inflate(R.layout.extra_keys_right, collection, false);
final EditText editText = (EditText) layout.findViewById(R.id.text_input); final EditText editText = layout.findViewById(R.id.text_input);
editText.setOnEditorActionListener(new TextView.OnEditorActionListener() { editText.setOnEditorActionListener((v, actionId, event) -> {
@Override TerminalSession session = getCurrentTermSession();
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { if (session != null) {
TerminalSession session = getCurrentTermSession(); if (session.isRunning()) {
if (session != null) { String textToSend = editText.getText().toString();
if (session.isRunning()) { if (textToSend.length() == 0) textToSend = "\r";
String textToSend = editText.getText().toString(); session.write(textToSend);
if (textToSend.length() == 0) textToSend = "\n"; } else {
session.write(textToSend); removeFinishedSession(session);
} else {
removeFinishedSession(session);
}
editText.setText("");
} }
return true; editText.setText("");
} }
return true;
}); });
} }
collection.addView(layout); collection.addView(layout);
@@ -267,7 +280,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
} }
@Override @Override
public void destroyItem(ViewGroup collection, int position, Object view) { public void destroyItem(@NonNull ViewGroup collection, int position, @NonNull Object view) {
collection.removeView((View) view); collection.removeView((View) view);
} }
}); });
@@ -278,54 +291,30 @@ public final class TermuxActivity extends Activity implements ServiceConnection
if (position == 0) { if (position == 0) {
mTerminalView.requestFocus(); mTerminalView.requestFocus();
} else { } else {
final EditText editText = (EditText) viewPager.findViewById(R.id.text_input); final EditText editText = viewPager.findViewById(R.id.text_input);
if (editText != null) editText.requestFocus(); if (editText != null) editText.requestFocus();
} }
} }
}); });
View newSessionButton = findViewById(R.id.new_session_button); View newSessionButton = findViewById(R.id.new_session_button);
newSessionButton.setOnClickListener(new OnClickListener() { newSessionButton.setOnClickListener(v -> addNewSession(false, null));
@Override newSessionButton.setOnLongClickListener(v -> {
public void onClick(View v) { DialogUtils.textInput(TermuxActivity.this, R.string.session_new_named_title, null, R.string.session_new_named_positive_button,
addNewSession(false, null); text -> addNewSession(false, text), R.string.new_session_failsafe, text -> addNewSession(true, text)
} , -1, null, null);
}); 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(v -> {
@Override InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
public void onClick(View v) { imm.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, 0);
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); getDrawer().closeDrawers();
imm.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, 0);
getDrawer().closeDrawers();
}
}); });
findViewById(R.id.toggle_keyboard_button).setOnLongClickListener(new OnLongClickListener() { findViewById(R.id.toggle_keyboard_button).setOnLongClickListener(v -> {
@Override toggleShowExtraKeys();
public boolean onLongClick(View v) { return true;
toggleShowExtraKeys();
return true;
}
}); });
registerForContextMenu(mTerminalView); registerForContextMenu(mTerminalView);
@@ -342,7 +331,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
} }
void toggleShowExtraKeys() { void toggleShowExtraKeys() {
final ViewPager viewPager = (ViewPager) findViewById(R.id.viewpager); final ViewPager viewPager = findViewById(R.id.viewpager);
final boolean showNow = mSettings.toggleShowExtraKeys(TermuxActivity.this); final boolean showNow = mSettings.toggleShowExtraKeys(TermuxActivity.this);
viewPager.setVisibility(showNow ? View.VISIBLE : View.GONE); viewPager.setVisibility(showNow ? View.VISIBLE : View.GONE);
if (showNow && viewPager.getCurrentItem() == 1) { if (showNow && viewPager.getCurrentItem() == 1) {
@@ -393,13 +382,27 @@ public final class TermuxActivity extends Activity implements ServiceConnection
if (indexOfSession >= 0) if (indexOfSession >= 0)
showToast(toToastTitle(finishedSession) + " - exited", true); showToast(toToastTitle(finishedSession) + " - exited", true);
} }
if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
// On Android TV devices we need to use older behaviour because we may
// not be able to have multiple launcher icons.
if (mTermService.getSessions().size() > 1) {
removeFinishedSession(finishedSession);
}
} else {
// Once we have a separate launcher icon for the failsafe session, it
// should be safe to auto-close session on exit code '0' or '130'.
if (finishedSession.getExitStatus() == 0 || finishedSession.getExitStatus() == 130) {
removeFinishedSession(finishedSession);
}
}
mListViewAdapter.notifyDataSetChanged(); mListViewAdapter.notifyDataSetChanged();
} }
@Override @Override
public void onClipboardText(TerminalSession session, String text) { public void onClipboardText(TerminalSession session, String text) {
if (!mIsVisible) return; if (!mIsVisible) return;
showToast("Clipboard:\n\"" + text + "\"", false);
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(text))); clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(text)));
} }
@@ -413,7 +416,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
mBellSoundPool.play(mBellSoundId, 1.f, 1.f, 1, 0, 1.f); mBellSoundPool.play(mBellSoundId, 1.f, 1.f, 1, 0, 1.f);
break; break;
case TermuxPreferences.BELL_VIBRATE: case TermuxPreferences.BELL_VIBRATE:
((Vibrator) getSystemService(VIBRATOR_SERVICE)).vibrate(50); BellUtil.getInstance(TermuxActivity.this).doBell();
break; break;
case TermuxPreferences.BELL_IGNORE: case TermuxPreferences.BELL_IGNORE:
// Ignore the bell character. // Ignore the bell character.
@@ -428,7 +431,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
} }
}; };
ListView listView = (ListView) findViewById(R.id.left_drawer_list); ListView listView = findViewById(R.id.left_drawer_list);
mListViewAdapter = new ArrayAdapter<TerminalSession>(getApplicationContext(), R.layout.line_in_drawer, mTermService.getSessions()) { mListViewAdapter = new ArrayAdapter<TerminalSession>(getApplicationContext(), R.layout.line_in_drawer, mTermService.getSessions()) {
final StyleSpan boldSpan = new StyleSpan(Typeface.BOLD); final StyleSpan boldSpan = new StyleSpan(Typeface.BOLD);
final StyleSpan italicSpan = new StyleSpan(Typeface.ITALIC); final StyleSpan italicSpan = new StyleSpan(Typeface.ITALIC);
@@ -445,8 +448,12 @@ public final class TermuxActivity extends Activity implements ServiceConnection
TerminalSession sessionAtRow = getItem(position); TerminalSession sessionAtRow = getItem(position);
boolean sessionRunning = sessionAtRow.isRunning(); boolean sessionRunning = sessionAtRow.isRunning();
TextView firstLineView = (TextView) row.findViewById(R.id.row_line); TextView firstLineView = row.findViewById(R.id.row_line);
if (mIsUsingBlackUI) {
firstLineView.setBackground(
getResources().getDrawable(R.drawable.selected_session_background_black)
);
}
String name = sessionAtRow.mSessionName; String name = sessionAtRow.mSessionName;
String sessionTitle = sessionAtRow.getTitle(); String sessionTitle = sessionAtRow.getTitle();
@@ -466,40 +473,37 @@ public final class TermuxActivity extends Activity implements ServiceConnection
} else { } else {
firstLineView.setPaintFlags(firstLineView.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); firstLineView.setPaintFlags(firstLineView.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
} }
int color = sessionRunning || sessionAtRow.getExitStatus() == 0 ? Color.BLACK : Color.RED; int defaultColor = mIsUsingBlackUI ? Color.WHITE : Color.BLACK;
int color = sessionRunning || sessionAtRow.getExitStatus() == 0 ? defaultColor : Color.RED;
firstLineView.setTextColor(color); firstLineView.setTextColor(color);
return row; return row;
} }
}; };
listView.setAdapter(mListViewAdapter); listView.setAdapter(mListViewAdapter);
listView.setOnItemClickListener(new OnItemClickListener() { listView.setOnItemClickListener((parent, view, position, id) -> {
@Override TerminalSession clickedSession = mListViewAdapter.getItem(position);
public void onItemClick(AdapterView<?> parent, View view, int position, long id) { switchToSession(clickedSession);
TerminalSession clickedSession = mListViewAdapter.getItem(position); getDrawer().closeDrawers();
switchToSession(clickedSession);
getDrawer().closeDrawers();
}
}); });
listView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { listView.setOnItemLongClickListener((parent, view, position, id) -> {
@Override final TerminalSession selectedSession = mListViewAdapter.getItem(position);
public boolean onItemLongClick(AdapterView<?> parent, View view, final int position, long id) { renameSession(selectedSession);
final TerminalSession selectedSession = mListViewAdapter.getItem(position); return true;
renameSession(selectedSession);
return true;
}
}); });
if (mTermService.getSessions().isEmpty()) { if (mTermService.getSessions().isEmpty()) {
if (mIsVisible) { if (mIsVisible) {
TermuxInstaller.setupIfNeeded(TermuxActivity.this, new Runnable() { TermuxInstaller.setupIfNeeded(TermuxActivity.this, () -> {
@Override if (mTermService == null) return; // Activity might have been destroyed.
public void run() { try {
if (mTermService == null) return; // Activity might have been destroyed. Bundle bundle = getIntent().getExtras();
try { boolean launchFailsafe = false;
addNewSession(false, null); if (bundle != null) {
} catch (WindowManager.BadTokenException e) { launchFailsafe = bundle.getBoolean(TERMUX_FAILSAFE_SESSION_ACTION, false);
// Activity finished - ignore.
} }
addNewSession(launchFailsafe, null);
} catch (WindowManager.BadTokenException e) {
// Activity finished - ignore.
} }
}); });
} else { } else {
@@ -510,7 +514,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
Intent i = getIntent(); Intent i = getIntent();
if (i != null && Intent.ACTION_RUN.equals(i.getAction())) { if (i != null && Intent.ACTION_RUN.equals(i.getAction())) {
// Android 7.1 app shortcut from res/xml/shortcuts.xml. // Android 7.1 app shortcut from res/xml/shortcuts.xml.
addNewSession(false, null); boolean failSafe = i.getBooleanExtra(TERMUX_FAILSAFE_SESSION_ACTION, false);
addNewSession(failSafe, null);
} else { } else {
switchToSession(getStoredCurrentSessionOrLast()); switchToSession(getStoredCurrentSessionOrLast());
} }
@@ -530,21 +535,16 @@ public final class TermuxActivity extends Activity implements ServiceConnection
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
void renameSession(final TerminalSession sessionToRename) { void renameSession(final TerminalSession sessionToRename) {
DialogUtils.textInput(this, R.string.session_rename_title, sessionToRename.mSessionName, R.string.session_rename_positive_button, new DialogUtils.TextSetListener() { DialogUtils.textInput(this, R.string.session_rename_title, sessionToRename.mSessionName, R.string.session_rename_positive_button, text -> {
@Override sessionToRename.mSessionName = text;
public void onTextSet(String text) { mListViewAdapter.notifyDataSetChanged();
sessionToRename.mSessionName = text;
mListViewAdapter.notifyDataSetChanged();
}
}, -1, null, -1, null, null); }, -1, null, -1, null, null);
} }
@Override @Override
public void onServiceDisconnected(ComponentName name) { public void onServiceDisconnected(ComponentName name) {
if (mTermService != null) { // Respect being stopped from the TermuxService notification action.
// Respect being stopped from the TermuxService notification action. finish();
finish();
}
} }
@Nullable @Nullable
@@ -609,8 +609,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
new AlertDialog.Builder(this).setTitle(R.string.max_terminals_reached_title).setMessage(R.string.max_terminals_reached_message) new AlertDialog.Builder(this).setTitle(R.string.max_terminals_reached_title).setMessage(R.string.max_terminals_reached_message)
.setPositiveButton(android.R.string.ok, null).show(); .setPositiveButton(android.R.string.ok, null).show();
} else { } else {
String executablePath = (failSafe ? "/system/bin/sh" : null); TerminalSession newSession = mTermService.createTermSession(null, null, null, failSafe);
TerminalSession newSession = mTermService.createTermSession(executablePath, null, null, failSafe);
if (sessionName != null) { if (sessionName != null) {
newSession.mSessionName = sessionName; newSession.mSessionName = sessionName;
} }
@@ -648,7 +647,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
final int indexOfSession = mTermService.getSessions().indexOf(session); final int indexOfSession = mTermService.getSessions().indexOf(session);
showToast(toToastTitle(session), false); showToast(toToastTitle(session), false);
mListViewAdapter.notifyDataSetChanged(); mListViewAdapter.notifyDataSetChanged();
final ListView lv = ((ListView) findViewById(R.id.left_drawer_list)); final ListView lv = findViewById(R.id.left_drawer_list);
lv.setItemChecked(indexOfSession, true); lv.setItemChecked(indexOfSession, true);
lv.smoothScrollToPosition(indexOfSession); lv.smoothScrollToPosition(indexOfSession);
} }
@@ -662,8 +661,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
menu.add(Menu.NONE, CONTEXTMENU_SHARE_TRANSCRIPT_ID, Menu.NONE, R.string.select_all_and_share); 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_RESET_TERMINAL_ID, Menu.NONE, R.string.reset_terminal);
menu.add(Menu.NONE, CONTEXTMENU_KILL_PROCESS_ID, Menu.NONE, getResources().getString(R.string.kill_process, getCurrentTermSession().getPid())).setEnabled(currentSession.isRunning()); menu.add(Menu.NONE, CONTEXTMENU_KILL_PROCESS_ID, Menu.NONE, getResources().getString(R.string.kill_process, getCurrentTermSession().getPid())).setEnabled(currentSession.isRunning());
menu.add(Menu.NONE, CONTEXTMENU_TOGGLE_FULLSCREEN_ID, Menu.NONE, R.string.toggle_fullscreen).setCheckable(true).setChecked(mSettings.isFullScreen());
menu.add(Menu.NONE, CONTEXTMENU_STYLING_ID, Menu.NONE, R.string.style_terminal); menu.add(Menu.NONE, CONTEXTMENU_STYLING_ID, Menu.NONE, R.string.style_terminal);
menu.add(Menu.NONE, CONTEXTMENU_TOGGLE_KEEP_SCREEN_ON, Menu.NONE, R.string.toggle_keep_screen_on).setCheckable(true).setChecked(mSettings.isScreenAlwaysOn());
menu.add(Menu.NONE, CONTEXTMENU_HELP_ID, Menu.NONE, R.string.help); menu.add(Menu.NONE, CONTEXTMENU_HELP_ID, Menu.NONE, R.string.help);
} }
@@ -675,19 +674,86 @@ public final class TermuxActivity extends Activity implements ServiceConnection
} }
static LinkedHashSet<CharSequence> extractUrls(String text) { 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 StringBuilder regex_sb = new StringBuilder();
regex_sb.append("("); // Begin first matching group.
regex_sb.append("(?:"); // Begin scheme group.
regex_sb.append("dav|"); // The DAV proto.
regex_sb.append("dict|"); // The DICT proto.
regex_sb.append("dns|"); // The DNS proto.
regex_sb.append("file|"); // File path.
regex_sb.append("finger|"); // The Finger proto.
regex_sb.append("ftp(?:s?)|"); // The FTP proto.
regex_sb.append("git|"); // The Git proto.
regex_sb.append("gopher|"); // The Gopher proto.
regex_sb.append("http(?:s?)|"); // The HTTP proto.
regex_sb.append("imap(?:s?)|"); // The IMAP proto.
regex_sb.append("irc(?:[6s]?)|"); // The IRC proto.
regex_sb.append("ip[fn]s|"); // The IPFS proto.
regex_sb.append("ldap(?:s?)|"); // The LDAP proto.
regex_sb.append("pop3(?:s?)|"); // The POP3 proto.
regex_sb.append("redis(?:s?)|"); // The Redis proto.
regex_sb.append("rsync|"); // The Rsync proto.
regex_sb.append("rtsp(?:[su]?)|"); // The RTSP proto.
regex_sb.append("sftp|"); // The SFTP proto.
regex_sb.append("smb(?:s?)|"); // The SAMBA proto.
regex_sb.append("smtp(?:s?)|"); // The SMTP proto.
regex_sb.append("svn(?:(?:\\+ssh)?)|"); // The Subversion proto.
regex_sb.append("tcp|"); // The TCP proto.
regex_sb.append("telnet|"); // The Telnet proto.
regex_sb.append("tftp|"); // The TFTP proto.
regex_sb.append("udp|"); // The UDP proto.
regex_sb.append("vnc|"); // The VNC proto.
regex_sb.append("ws(?:s?)"); // The Websocket proto.
regex_sb.append(")://"); // End scheme group.
regex_sb.append(")"); // End first matching group.
// Begin second matching group.
regex_sb.append("(");
// User name and/or password in format 'user:pass@'.
regex_sb.append("(?:\\S+(?::\\S*)?@)?");
// Begin host group.
regex_sb.append("(?:");
// IP address (from http://www.regular-expressions.info/examples.html).
regex_sb.append("(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|");
// Host name or domain.
regex_sb.append("(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)(?:(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))?|");
// Just path. Used in case of 'file://' scheme.
regex_sb.append("/(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)");
// End host group.
regex_sb.append(")");
// Port number.
regex_sb.append("(?::\\d{1,5})?");
// Resource path with optional query string.
regex_sb.append("(?:/[a-zA-Z0-9:@%\\-._~!$&()*+,;=?/]*)?");
// End second matching group.
regex_sb.append(")");
final Pattern urlPattern = Pattern.compile( final Pattern urlPattern = Pattern.compile(
"(?:^|[\\W])((ht|f)tp(s?)://|www\\.)" + "(([\\w\\-]+\\.)+?([\\w\\-.~]+/?)*" + "[\\p{Alnum}.,%_=?&#\\-+()\\[\\]\\*$~@!:/{};']*)", regex_sb.toString(),
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL); Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
LinkedHashSet<CharSequence> urlSet = new LinkedHashSet<>(); LinkedHashSet<CharSequence> urlSet = new LinkedHashSet<>();
Matcher matcher = urlPattern.matcher(text); Matcher matcher = urlPattern.matcher(text);
while (matcher.find()) { while (matcher.find()) {
int matchStart = matcher.start(1); int matchStart = matcher.start(1);
int matchEnd = matcher.end(); int matchEnd = matcher.end();
String url = text.substring(matchStart, matchEnd); String url = text.substring(matchStart, matchEnd);
urlSet.add(url); urlSet.add(url);
} }
return urlSet; return urlSet;
} }
@@ -699,41 +765,32 @@ public final class TermuxActivity extends Activity implements ServiceConnection
return; return;
} }
final CharSequence[] urls = urlSet.toArray(new CharSequence[urlSet.size()]); final CharSequence[] urls = urlSet.toArray(new CharSequence[0]);
Collections.reverse(Arrays.asList(urls)); // Latest first. Collections.reverse(Arrays.asList(urls)); // Latest first.
// Click to copy url to clipboard: // Click to copy url to clipboard:
final AlertDialog dialog = new AlertDialog.Builder(TermuxActivity.this).setItems(urls, new DialogInterface.OnClickListener() { final AlertDialog dialog = new AlertDialog.Builder(TermuxActivity.this).setItems(urls, (di, which) -> {
@Override String url = (String) urls[which];
public void onClick(DialogInterface di, int which) { ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
String url = (String) urls[which]; clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(url)));
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); Toast.makeText(TermuxActivity.this, R.string.select_url_copied_to_clipboard, Toast.LENGTH_LONG).show();
clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(url)));
Toast.makeText(TermuxActivity.this, R.string.select_url_copied_to_clipboard, Toast.LENGTH_LONG).show();
}
}).setTitle(R.string.select_url_dialog_title).create(); }).setTitle(R.string.select_url_dialog_title).create();
// Long press to open URL: // Long press to open URL:
dialog.setOnShowListener(new OnShowListener() { dialog.setOnShowListener(di -> {
@Override ListView lv = dialog.getListView(); // this is a ListView with your "buds" in it
public void onShow(DialogInterface di) { lv.setOnItemLongClickListener((parent, view, position, id) -> {
ListView lv = dialog.getListView(); // this is a ListView with your "buds" in it dialog.dismiss();
lv.setOnItemLongClickListener(new OnItemLongClickListener() { String url = (String) urls[position];
@Override Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { try {
dialog.dismiss(); startActivity(i, null);
String url = (String) urls[position]; } catch (ActivityNotFoundException e) {
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); // If no applications match, Android displays a system message.
try { startActivity(Intent.createChooser(i, null));
startActivity(i, null); }
} catch (ActivityNotFoundException e) { return true;
// If no applications match, Android displays a system message. });
startActivity(Intent.createChooser(i, null));
}
return true;
}
});
}
}); });
dialog.show(); dialog.show();
@@ -751,7 +808,18 @@ public final class TermuxActivity extends Activity implements ServiceConnection
if (session != null) { if (session != null) {
Intent intent = new Intent(Intent.ACTION_SEND); Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("text/plain"); intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TEXT, session.getEmulator().getScreen().getTranscriptText().trim()); String transcriptText = session.getEmulator().getScreen().getTranscriptTextWithoutJoinedLines().trim();
// See https://github.com/termux/termux-app/issues/1166.
final int MAX_LENGTH = 100_000;
if (transcriptText.length() > MAX_LENGTH) {
int cutOffIndex = transcriptText.length() - MAX_LENGTH;
int nextNewlineIndex = transcriptText.indexOf('\n', cutOffIndex);
if (nextNewlineIndex != -1 && nextNewlineIndex != transcriptText.length() - 1) {
cutOffIndex = nextNewlineIndex + 1;
}
transcriptText = transcriptText.substring(cutOffIndex).trim();
}
intent.putExtra(Intent.EXTRA_TEXT, transcriptText);
intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.share_transcript_title)); intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.share_transcript_title));
startActivity(Intent.createChooser(intent, getString(R.string.share_transcript_chooser_title))); startActivity(Intent.createChooser(intent, getString(R.string.share_transcript_chooser_title)));
} }
@@ -763,12 +831,9 @@ public final class TermuxActivity extends Activity implements ServiceConnection
final AlertDialog.Builder b = new AlertDialog.Builder(this); final AlertDialog.Builder b = new AlertDialog.Builder(this);
b.setIcon(android.R.drawable.ic_dialog_alert); b.setIcon(android.R.drawable.ic_dialog_alert);
b.setMessage(R.string.confirm_kill_process); b.setMessage(R.string.confirm_kill_process);
b.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { b.setPositiveButton(android.R.string.yes, (dialog, id) -> {
@Override dialog.dismiss();
public void onClick(DialogInterface dialog, int id) { getCurrentTermSession().finishIfRunning();
dialog.dismiss();
getCurrentTermSession().finishIfRunning();
}
}); });
b.setNegativeButton(android.R.string.no, null); b.setNegativeButton(android.R.string.no, null);
b.show(); b.show();
@@ -789,21 +854,23 @@ public final class TermuxActivity extends Activity implements ServiceConnection
// The startActivity() call is not documented to throw IllegalArgumentException. // The startActivity() call is not documented to throw IllegalArgumentException.
// However, crash reporting shows that it sometimes does, so catch it here. // However, crash reporting shows that it sometimes does, so catch it here.
new AlertDialog.Builder(this).setMessage(R.string.styling_not_installed) new AlertDialog.Builder(this).setMessage(R.string.styling_not_installed)
.setPositiveButton(R.string.styling_install, new DialogInterface.OnClickListener() { .setPositiveButton(R.string.styling_install, (dialog, 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();
@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; return true;
}
case CONTEXTMENU_HELP_ID: case CONTEXTMENU_HELP_ID:
startActivity(new Intent(this, TermuxHelpActivity.class)); startActivity(new Intent(this, TermuxHelpActivity.class));
return true; return true;
case CONTEXTMENU_TOGGLE_KEEP_SCREEN_ON: {
if(mTerminalView.getKeepScreenOn()) {
mTerminalView.setKeepScreenOn(false);
mSettings.setScreenAlwaysOn(this, false);
} else {
mTerminalView.setKeepScreenOn(true);
mSettings.setScreenAlwaysOn(this, true);
}
return true;
}
default: default:
return super.onContextItemSelected(item); return super.onContextItemSelected(item);
} }
@@ -816,12 +883,6 @@ public final class TermuxActivity extends Activity implements ServiceConnection
} }
} }
void toggleImmersive() {
boolean newValue = !mSettings.isFullScreen();
mSettings.setFullScreen(this, newValue);
mFullScreenHelper.setImmersive(newValue);
}
void changeFontSize(boolean increase) { void changeFontSize(boolean increase) {
mSettings.changeFontSize(this, increase); mSettings.changeFontSize(this, increase);
mTerminalView.setTextSize(mSettings.getFontSize()); mTerminalView.setTextSize(mSettings.getFontSize());
@@ -840,9 +901,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
public TerminalSession getStoredCurrentSessionOrLast() { public TerminalSession getStoredCurrentSessionOrLast() {
TerminalSession stored = TermuxPreferences.getCurrentSession(this); TerminalSession stored = TermuxPreferences.getCurrentSession(this);
if (stored != null) return stored; if (stored != null) return stored;
int numberOfSessions = mTermService.getSessions().size(); List<TerminalSession> sessions = mTermService.getSessions();
if (numberOfSessions == 0) return null; return sessions.isEmpty() ? null : sessions.get(sessions.size() - 1);
return mTermService.getSessions().get(numberOfSessions - 1);
} }
/** Show a toast and dismiss the last one if still visible. */ /** Show a toast and dismiss the last one if still visible. */

View File

@@ -39,7 +39,7 @@ public final class TermuxHelpActivity extends Activity {
mWebView.setWebViewClient(new WebViewClient() { mWebView.setWebViewClient(new WebViewClient() {
@Override @Override
public boolean shouldOverrideUrlLoading(WebView view, String url) { public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url.startsWith("https://termux.com")) { if (url.startsWith("https://wiki.termux.com")) {
// Inline help. // Inline help.
setContentView(progressLayout); setContentView(progressLayout);
return false; return false;
@@ -60,7 +60,7 @@ public final class TermuxHelpActivity extends Activity {
setContentView(mWebView); setContentView(mWebView);
} }
}); });
mWebView.loadUrl("https://termux.com/help.html"); mWebView.loadUrl("https://wiki.termux.com/wiki/Main_Page");
} }
@Override @Override

View File

@@ -4,9 +4,6 @@ import android.app.Activity;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.app.ProgressDialog; import android.app.ProgressDialog;
import android.content.Context; 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.Build;
import android.os.Environment; import android.os.Environment;
import android.os.UserManager; import android.os.UserManager;
@@ -19,8 +16,10 @@ import com.termux.R;
import com.termux.terminal.EmulatorDebug; import com.termux.terminal.EmulatorDebug;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
@@ -40,10 +39,10 @@ import java.util.zip.ZipInputStream;
* <p/> * <p/>
* (3) A staging folder, $STAGING_PREFIX, is {@link #deleteFolder(File)} if left over from broken installation below. * (3) A staging folder, $STAGING_PREFIX, is {@link #deleteFolder(File)} if left over from broken installation below.
* <p/> * <p/>
* (4) The architecture is determined and an appropriate bootstrap zip url is determined in {@link #determineZipUrl()}. * (4) The zip file is loaded from a shared library.
* <p/> * <p/>
* (5) The zip, containing entries relative to the $PREFIX, is is downloaded and extracted by a zip input stream * (5) The zip, containing entries relative to the $PREFIX, is is downloaded and extracted by a zip input stream
* continously encountering zip file entries: * continuously encountering zip file entries:
* <p/> * <p/>
* (5.1) If the zip entry encountered is SYMLINKS.txt, go through it and remember all symlinks to setup. * (5.1) If the zip entry encountered is SYMLINKS.txt, go through it and remember all symlinks to setup.
* <p/> * <p/>
@@ -59,12 +58,7 @@ final class TermuxInstaller {
boolean isPrimaryUser = um.getSerialNumberForUser(android.os.Process.myUserHandle()) == 0; boolean isPrimaryUser = um.getSerialNumberForUser(android.os.Process.myUserHandle()) == 0;
if (!isPrimaryUser) { if (!isPrimaryUser) {
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_not_primary_user_message) new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_not_primary_user_message)
.setOnDismissListener(new OnDismissListener() { .setOnDismissListener(dialog -> System.exit(0)).setPositiveButton(android.R.string.ok, null).show();
@Override
public void onDismiss(DialogInterface dialog) {
System.exit(0);
}
}).setPositiveButton(android.R.string.ok, null).show();
return; return;
} }
@@ -89,8 +83,8 @@ final class TermuxInstaller {
final byte[] buffer = new byte[8096]; final byte[] buffer = new byte[8096];
final List<Pair<String, String>> symlinks = new ArrayList<>(50); final List<Pair<String, String>> symlinks = new ArrayList<>(50);
final URL zipUrl = determineZipUrl(); final byte[] zipBytes = loadZipBytes();
try (ZipInputStream zipInput = new ZipInputStream(zipUrl.openStream())) { try (ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(zipBytes))) {
ZipEntry zipEntry; ZipEntry zipEntry;
while ((zipEntry = zipInput.getNextEntry()) != null) { while ((zipEntry = zipInput.getNextEntry()) != null) {
if (zipEntry.getName().equals("SYMLINKS.txt")) { if (zipEntry.getName().equals("SYMLINKS.txt")) {
@@ -103,14 +97,17 @@ final class TermuxInstaller {
String oldPath = parts[0]; String oldPath = parts[0];
String newPath = STAGING_PREFIX_PATH + "/" + parts[1]; String newPath = STAGING_PREFIX_PATH + "/" + parts[1];
symlinks.add(Pair.create(oldPath, newPath)); symlinks.add(Pair.create(oldPath, newPath));
ensureDirectoryExists(new File(newPath).getParentFile());
} }
} else { } else {
String zipEntryName = zipEntry.getName(); String zipEntryName = zipEntry.getName();
File targetFile = new File(STAGING_PREFIX_PATH, zipEntryName); File targetFile = new File(STAGING_PREFIX_PATH, zipEntryName);
if (zipEntry.isDirectory()) { boolean isDirectory = zipEntry.isDirectory();
if (!targetFile.mkdirs())
throw new RuntimeException("Failed to create directory: " + targetFile.getAbsolutePath()); ensureDirectoryExists(isDirectory ? targetFile : targetFile.getParentFile());
} else {
if (!isDirectory) {
try (FileOutputStream outStream = new FileOutputStream(targetFile)) { try (FileOutputStream outStream = new FileOutputStream(targetFile)) {
int readBytes; int readBytes;
while ((readBytes = zipInput.read(buffer)) != -1) while ((readBytes = zipInput.read(buffer)) != -1)
@@ -135,46 +132,29 @@ final class TermuxInstaller {
throw new RuntimeException("Unable to rename staging folder"); throw new RuntimeException("Unable to rename staging folder");
} }
activity.runOnUiThread(new Runnable() { activity.runOnUiThread(whenDone);
@Override
public void run() {
whenDone.run();
}
});
} catch (final Exception e) { } catch (final Exception e) {
Log.e(EmulatorDebug.LOG_TAG, "Bootstrap error", e); Log.e(EmulatorDebug.LOG_TAG, "Bootstrap error", e);
activity.runOnUiThread(new Runnable() { activity.runOnUiThread(() -> {
@Override try {
public void run() { new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_body)
try { .setNegativeButton(R.string.bootstrap_error_abort, (dialog, which) -> {
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_body) dialog.dismiss();
.setNegativeButton(R.string.bootstrap_error_abort, new OnClickListener() { activity.finish();
@Override }).setPositiveButton(R.string.bootstrap_error_try_again, (dialog, which) -> {
public void onClick(DialogInterface dialog, int which) { dialog.dismiss();
dialog.dismiss(); TermuxInstaller.setupIfNeeded(activity, whenDone);
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(); }).show();
} catch (WindowManager.BadTokenException e) { } catch (WindowManager.BadTokenException e1) {
// Activity already dismissed - ignore. // Activity already dismissed - ignore.
}
} }
}); });
} finally { } finally {
activity.runOnUiThread(new Runnable() { activity.runOnUiThread(() -> {
@Override try {
public void run() { progress.dismiss();
try { } catch (RuntimeException e) {
progress.dismiss(); // Activity already dismissed - ignore.
} catch (RuntimeException e) {
// Activity already dismissed - ignore.
}
} }
}); });
} }
@@ -182,55 +162,51 @@ final class TermuxInstaller {
}.start(); }.start();
} }
/** Get bootstrap zip url for this systems cpu architecture. */ private static void ensureDirectoryExists(File directory) {
static URL determineZipUrl() throws MalformedURLException { if (!directory.isDirectory() && !directory.mkdirs()) {
String archName = determineTermuxArchName(); throw new RuntimeException("Unable to create directory: " + directory.getAbsolutePath());
return new URL("https://termux.net/bootstrap/bootstrap-" + archName + ".zip"); }
} }
private static String determineTermuxArchName() { public static byte[] loadZipBytes() {
// Note that we cannot use System.getProperty("os.arch") since that may give e.g. "aarch64" // Only load the shared library when necessary to save memory usage.
// while a 64-bit runtime may not be installed (like on the Samsung Galaxy S5 Neo). System.loadLibrary("termux-bootstrap");
// Instead we search through the supported abi:s on the device, see: return getZip();
// http://developer.android.com/ndk/guides/abis.html
// Note that we search for abi:s in preferred order (the ordering of the
// Build.SUPPORTED_ABIS list) to avoid e.g. installing arm on an x86 system where arm
// emulation is available.
for (String androidArch : Build.SUPPORTED_ABIS) {
switch (androidArch) {
case "arm64-v8a": return "aarch64";
case "armeabi-v7a": return "arm";
case "x86_64": return "x86_64";
case "x86": return "i686";
}
}
throw new RuntimeException("Unable to determine arch from Build.SUPPORTED_ABIS = " +
Arrays.toString(Build.SUPPORTED_ABIS));
} }
/** Delete a folder and all its content or throw. */ public static native byte[] getZip();
static void deleteFolder(File fileOrDirectory) {
File[] children = fileOrDirectory.listFiles(); /** Delete a folder and all its content or throw. Don't follow symlinks. */
if (children != null) { static void deleteFolder(File fileOrDirectory) throws IOException {
for (File child : children) { if (fileOrDirectory.getCanonicalPath().equals(fileOrDirectory.getAbsolutePath()) && fileOrDirectory.isDirectory()) {
deleteFolder(child); File[] children = fileOrDirectory.listFiles();
if (children != null) {
for (File child : children) {
deleteFolder(child);
}
} }
} }
if (!fileOrDirectory.delete()) { if (!fileOrDirectory.delete()) {
throw new RuntimeException("Unable to delete " + (fileOrDirectory.isDirectory() ? "directory " : "file ") + fileOrDirectory.getAbsolutePath()); throw new RuntimeException("Unable to delete " + (fileOrDirectory.isDirectory() ? "directory " : "file ") + fileOrDirectory.getAbsolutePath());
} }
} }
public static void setupStorageSymlinks(final Context context) { static void setupStorageSymlinks(final Context context) {
final String LOG_TAG = "termux-storage"; final String LOG_TAG = "termux-storage";
new Thread() { new Thread() {
public void run() { public void run() {
try { try {
File storageDir = new File(TermuxService.HOME_PATH, "storage"); File storageDir = new File(TermuxService.HOME_PATH, "storage");
if (storageDir.exists() && !storageDir.delete()) { if (storageDir.exists()) {
Log.e(LOG_TAG, "Could not delete old $HOME/storage"); try {
return; deleteFolder(storageDir);
} catch (IOException e) {
Log.e(LOG_TAG, "Could not delete old $HOME/storage, " + e.getMessage());
return;
}
} }
if (!storageDir.mkdirs()) { if (!storageDir.mkdirs()) {

View File

@@ -8,6 +8,7 @@ import android.content.Intent;
import android.database.Cursor; import android.database.Cursor;
import android.database.MatrixCursor; import android.database.MatrixCursor;
import android.net.Uri; import android.net.Uri;
import android.os.Environment;
import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.util.Log; import android.util.Log;
@@ -17,6 +18,9 @@ import com.termux.terminal.EmulatorDebug;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException;
import androidx.annotation.NonNull;
public class TermuxOpenReceiver extends BroadcastReceiver { public class TermuxOpenReceiver extends BroadcastReceiver {
@@ -28,23 +32,10 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
return; return;
} }
final boolean isExternalUrl = data.getScheme() != null && !data.getScheme().equals("file");
if (isExternalUrl) {
Intent viewIntent = new Intent(Intent.ACTION_VIEW, data);
viewIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
try {
context.startActivity(viewIntent);
} catch (ActivityNotFoundException e) {
Log.e(EmulatorDebug.LOG_TAG, "termux-open: No app handles the url " + data);
}
return;
}
final String filePath = data.getPath(); final String filePath = data.getPath();
final String contentTypeExtra = intent.getStringExtra("content-type"); final String contentTypeExtra = intent.getStringExtra("content-type");
final boolean useChooser = intent.getBooleanExtra("chooser", false); final boolean useChooser = intent.getBooleanExtra("chooser", false);
final String intentAction = intent.getAction() == null ? Intent.ACTION_VIEW : intent.getAction(); final String intentAction = intent.getAction() == null ? Intent.ACTION_VIEW : intent.getAction();
switch (intentAction) { switch (intentAction) {
case Intent.ACTION_SEND: case Intent.ACTION_SEND:
case Intent.ACTION_VIEW: case Intent.ACTION_VIEW:
@@ -55,6 +46,24 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
break; break;
} }
final boolean isExternalUrl = data.getScheme() != null && !data.getScheme().equals("file");
if (isExternalUrl) {
Intent urlIntent = new Intent(intentAction, data);
if (intentAction.equals(Intent.ACTION_SEND)) {
urlIntent.putExtra(Intent.EXTRA_TEXT, data.toString());
urlIntent.setData(null);
} else if (contentTypeExtra != null) {
urlIntent.setDataAndType(data, contentTypeExtra);
}
urlIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
try {
context.startActivity(urlIntent);
} catch (ActivityNotFoundException e) {
Log.e(EmulatorDebug.LOG_TAG, "termux-open: No app handles the url " + data);
}
return;
}
final File fileToShare = new File(filePath); final File fileToShare = new File(filePath);
if (!(fileToShare.isFile() && fileToShare.canRead())) { if (!(fileToShare.isFile() && fileToShare.canRead())) {
Log.e(EmulatorDebug.LOG_TAG, "termux-open: Not a readable file: '" + fileToShare.getAbsolutePath() + "'"); Log.e(EmulatorDebug.LOG_TAG, "termux-open: Not a readable file: '" + fileToShare.getAbsolutePath() + "'");
@@ -69,7 +78,7 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
if (contentTypeExtra == null) { if (contentTypeExtra == null) {
String fileName = fileToShare.getName(); String fileName = fileToShare.getName();
int lastDotIndex = fileName.lastIndexOf('.'); int lastDotIndex = fileName.lastIndexOf('.');
String fileExtension = fileName.substring(lastDotIndex + 1, fileName.length()); String fileExtension = fileName.substring(lastDotIndex + 1);
MimeTypeMap mimeTypes = MimeTypeMap.getSingleton(); MimeTypeMap mimeTypes = MimeTypeMap.getSingleton();
// Lower casing makes it work with e.g. "JPG": // Lower casing makes it work with e.g. "JPG":
contentTypeToUse = mimeTypes.getMimeTypeFromExtension(fileExtension.toLowerCase()); contentTypeToUse = mimeTypes.getMimeTypeFromExtension(fileExtension.toLowerCase());
@@ -78,7 +87,7 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
contentTypeToUse = contentTypeExtra; contentTypeToUse = contentTypeExtra;
} }
Uri uriToShare = Uri.withAppendedPath(Uri.parse("content://com.termux.files/"), filePath); Uri uriToShare = Uri.parse("content://com.termux.files" + fileToShare.getAbsolutePath());
if (Intent.ACTION_SEND.equals(intentAction)) { if (Intent.ACTION_SEND.equals(intentAction)) {
sendIntent.putExtra(Intent.EXTRA_STREAM, uriToShare); sendIntent.putExtra(Intent.EXTRA_STREAM, uriToShare);
@@ -106,7 +115,7 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
} }
@Override @Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
File file = new File(uri.getPath()); File file = new File(uri.getPath());
if (projection == null) { if (projection == null) {
@@ -143,28 +152,38 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
} }
@Override @Override
public String getType(Uri uri) { public String getType(@NonNull Uri uri) {
return null; return null;
} }
@Override @Override
public Uri insert(Uri uri, ContentValues values) { public Uri insert(@NonNull Uri uri, ContentValues values) {
return null; return null;
} }
@Override @Override
public int delete(Uri uri, String selection, String[] selectionArgs) { public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {
return 0; return 0;
} }
@Override @Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) {
return 0; return 0;
} }
@Override @Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
File file = new File(uri.getPath()); File file = new File(uri.getPath());
try {
String path = file.getCanonicalPath();
String storagePath = Environment.getExternalStorageDirectory().getCanonicalPath();
// See https://support.google.com/faqs/answer/7496913:
if (!(path.startsWith(TermuxService.FILES_PATH) || path.startsWith(storagePath))) {
throw new IllegalArgumentException("Invalid path: " + path);
}
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
} }
} }

View File

@@ -3,28 +3,49 @@ package com.termux.app;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.annotation.IntDef;
import android.util.Log; import android.util.Log;
import android.util.TypedValue; import android.util.TypedValue;
import android.widget.Toast; import android.widget.Toast;
import com.termux.terminal.TerminalSession; import com.termux.terminal.TerminalSession;
import org.json.JSONArray;
import org.json.JSONException;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Properties; import java.util.Properties;
import androidx.annotation.IntDef;
final class TermuxPreferences { final class TermuxPreferences {
@IntDef({BELL_VIBRATE, BELL_BEEP, BELL_IGNORE}) @IntDef({BELL_VIBRATE, BELL_BEEP, BELL_IGNORE})
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
public @interface AsciiBellBehaviour { @interface AsciiBellBehaviour {
} }
final static class KeyboardShortcut {
KeyboardShortcut(int codePoint, int shortcutAction) {
this.codePoint = codePoint;
this.shortcutAction = shortcutAction;
}
final int codePoint;
final int shortcutAction;
}
static final int SHORTCUT_ACTION_CREATE_SESSION = 1;
static final int SHORTCUT_ACTION_NEXT_SESSION = 2;
static final int SHORTCUT_ACTION_PREVIOUS_SESSION = 3;
static final int SHORTCUT_ACTION_RENAME_SESSION = 4;
static final int BELL_VIBRATE = 1; static final int BELL_VIBRATE = 1;
static final int BELL_BEEP = 2; static final int BELL_BEEP = 2;
static final int BELL_IGNORE = 3; static final int BELL_IGNORE = 3;
@@ -32,12 +53,13 @@ final class TermuxPreferences {
private final int MIN_FONTSIZE; private final int MIN_FONTSIZE;
private static final int MAX_FONTSIZE = 256; private static final int MAX_FONTSIZE = 256;
private static final String FULLSCREEN_KEY = "fullscreen";
private static final String SHOW_EXTRA_KEYS_KEY = "show_extra_keys"; private static final String SHOW_EXTRA_KEYS_KEY = "show_extra_keys";
private static final String FONTSIZE_KEY = "fontsize"; private static final String FONTSIZE_KEY = "fontsize";
private static final String CURRENT_SESSION_KEY = "current_session"; private static final String CURRENT_SESSION_KEY = "current_session";
private static final String SCREEN_ALWAYS_ON_KEY = "screen_always_on";
private boolean mFullScreen; private String mUseDarkUI;
private boolean mScreenAlwaysOn;
private int mFontSize; private int mFontSize;
@AsciiBellBehaviour @AsciiBellBehaviour
@@ -46,6 +68,17 @@ final class TermuxPreferences {
boolean mBackIsEscape; boolean mBackIsEscape;
boolean mShowExtraKeys; boolean mShowExtraKeys;
String[][] mExtraKeys;
final List<KeyboardShortcut> shortcuts = new ArrayList<>();
/**
* If value is not in the range [min, max], set it to either min or max.
*/
static int clamp(int value, int min, int max) {
return Math.min(Math.max(value, min), max);
}
TermuxPreferences(Context context) { TermuxPreferences(Context context) {
reloadFromProperties(context); reloadFromProperties(context);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
@@ -56,8 +89,8 @@ final class TermuxPreferences {
// to prevent invisible text due to zoom be mistake: // to prevent invisible text due to zoom be mistake:
MIN_FONTSIZE = (int) (4f * dipInPixels); MIN_FONTSIZE = (int) (4f * dipInPixels);
mFullScreen = prefs.getBoolean(FULLSCREEN_KEY, false); mShowExtraKeys = prefs.getBoolean(SHOW_EXTRA_KEYS_KEY, true);
mShowExtraKeys = prefs.getBoolean(SHOW_EXTRA_KEYS_KEY, false); mScreenAlwaysOn = prefs.getBoolean(SCREEN_ALWAYS_ON_KEY, false);
// http://www.google.com/design/spec/style/typography.html#typography-line-height // http://www.google.com/design/spec/style/typography.html#typography-line-height
int defaultFontSize = Math.round(12 * dipInPixels); int defaultFontSize = Math.round(12 * dipInPixels);
@@ -69,20 +102,7 @@ final class TermuxPreferences {
} catch (NumberFormatException | ClassCastException e) { } catch (NumberFormatException | ClassCastException e) {
mFontSize = defaultFontSize; mFontSize = defaultFontSize;
} }
mFontSize = Math.max(MIN_FONTSIZE, Math.min(mFontSize, MAX_FONTSIZE)); mFontSize = clamp(mFontSize, MIN_FONTSIZE, MAX_FONTSIZE);
}
boolean isFullScreen() {
return mFullScreen;
}
void setFullScreen(Context context, boolean newValue) {
mFullScreen = newValue;
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(FULLSCREEN_KEY, newValue).apply();
}
boolean isShowExtraKeys() {
return mShowExtraKeys;
} }
boolean toggleShowExtraKeys(Context context) { boolean toggleShowExtraKeys(Context context) {
@@ -103,6 +123,19 @@ final class TermuxPreferences {
prefs.edit().putString(FONTSIZE_KEY, Integer.toString(mFontSize)).apply(); prefs.edit().putString(FONTSIZE_KEY, Integer.toString(mFontSize)).apply();
} }
boolean isScreenAlwaysOn() {
return mScreenAlwaysOn;
}
boolean isUsingBlackUI() {
return mUseDarkUI.toLowerCase().equals("true");
}
void setScreenAlwaysOn(Context context, boolean newValue) {
mScreenAlwaysOn = newValue;
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(SCREEN_ALWAYS_ON_KEY, newValue).apply();
}
static void storeCurrentSession(Context context, TerminalSession session) { static void storeCurrentSession(Context context, TerminalSession session) {
PreferenceManager.getDefaultSharedPreferences(context).edit().putString(TermuxPreferences.CURRENT_SESSION_KEY, session.mHandle).apply(); PreferenceManager.getDefaultSharedPreferences(context).edit().putString(TermuxPreferences.CURRENT_SESSION_KEY, session.mHandle).apply();
} }
@@ -115,62 +148,63 @@ final class TermuxPreferences {
} }
return null; return null;
} }
void reloadFromProperties(Context context) {
File propsFile = new File(TermuxService.HOME_PATH + "/.termux/termux.properties");
if (!propsFile.exists())
propsFile = new File(TermuxService.HOME_PATH + "/.config/termux/termux.properties");
public void reloadFromProperties(Context context) { Properties props = new Properties();
try { try {
File propsFile = new File(TermuxService.HOME_PATH + "/.termux/termux.properties");
if (!propsFile.exists())
propsFile = new File(TermuxService.HOME_PATH + "/.config/termux/termux.properties");
Properties props = new Properties();
if (propsFile.isFile() && propsFile.canRead()) { if (propsFile.isFile() && propsFile.canRead()) {
try (FileInputStream in = new FileInputStream(propsFile)) { try (FileInputStream in = new FileInputStream(propsFile)) {
props.load(in); props.load(new InputStreamReader(in, StandardCharsets.UTF_8));
} }
} }
} catch (IOException e) {
switch (props.getProperty("bell-character", "vibrate")) { Toast.makeText(context, "Could not open properties file termux.properties.", Toast.LENGTH_LONG).show();
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"));
shortcuts.clear();
parseAction("shortcut.create-session", SHORTCUT_ACTION_CREATE_SESSION, props);
parseAction("shortcut.next-session", SHORTCUT_ACTION_NEXT_SESSION, props);
parseAction("shortcut.previous-session", SHORTCUT_ACTION_PREVIOUS_SESSION, props);
parseAction("shortcut.rename-session", SHORTCUT_ACTION_RENAME_SESSION, props);
} catch (Exception e) {
Toast.makeText(context, "Error loading properties: " + e.getMessage(), Toast.LENGTH_LONG).show();
Log.e("termux", "Error loading props", e); Log.e("termux", "Error loading props", e);
} }
}
public static final int SHORTCUT_ACTION_CREATE_SESSION = 1; switch (props.getProperty("bell-character", "vibrate")) {
public static final int SHORTCUT_ACTION_NEXT_SESSION = 2; case "beep":
public static final int SHORTCUT_ACTION_PREVIOUS_SESSION = 3; mBellBehaviour = BELL_BEEP;
public static final int SHORTCUT_ACTION_RENAME_SESSION = 4; break;
case "ignore":
public final static class KeyboardShortcut { mBellBehaviour = BELL_IGNORE;
break;
public KeyboardShortcut(int codePoint, int shortcutAction) { default: // "vibrate".
this.codePoint = codePoint; mBellBehaviour = BELL_VIBRATE;
this.shortcutAction = shortcutAction; break;
} }
final int codePoint; mUseDarkUI = props.getProperty("use-black-ui", "false");
final int shortcutAction;
}
final List<KeyboardShortcut> shortcuts = new ArrayList<>(); try {
JSONArray arr = new JSONArray(props.getProperty("extra-keys", "[['ESC', 'TAB', 'CTRL', 'ALT', '-', 'DOWN', 'UP']]"));
mExtraKeys = new String[arr.length()][];
for (int i = 0; i < arr.length(); i++) {
JSONArray line = arr.getJSONArray(i);
mExtraKeys[i] = new String[line.length()];
for (int j = 0; j < line.length(); j++) {
mExtraKeys[i][j] = line.getString(j);
}
}
} catch (JSONException e) {
Toast.makeText(context, "Could not load the extra-keys property from the config: " + e.toString(), Toast.LENGTH_LONG).show();
Log.e("termux", "Error loading props", e);
mExtraKeys = new String[0][];
}
mBackIsEscape = "escape".equals(props.getProperty("back-key", "back"));
shortcuts.clear();
parseAction("shortcut.create-session", SHORTCUT_ACTION_CREATE_SESSION, props);
parseAction("shortcut.next-session", SHORTCUT_ACTION_NEXT_SESSION, props);
parseAction("shortcut.previous-session", SHORTCUT_ACTION_PREVIOUS_SESSION, props);
parseAction("shortcut.rename-session", SHORTCUT_ACTION_RENAME_SESSION, props);
}
private void parseAction(String name, int shortcutAction, Properties props) { private void parseAction(String name, int shortcutAction, Properties props) {
String value = props.getProperty(name); String value = props.getProperty(name);

View File

@@ -2,19 +2,22 @@ package com.termux.app;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Notification; import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.app.Service; import android.app.Service;
import android.content.ActivityNotFoundException;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.res.Resources; import android.content.res.Resources;
import android.net.Uri; import android.net.Uri;
import android.net.wifi.WifiManager; import android.net.wifi.WifiManager;
import android.os.Binder; import android.os.Binder;
import android.os.Build;
import android.os.Handler; import android.os.Handler;
import android.os.IBinder; import android.os.IBinder;
import android.os.PowerManager; import android.os.PowerManager;
import android.support.v4.content.WakefulBroadcastReceiver; import android.provider.Settings;
import android.util.Log; import android.util.Log;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
@@ -41,6 +44,8 @@ import java.util.List;
*/ */
public final class TermuxService extends Service implements SessionChangedCallback { public final class TermuxService extends Service implements SessionChangedCallback {
private static final String NOTIFICATION_CHANNEL_ID = "termux_notification_channel";
/** Note that this is a symlink on the Android M preview. */ /** Note that this is a symlink on the Android M preview. */
@SuppressLint("SdCardPath") @SuppressLint("SdCardPath")
public static final String FILES_PATH = "/data/data/com.termux/files"; public static final String FILES_PATH = "/data/data/com.termux/files";
@@ -104,10 +109,27 @@ public final class TermuxService extends Service implements SessionChangedCallba
mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, EmulatorDebug.LOG_TAG); mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, EmulatorDebug.LOG_TAG);
mWakeLock.acquire(); mWakeLock.acquire();
WifiManager wm = (WifiManager) getSystemService(Context.WIFI_SERVICE); // http://tools.android.com/tech-docs/lint-in-studio-2-3#TOC-WifiManager-Leak
WifiManager wm = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
mWifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, EmulatorDebug.LOG_TAG); mWifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, EmulatorDebug.LOG_TAG);
mWifiLock.acquire(); mWifiLock.acquire();
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
String packageName = getPackageName();
if (!pm.isIgnoringBatteryOptimizations(packageName)) {
Intent whitelist = new Intent();
whitelist.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
whitelist.setData(Uri.parse("package:" + packageName));
whitelist.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
try {
startActivity(whitelist);
} catch (ActivityNotFoundException e) {
Log.e(EmulatorDebug.LOG_TAG, "Failed to call ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS", e);
}
}
}
updateNotification(); updateNotification();
} }
} else if (ACTION_UNLOCK_WAKE.equals(action)) { } else if (ACTION_UNLOCK_WAKE.equals(action)) {
@@ -132,7 +154,8 @@ public final class TermuxService extends Service implements SessionChangedCallba
mBackgroundTasks.add(task); mBackgroundTasks.add(task);
updateNotification(); updateNotification();
} else { } else {
TerminalSession newSession = createTermSession(executablePath, arguments, cwd, false); boolean failsafe = intent.getBooleanExtra(TermuxActivity.TERMUX_FAILSAFE_SESSION_ACTION, false);
TerminalSession newSession = createTermSession(executablePath, arguments, cwd, failsafe);
// Transform executable path to session name, e.g. "/bin/do-something.sh" => "do something.sh". // Transform executable path to session name, e.g. "/bin/do-something.sh" => "do something.sh".
if (executablePath != null) { if (executablePath != null) {
@@ -152,11 +175,6 @@ public final class TermuxService extends Service implements SessionChangedCallba
Log.e(EmulatorDebug.LOG_TAG, "Unknown TermuxService action: '" + action + "'"); Log.e(EmulatorDebug.LOG_TAG, "Unknown TermuxService action: '" + action + "'");
} }
if ((flags & START_FLAG_REDELIVERY) == 0) {
// Service is started by WBR, not restarted by system, so release the WakeLock from WBR.
WakefulBroadcastReceiver.completeWakefulIntent(intent);
}
// If this service really do get killed, there is no point restarting it automatically - let the user do on next // If this service really do get killed, there is no point restarting it automatically - let the user do on next
// start of {@link Term): // start of {@link Term):
return Service.START_NOT_STICKY; return Service.START_NOT_STICKY;
@@ -169,11 +187,12 @@ public final class TermuxService extends Service implements SessionChangedCallba
@Override @Override
public void onCreate() { public void onCreate() {
setupNotificationChannel();
startForeground(NOTIFICATION_ID, buildNotification()); startForeground(NOTIFICATION_ID, buildNotification());
} }
/** Update the shown foreground service notification after making any changes that affect it. */ /** Update the shown foreground service notification after making any changes that affect it. */
private void updateNotification() { void updateNotification() {
if (mWakeLock == null && mTerminalSessions.isEmpty() && mBackgroundTasks.isEmpty()) { if (mWakeLock == null && mTerminalSessions.isEmpty() && mBackgroundTasks.isEmpty()) {
// Exit if we are updating after the user disabled all locks with no sessions or tasks running. // Exit if we are updating after the user disabled all locks with no sessions or tasks running.
stopSelf(); stopSelf();
@@ -207,14 +226,18 @@ public final class TermuxService extends Service implements SessionChangedCallba
builder.setOngoing(true); builder.setOngoing(true);
// If holding a wake or wifi lock consider the notification of high priority since it's using power, // If holding a wake or wifi lock consider the notification of high priority since it's using power,
// otherwise use a minimal priority since this is just a background service notification: // otherwise use a low priority
builder.setPriority((wakeLockHeld) ? Notification.PRIORITY_HIGH : Notification.PRIORITY_MIN); builder.setPriority((wakeLockHeld) ? Notification.PRIORITY_HIGH : Notification.PRIORITY_LOW);
// No need to show a timestamp: // No need to show a timestamp:
builder.setShowWhen(false); builder.setShowWhen(false);
// Background color for small notification icon: // Background color for small notification icon:
builder.setColor(0xFF000000); builder.setColor(0xFF607D8B);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder.setChannelId(NOTIFICATION_CHANNEL_ID);
}
Resources res = getResources(); Resources res = getResources();
Intent exitIntent = new Intent(this, TermuxService.class).setAction(ACTION_STOP_SERVICE); Intent exitIntent = new Intent(this, TermuxService.class).setAction(ACTION_STOP_SERVICE);
@@ -233,6 +256,18 @@ public final class TermuxService extends Service implements SessionChangedCallba
@Override @Override
public void onDestroy() { public void onDestroy() {
File termuxTmpDir = new File(TermuxService.PREFIX_PATH + "/tmp");
if (termuxTmpDir.exists()) {
try {
TermuxInstaller.deleteFolder(termuxTmpDir.getCanonicalFile());
} catch (Exception e) {
Log.e(EmulatorDebug.LOG_TAG, "Error while removing file at " + termuxTmpDir.getAbsolutePath(), e);
}
termuxTmpDir.mkdirs();
}
if (mWakeLock != null) mWakeLock.release(); if (mWakeLock != null) mWakeLock.release();
if (mWifiLock != null) mWifiLock.release(); if (mWifiLock != null) mWifiLock.release();
@@ -240,7 +275,6 @@ public final class TermuxService extends Service implements SessionChangedCallba
for (int i = 0; i < mTerminalSessions.size(); i++) for (int i = 0; i < mTerminalSessions.size(); i++)
mTerminalSessions.get(i).finishIfRunning(); mTerminalSessions.get(i).finishIfRunning();
mTerminalSessions.clear();
} }
public List<TerminalSession> getSessions() { public List<TerminalSession> getSessions() {
@@ -256,11 +290,13 @@ public final class TermuxService extends Service implements SessionChangedCallba
boolean isLoginShell = false; boolean isLoginShell = false;
if (executablePath == null) { if (executablePath == null) {
for (String shellBinary : new String[]{"login", "bash", "zsh"}) { if (!failSafe) {
File shellFile = new File(PREFIX_PATH + "/bin/" + shellBinary); for (String shellBinary : new String[]{"login", "bash", "zsh"}) {
if (shellFile.canExecute()) { File shellFile = new File(PREFIX_PATH + "/bin/" + shellBinary);
executablePath = shellFile.getAbsolutePath(); if (shellFile.canExecute()) {
break; executablePath = shellFile.getAbsolutePath();
break;
}
} }
} }
@@ -284,6 +320,12 @@ public final class TermuxService extends Service implements SessionChangedCallba
TerminalSession session = new TerminalSession(executablePath, cwd, args, env, this); TerminalSession session = new TerminalSession(executablePath, cwd, args, env, this);
mTerminalSessions.add(session); mTerminalSessions.add(session);
updateNotification(); updateNotification();
// Make sure that terminal styling is always applied.
Intent stylingIntent = new Intent("com.termux.app.reload_style");
stylingIntent.putExtra("com.termux.app.reload_style", "styling");
sendBroadcast(stylingIntent);
return session; return session;
} }
@@ -332,12 +374,22 @@ public final class TermuxService extends Service implements SessionChangedCallba
} }
public void onBackgroundJobExited(final BackgroundJob task) { public void onBackgroundJobExited(final BackgroundJob task) {
mHandler.post(new Runnable() { mHandler.post(() -> {
@Override mBackgroundTasks.remove(task);
public void run() { updateNotification();
mBackgroundTasks.remove(task);
updateNotification();
}
}); });
} }
private void setupNotificationChannel() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
String channelName = "Termux";
String channelDescription = "Notifications from Termux";
int importance = NotificationManager.IMPORTANCE_LOW;
NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName,importance);
channel.setDescription(channelDescription);
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
manager.createNotificationChannel(channel);
}
} }

View File

@@ -2,7 +2,6 @@ package com.termux.app;
import android.content.Context; import android.content.Context;
import android.media.AudioManager; import android.media.AudioManager;
import android.support.v4.widget.DrawerLayout;
import android.view.Gravity; import android.view.Gravity;
import android.view.InputDevice; import android.view.InputDevice;
import android.view.KeyEvent; import android.view.KeyEvent;
@@ -12,18 +11,20 @@ import android.view.inputmethod.InputMethodManager;
import com.termux.terminal.KeyHandler; import com.termux.terminal.KeyHandler;
import com.termux.terminal.TerminalEmulator; import com.termux.terminal.TerminalEmulator;
import com.termux.terminal.TerminalSession; import com.termux.terminal.TerminalSession;
import com.termux.view.TerminalKeyListener; import com.termux.view.TerminalViewClient;
import java.util.List; import java.util.List;
public final class TermuxKeyListener implements TerminalKeyListener { import androidx.drawerlayout.widget.DrawerLayout;
public final class TermuxViewClient implements TerminalViewClient {
final TermuxActivity mActivity; final TermuxActivity mActivity;
/** Keeping track of the special keys acting as Ctrl and Fn for the soft keyboard and other hardware keys. */ /** Keeping track of the special keys acting as Ctrl and Fn for the soft keyboard and other hardware keys. */
boolean mVirtualControlKeyDown, mVirtualFnKeyDown; boolean mVirtualControlKeyDown, mVirtualFnKeyDown;
public TermuxKeyListener(TermuxActivity activity) { public TermuxViewClient(TermuxActivity activity) {
this.mActivity = activity; this.mActivity = activity;
} }
@@ -73,8 +74,6 @@ public final class TermuxKeyListener implements TerminalKeyListener {
mActivity.getDrawer().openDrawer(Gravity.LEFT); mActivity.getDrawer().openDrawer(Gravity.LEFT);
} else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) { } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
mActivity.getDrawer().closeDrawers(); mActivity.getDrawer().closeDrawers();
} else if (unicodeChar == 'f'/* full screen */) {
mActivity.toggleImmersive();
} else if (unicodeChar == 'k'/* keyboard */) { } else if (unicodeChar == 'k'/* keyboard */) {
InputMethodManager imm = (InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE); InputMethodManager imm = (InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
@@ -114,12 +113,12 @@ public final class TermuxKeyListener implements TerminalKeyListener {
@Override @Override
public boolean readControlKey() { public boolean readControlKey() {
return (mActivity.mExtraKeysView != null && mActivity.mExtraKeysView.readControlButton()) || mVirtualControlKeyDown; return (mActivity.mExtraKeysView != null && mActivity.mExtraKeysView.readSpecialButton(ExtraKeysView.SpecialButton.CTRL)) || mVirtualControlKeyDown;
} }
@Override @Override
public boolean readAltKey() { public boolean readAltKey() {
return (mActivity.mExtraKeysView != null && mActivity.mExtraKeysView.readAltButton()); return (mActivity.mExtraKeysView != null && mActivity.mExtraKeysView.readSpecialButton(ExtraKeysView.SpecialButton.ALT));
} }
@Override @Override
@@ -211,6 +210,7 @@ public final class TermuxKeyListener implements TerminalKeyListener {
// Writing mode: // Writing mode:
case 'q': case 'q':
case 'k':
mActivity.toggleShowExtraKeys(); mActivity.toggleShowExtraKeys();
break; break;
} }
@@ -256,6 +256,11 @@ public final class TermuxKeyListener implements TerminalKeyListener {
return false; return false;
} }
@Override
public boolean onLongPress(MotionEvent event) {
return false;
}
/** Handle dedicated volume buttons as virtual keys if applicable. */ /** Handle dedicated volume buttons as virtual keys if applicable. */
private boolean handleVirtualKeys(int keyCode, KeyEvent event, boolean down) { private boolean handleVirtualKeys(int keyCode, KeyEvent event, boolean down) {
InputDevice inputDevice = event.getDevice(); InputDevice inputDevice = event.getDevice();

View File

@@ -75,7 +75,7 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
row.add(Root.COLUMN_TITLE, applicationName); row.add(Root.COLUMN_TITLE, applicationName);
row.add(Root.COLUMN_MIME_TYPES, ALL_MIME_TYPES); row.add(Root.COLUMN_MIME_TYPES, ALL_MIME_TYPES);
row.add(Root.COLUMN_AVAILABLE_BYTES, BASE_DIR.getFreeSpace()); row.add(Root.COLUMN_AVAILABLE_BYTES, BASE_DIR.getFreeSpace());
row.add(Root.COLUMN_ICON, R.mipmap.ic_launcher); row.add(Root.COLUMN_ICON, R.drawable.ic_launcher);
return result; return result;
} }
@@ -195,7 +195,7 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
final String name = file.getName(); final String name = file.getName();
final int lastDot = name.lastIndexOf('.'); final int lastDot = name.lastIndexOf('.');
if (lastDot >= 0) { if (lastDot >= 0) {
final String extension = name.substring(lastDot + 1); final String extension = name.substring(lastDot + 1).toLowerCase();
final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
if (mime != null) return mime; if (mime != null) return mime;
} }
@@ -236,7 +236,7 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
row.add(Document.COLUMN_MIME_TYPE, mimeType); row.add(Document.COLUMN_MIME_TYPE, mimeType);
row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified()); row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified());
row.add(Document.COLUMN_FLAGS, flags); row.add(Document.COLUMN_FLAGS, flags);
row.add(Document.COLUMN_ICON, R.mipmap.ic_launcher); row.add(Document.COLUMN_ICON, R.drawable.ic_launcher);
} }
} }

View File

@@ -2,7 +2,6 @@ package com.termux.filepicker;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor; import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
@@ -83,17 +82,7 @@ public class TermuxFileReceiverActivity extends Activity {
void showErrorDialogAndQuit(String message) { void showErrorDialogAndQuit(String message) {
mFinishOnDismissNameDialog = false; mFinishOnDismissNameDialog = false;
new AlertDialog.Builder(this).setMessage(message).setOnDismissListener(new DialogInterface.OnDismissListener() { new AlertDialog.Builder(this).setMessage(message).setOnDismissListener(dialog -> finish()).setPositiveButton(android.R.string.ok, (dialog, which) -> finish()).show();
@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) { void handleContentUri(final Uri uri, String subjectFromIntent) {
@@ -119,54 +108,40 @@ public class TermuxFileReceiverActivity extends Activity {
} }
void promptNameAndSave(final InputStream in, final String attachmentFileName) { 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() { DialogUtils.textInput(this, R.string.file_received_title, attachmentFileName, R.string.file_received_edit_button, text -> {
@Override File outFile = saveStreamWithName(in, text);
public void onTextSet(String text) { if (outFile == null) return;
File outFile = saveStreamWithName(in, text);
if (outFile == null) return;
final File editorProgramFile = new File(EDITOR_PROGRAM); final File editorProgramFile = new File(EDITOR_PROGRAM);
if (!editorProgramFile.isFile()) { if (!editorProgramFile.isFile()) {
showErrorDialogAndQuit("The following file does not exist:\n$HOME/bin/termux-file-editor\n\n" 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."); + "Create this file as a script or a symlink - it will be called with the received file as only argument.");
return; return;
} }
// Do this for the user if necessary: // Do this for the user if necessary:
//noinspection ResultOfMethodCallIgnored //noinspection ResultOfMethodCallIgnored
editorProgramFile.setExecutable(true); editorProgramFile.setExecutable(true);
final Uri scriptUri = new Uri.Builder().scheme("file").path(EDITOR_PROGRAM).build(); final Uri scriptUri = new Uri.Builder().scheme("file").path(EDITOR_PROGRAM).build();
Intent executeIntent = new Intent(TermuxService.ACTION_EXECUTE, scriptUri); Intent executeIntent = new Intent(TermuxService.ACTION_EXECUTE, scriptUri);
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class); executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
executeIntent.putExtra(TermuxService.EXTRA_ARGUMENTS, new String[]{outFile.getAbsolutePath()}); executeIntent.putExtra(TermuxService.EXTRA_ARGUMENTS, new String[]{outFile.getAbsolutePath()});
startService(executeIntent); startService(executeIntent);
finish(); finish();
} },
R.string.file_received_open_folder_button, text -> {
if (saveStreamWithName(in, text) == null) return;
Intent executeIntent = new Intent(TermuxService.ACTION_EXECUTE);
executeIntent.putExtra(TermuxService.EXTRA_CURRENT_WORKING_DIRECTORY, TERMUX_RECEIVEDIR);
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
startService(executeIntent);
finish();
}, },
R.string.file_received_open_folder_button, new DialogUtils.TextSetListener() { android.R.string.cancel, text -> finish(), dialog -> {
@Override if (mFinishOnDismissNameDialog) finish();
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();
}
}); });
} }

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@android:color/black"/>
<foreground android:drawable="@drawable/ic_foreground"/>
</adaptive-icon>

View File

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

View File

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

View File

@@ -0,0 +1,28 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="108dp"
android:width="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<!-- Keep in sync with non-adaptive ic_launcher.xml -->
<path
android:fillColor="#FFFFFF"
android:pathData="M34,38
h6
l12,16
l-12,16
h-6
l12,-16
"
/>
<path
android:fillColor="#FFFFFF"
android:pathData="M56,66
h18
v4
h-18
"
/>
</vector>

View File

@@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="108dp"
android:width="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#000000"
android:pathData="M18,54
A36,36 0 1,1 90,54
A36,36 0 1,1 18,54 Z" />
<!-- Keep in sync with adaptive ic_foreground.xml: -->
<path
android:fillColor="#FFFFFF"
android:pathData="M34,38
h6
l12,16
l-12,16
h-6
l12,-16
"
/>
<path
android:fillColor="#FFFFFF"
android:pathData="M56,66
h18
v4
h-18
"
/>
</vector>

View File

@@ -9,7 +9,7 @@
<!-- Screen border. --> <!-- Screen border. -->
<path android:fillColor="#00000000" <path android:fillColor="#00000000"
android:strokeColor="#000" android:strokeColor="#FFF"
android:strokeWidth="3" android:strokeWidth="3"
android:pathData="M7,4 android:pathData="M7,4
l34,0 l34,0
@@ -23,7 +23,7 @@
/> />
<!-- Block cursor. --> <!-- Block cursor. -->
<path android:fillColor="#000" <path android:fillColor="#FFF"
android:pathData="M14,14 android:pathData="M14,14
l5,0 l5,0
l0,10 l0,10

View File

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

View File

@@ -4,4 +4,4 @@
<item> <item>
<color android:color="@android:color/white" /> <color android:color="@android:color/white" />
</item> </item>
</ripple> </ripple>

View File

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

View File

@@ -1,9 +1,10 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical"
android:fitsSystemWindows="true">
<android.support.v4.widget.DrawerLayout <androidx.drawerlayout.widget.DrawerLayout
android:id="@+id/drawer_layout" android:id="@+id/drawer_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_alignParentTop="true" android:layout_alignParentTop="true"
@@ -14,6 +15,8 @@
android:id="@+id/terminal_view" android:id="@+id/terminal_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginRight="3dp"
android:layout_marginLeft="3dp"
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:scrollbarThumbVertical="@drawable/terminal_scroll_shape" android:scrollbarThumbVertical="@drawable/terminal_scroll_shape"
android:scrollbars="vertical" /> android:scrollbars="vertical" />
@@ -63,13 +66,13 @@
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
</android.support.v4.widget.DrawerLayout> </androidx.drawerlayout.widget.DrawerLayout>
<android.support.v4.view.ViewPager <androidx.viewpager.widget.ViewPager
android:id="@+id/viewpager" android:id="@+id/viewpager"
android:visibility="gone" android:visibility="gone"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="40dp" android:layout_height="37.5dp"
android:background="@android:drawable/screen_background_dark_transparent" android:background="@android:drawable/screen_background_dark_transparent"
android:layout_alignParentBottom="true" /> android:layout_alignParentBottom="true" />
</RelativeLayout> </RelativeLayout>

View File

@@ -8,6 +8,7 @@
android:maxLines="1" android:maxLines="1"
android:inputType="text" android:inputType="text"
android:textColor="@android:color/white" android:textColor="@android:color/white"
android:textColorHighlight="@android:color/darker_gray"
android:paddingTop="0dp" android:paddingTop="0dp"
android:textCursorDrawable="@null" android:textCursorDrawable="@null"
android:paddingBottom="0dp" android:paddingBottom="0dp"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 338 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 331 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 505 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 691 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -1,56 +1,51 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="application_name">Termux</string> <string name="application_name">Termux</string>
<string name="shared_user_label">Termux user</string> <string name="shared_user_label">Termux user</string>
<string name="new_session">New session</string> <string name="new_session">New session</string>
<string name="new_session_failsafe">Failsafe</string> <string name="new_session_failsafe">Failsafe</string>
<string name="toggle_soft_keyboard">Keyboard</string> <string name="toggle_soft_keyboard">Keyboard</string>
<string name="reset_terminal">Reset</string> <string name="reset_terminal">Reset</string>
<string name="style_terminal">Style</string> <string name="style_terminal">Style</string>
<string name="toggle_fullscreen">Fullscreen</string> <string name="share_transcript_title">Terminal transcript</string>
<string name="share_transcript_title">Terminal transcript</string> <string name="help">Help</string>
<string name="help">Help</string> <string name="toggle_keep_screen_on">Keep screen on</string>
<string name="bootstrap_installer_body">Installing…</string> <string name="bootstrap_installer_body">Installing…</string>
<string name="bootstrap_error_title">Unable to install</string> <string name="bootstrap_error_title">Unable to install</string>
<string name="bootstrap_error_body">Termux was unable to install the bootstrap packages.\n\nCheck your network connection and try again.</string> <string name="bootstrap_error_body">Termux was unable to install the bootstrap packages.\n\nCheck your network connection and try again.</string>
<string name="bootstrap_error_abort">Abort</string> <string name="bootstrap_error_abort">Abort</string>
<string name="bootstrap_error_try_again">Try again</string> <string name="bootstrap_error_try_again">Try again</string>
<string name="bootstrap_error_not_primary_user_message">Termux can only be installed on the primary user account.</string> <string name="bootstrap_error_not_primary_user_message">Termux can only be installed on the primary user account.</string>
<string name="max_terminals_reached_title">Max terminals reached</string> <string name="max_terminals_reached_title">Max terminals reached</string>
<string name="max_terminals_reached_message">Close down existing ones before creating new.</string> <string name="max_terminals_reached_message">Close down existing ones before creating new.</string>
<string name="reset_toast_notification">Terminal reset.</string> <string name="reset_toast_notification">Terminal reset.</string>
<string name="select_url">Select URL</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_url_dialog_title">Click URL to copy or long press to open</string>
<string name="select_all_and_share">Share transcript</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_no_found">No URL found in the terminal.</string>
<string name="select_url_copied_to_clipboard">URL copied to clipboard</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="share_transcript_chooser_title">Send text to:</string>
<string name="paste_text">Paste</string> <string name="kill_process">Kill process (%d)</string>
<string name="copy_text">Copy</string> <string name="confirm_kill_process">Really kill this session?</string>
<string name="text_selection_more">More…</string>
<string name="kill_process">Kill process (%d)</string> <string name="session_rename_title">Set session name</string>
<string name="confirm_kill_process">Really kill this session?</string> <string name="session_rename_positive_button">Set</string>
<string name="session_new_named_title">New named session</string>
<string name="session_new_named_positive_button">Create</string>
<string name="session_rename_title">Set session name</string> <string name="styling_not_installed">The Termux:Style add-on is not installed.</string>
<string name="session_rename_positive_button">Set</string> <string name="styling_install">Install</string>
<string name="session_new_named_title">New named session</string>
<string name="session_new_named_positive_button">Create</string>
<string name="styling_not_installed">The Termux:Style add-on is not installed.</string> <string name="notification_action_exit">Exit</string>
<string name="styling_install">Install</string> <string name="notification_action_wake_lock">Acquire wakelock</string>
<string name="notification_action_wake_unlock">Release wakelock</string>
<string name="notification_action_exit">Exit</string>
<string name="notification_action_wake_lock">Acquire wakelock</string>
<string name="notification_action_wake_unlock">Release wakelock</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>
<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> </resources>

View File

@@ -1,8 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:android="http://schemas.android.com/apk/res/android"> <resources xmlns:android="http://schemas.android.com/apk/res/android">
<!-- See https://developer.android.com/training/material/theme.html for how to customize the Material theme. -->
<!-- NOTE: Cannot use "Light." since it hides the terminal scrollbar on the default black background. -->
<style name="Theme.Termux" parent="@android:style/Theme.Material.Light.NoActionBar"> <style name="Theme.Termux" parent="@android:style/Theme.Material.Light.NoActionBar">
<item name="android:statusBarColor">#000000</item> <item name="android:statusBarColor">#000000</item>
<item name="android:colorPrimary">#FF000000</item> <item name="android:colorPrimary">#FF000000</item>
@@ -15,14 +13,37 @@
selecting text on pre-6.0 (non-floating toolbar). --> selecting text on pre-6.0 (non-floating toolbar). -->
<item name="android:windowActionModeOverlay">true</item> <item name="android:windowActionModeOverlay">true</item>
<item name="android:windowTranslucentStatus">true</item>
<item name="android:windowTranslucentNavigation">true</item>
<!-- https://developer.android.com/training/tv/start/start.html#transition-color --> <!-- https://developer.android.com/training/tv/start/start.html#transition-color -->
<item name="android:windowAllowReturnTransitionOverlap">true</item> <item name="android:windowAllowReturnTransitionOverlap">true</item>
<item name="android:windowAllowEnterTransitionOverlap">true</item> <item name="android:windowAllowEnterTransitionOverlap">true</item>
</style> </style>
<style name="TermuxAlertDialogStyle" parent="@android:style/Theme.Material.Light.Dialog.Alert"> <style name="TermuxAlertDialogStyle" parent="@android:style/Theme.Material.Light.Dialog.Alert">
<!-- Seen in buttons on alert dialog: --> <!-- Seen in buttons on alert dialog: -->
<item name="android:colorAccent">#212121</item> <item name="android:colorAccent">#212121</item>
</style> </style>
<!-- See https://developer.android.com/training/material/theme.html for how to customize the Material theme. -->
<!-- NOTE: Cannot use "Light." since it hides the terminal scrollbar on the default black background. -->
<style name="Theme.Termux.Black" parent="@android:style/Theme.Material.NoActionBar">
<item name="android:statusBarColor">#000000</item>
<item name="android:colorPrimary">#FF000000</item>
<item name="android:windowBackground">@android:color/black</item>
<!-- Seen in buttons on left drawer: -->
<item name="android:colorAccent">#FDFDFD</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>
<item name="android:windowTranslucentStatus">true</item>
<item name="android:windowTranslucentNavigation">true</item>
<!-- https://developer.android.com/training/tv/start/start.html#transition-color -->
<item name="android:windowAllowReturnTransitionOverlap">true</item>
<item name="android:windowAllowEnterTransitionOverlap">true</item>
</style>
</resources> </resources>

View File

@@ -1,5 +0,0 @@
<?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

@@ -1,5 +1,6 @@
<shortcuts xmlns:tools="http://schemas.android.com/tools" <shortcuts xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"> xmlns:android="http://schemas.android.com/apk/res/android">
<shortcut <shortcut
android:shortcutId="new_session" android:shortcutId="new_session"
android:enabled="true" android:enabled="true"
@@ -11,4 +12,19 @@
android:targetPackage="com.termux" android:targetPackage="com.termux"
android:targetClass="com.termux.app.TermuxActivity"/> android:targetClass="com.termux.app.TermuxActivity"/>
</shortcut> </shortcut>
<shortcut
android:shortcutId="new_failsafe_session"
android:enabled="true"
android:icon="@drawable/ic_new_session"
android:shortcutShortLabel="@string/new_session_failsafe"
tools:targetApi="n_mr1">
<intent
android:action="android.intent.action.RUN"
android:targetPackage="com.termux"
android:targetClass="com.termux.app.TermuxActivity">
<extra android:name="com.termux.app.failsafe_session" android:value="true" />
</intent>
</shortcut>
</shortcuts> </shortcuts>

View File

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

@@ -1,20 +1,13 @@
#!/bin/sh #!/bin/sh
set -e -u
for DENSITY in mdpi hdpi xhdpi xxhdpi xxxhdpi; do for APP in api boot styling tasker widget; do
FOLDER=../app/src/main/res/mipmap-$DENSITY APPDIR=../../termux-$APP
for file in ic_foreground ic_launcher; do
for FILE in ic_launcher ic_launcher_round; do cp ../app/src/main/res/drawable/$file.xml \
PNG=$FOLDER/$FILE.png $APPDIR/app/src/main/res/drawable/$file.xml
# Update other apps:
for APP in api boot styling tasker widget; do
APPDIR=../../termux-$APP
if [ -d $APPDIR ]; then
APP_FOLDER=$APPDIR/app/src/main/res/mipmap-$DENSITY
mkdir -p $APP_FOLDER
cp $PNG $APP_FOLDER/$FILE.png
fi
done
done done
cp ../app/src/main/res/drawable-anydpi-v26/ic_launcher.xml \
$APPDIR/app/src/main/res/drawable-anydpi-v26/$file.xml
done done

View File

@@ -9,8 +9,7 @@
- If adding text, use large font sizes. - If adding text, use large font sizes.
- Your graphic may be displayed alone without the app icon. - Your graphic may be displayed alone without the app icon.
--> -->
<svg xmlns:svg="http://www.w3.org/2000/svg" <svg xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.1" version="1.1"
viewBox="0 0 1024 500"> viewBox="0 0 1024 500">

Before

Width:  |  Height:  |  Size: 1006 B

After

Width:  |  Height:  |  Size: 962 B

23
art/generate-big-icon.sh Executable file
View File

@@ -0,0 +1,23 @@
#!/bin/sh
set -e -u
echo "Generating ~/termux-icons/ic_launcher.png..."
mkdir -p ~/termux-icons/
vector2svg ../app/src/main/res/drawable/ic_launcher.xml ~/termux-icons/ic_launcher.svg
sed -i "" 's/viewBox="0 0 108 108"/viewBox="18 18 72 72"/' ~/termux-icons/ic_launcher.svg
SIZE=512
rsvg-convert \
-w $SIZE \
-h $SIZE \
-o ~/termux-icons/ic_launcher_$SIZE.png \
~/termux-icons/ic_launcher.svg
rsvg-convert \
-b black \
-w $SIZE \
-h $SIZE \
-o ~/termux-icons/ic_launcher_square_$SIZE.png \
~/termux-icons/ic_launcher.svg

View File

@@ -1,21 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<!-- Screen and border. -->
<path fill="#000"
stroke="#BFCBCD"
stroke-width="2"
d="M 24, 24
m -21, 0
a 21,21 0 1,0 42,0
a 21,21 0 1,0 -42,0"
/>
<!-- Block cursor. -->
<path fill="#FFF"
d="M15,15
l5,0
l0,10
l-5,0"
/>
</svg>

Before

Width:  |  Height:  |  Size: 429 B

View File

@@ -7,8 +7,7 @@
- Text must be included in the image. If your app is available in more - Text must be included in the image. If your app is available in more
than one language, you must provide versions of the banner image for each supported language. than one language, you must provide versions of the banner image for each supported language.
--> -->
<svg xmlns:svg="http://www.w3.org/2000/svg" <svg xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.1" version="1.1"
viewBox="0 0 1280 720"> viewBox="0 0 1280 720">

Before

Width:  |  Height:  |  Size: 780 B

After

Width:  |  Height:  |  Size: 736 B

View File

@@ -1,16 +1,16 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
repositories { repositories {
jcenter() jcenter()
google()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:2.2.3' classpath 'com.android.tools.build:gradle:3.5.1'
} }
} }
allprojects { allprojects {
repositories { repositories {
google()
jcenter() jcenter()
} }
} }

Binary file not shown.

View File

@@ -1,6 +1,6 @@
#Sun Dec 04 17:26:05 CET 2016 #Sun Aug 25 01:57:11 CEST 2019
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-3.2.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip

8
gradlew vendored
View File

@@ -28,16 +28,16 @@ APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"` APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS="" DEFAULT_JVM_OPTS='"-Xmx64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum" MAX_FD="maximum"
warn ( ) { warn () {
echo "$*" echo "$*"
} }
die ( ) { die () {
echo echo
echo "$*" echo "$*"
echo echo
@@ -155,7 +155,7 @@ if $cygwin ; then
fi fi
# Escape application args # Escape application args
save ( ) { save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " " echo " "
} }

2
gradlew.bat vendored
View File

@@ -14,7 +14,7 @@ set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. @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 DEFAULT_JVM_OPTS="-Xmx64m"
@rem Find java.exe @rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome if defined JAVA_HOME goto findJavaFromJavaHome

View File

@@ -0,0 +1,77 @@
// Start https://raw.githubusercontent.com/nuuneoi/JCenter/master/installv1.gradle
group = publishedGroupId // Maven Group ID for the artifact
install {
repositories.mavenInstaller {
pom {
project {
packaging 'aar'
groupId publishedGroupId
artifactId artifact
name libraryName
description libraryDescription
url siteUrl
licenses {
license {
name 'GNU General Public License version 3'
url 'https://opensource.org/licenses/gpl-3.0.html'
}
}
developers {
developer {
id 'fornwall'
name 'Fredrik Fornwall'
email 'fredrik@fornwall.net'
}
}
scm {
connection gitUrl
developerConnection gitUrl
url siteUrl
}
}
}
}
}
// End https://raw.githubusercontent.com/nuuneoi/JCenter/master/installv1.gradle
// Start https://raw.githubusercontent.com/nuuneoi/JCenter/master/bintrayv1.gradle
apply plugin: 'com.jfrog.bintray'
version = libraryVersion
task sourcesJar(type: Jar) {
classifier = 'sources'
from android.sourceSets.main.java.srcDirs
}
artifacts {
archives sourcesJar
}
bintray {
user = System.getenv('BINTRAY_USER')
key = System.getenv('BINTRAY_API_KEY')
configurations = ['archives']
pkg {
repo = 'maven'
name = bintrayName
userOrg = 'termux'
desc = libraryDescription
websiteUrl = siteUrl
vcsUrl = gitUrl
licenses = ['GPL-3.0']
publish = true
publicDownloadNumbers = true
version {
desc = libraryDescription
gpg {
sign = false //Determines whether to GPG sign the files. The default is false
// passphrase = properties.getProperty("bintray.gpg.password")
}
}
}
}

View File

@@ -1 +1 @@
include ':app' include ':app', ':terminal-emulator', ':terminal-view'

View File

@@ -0,0 +1,66 @@
plugins {
id "com.jfrog.bintray" version "1.7.3"
id "com.github.dcendents.android-maven" version "2.0"
}
apply plugin: 'com.android.library'
ext {
bintrayName = 'terminal-emulator'
publishedGroupId = 'com.termux'
libraryName = 'TerminalEmulator'
artifact = 'terminal-emulator'
libraryDescription = 'The terminal emulator used in Termux'
siteUrl = 'https://github.com/termux/termux'
gitUrl = 'https://github.com/termux/termux.git'
libraryVersion = '0.52'
}
android {
compileSdkVersion 28
defaultConfig {
minSdkVersion 21
targetSdkVersion 28
externalNativeBuild {
ndkBuild {
cFlags "-std=c11", "-Wall", "-Wextra", "-Werror", "-Os", "-fno-stack-protector", "-Wl,--gc-sections"
}
}
ndk {
abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
externalNativeBuild {
ndkBuild {
path "src/main/jni/Android.mk"
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
tasks.withType(Test) {
testLogging {
events "started", "passed", "skipped", "failed"
}
}
dependencies {
testImplementation 'junit:junit:4.12'
}
apply from: '../scripts/bintray-publish.gradle'

25
terminal-emulator/proguard-rules.pro vendored Normal file
View File

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

View File

@@ -0,0 +1,2 @@
<manifest package="com.termux.terminal">
</manifest>

View File

@@ -1,9 +1,11 @@
package com.termux.terminal; package com.termux.terminal;
import java.util.Arrays;
/** /**
* A circular buffer of {@link TerminalRow}:s which keeps notes about what is visible on a logical screen and the scroll * A circular buffer of {@link TerminalRow}:s which keeps notes about what is visible on a logical screen and the scroll
* history. * history.
* <p/> * <p>
* See {@link #externalToInternalRow(int)} for how to map from logical screen rows to array indices. * See {@link #externalToInternalRow(int)} for how to map from logical screen rows to array indices.
*/ */
public final class TerminalBuffer { public final class TerminalBuffer {
@@ -39,7 +41,15 @@ public final class TerminalBuffer {
return getSelectedText(0, -getActiveTranscriptRows(), mColumns, mScreenRows).trim(); return getSelectedText(0, -getActiveTranscriptRows(), mColumns, mScreenRows).trim();
} }
public String getTranscriptTextWithoutJoinedLines() {
return getSelectedText(0, -getActiveTranscriptRows(), mColumns, mScreenRows, false).trim();
}
public String getSelectedText(int selX1, int selY1, int selX2, int selY2) { public String getSelectedText(int selX1, int selY1, int selX2, int selY2) {
return getSelectedText(selX1, selY1, selX2, selY2, true);
}
public String getSelectedText(int selX1, int selY1, int selX2, int selY2, boolean joinBackLines) {
final StringBuilder builder = new StringBuilder(); final StringBuilder builder = new StringBuilder();
final int columns = mColumns; final int columns = mColumns;
@@ -77,7 +87,8 @@ public final class TerminalBuffer {
} }
if (lastPrintingCharIndex != -1) if (lastPrintingCharIndex != -1)
builder.append(line, x1Index, lastPrintingCharIndex - x1Index + 1); builder.append(line, x1Index, lastPrintingCharIndex - x1Index + 1);
if (!rowLineWrap && row < selY2 && row < mScreenRows - 1) builder.append('\n'); if ((!joinBackLines || !rowLineWrap)
&& row < selY2 && row < mScreenRows - 1) builder.append('\n');
} }
return builder.toString(); return builder.toString();
} }
@@ -92,22 +103,20 @@ public final class TerminalBuffer {
/** /**
* Convert a row value from the public external coordinate system to our internal private coordinate system. * Convert a row value from the public external coordinate system to our internal private coordinate system.
* <p/> *
* <ul>
* <li>External coordinate system: -mActiveTranscriptRows to mScreenRows-1, with the screen being 0..mScreenRows-1.
* <li>Internal coordinate system: the mScreenRows lines starting at mScreenFirstRow comprise the screen, while the
* mActiveTranscriptRows lines ending at mScreenFirstRow-1 form the transcript (as a circular buffer).
* </ul>
* <p/>
* External <---> Internal:
* <p/>
* <pre> * <pre>
* [ ... ] [ ... ] * - External coordinate system: -mActiveTranscriptRows to mScreenRows-1, with the screen being 0..mScreenRows-1.
* [ -mActiveTranscriptRows ] [ mScreenFirstRow - mActiveTranscriptRows ] * - Internal coordinate system: the mScreenRows lines starting at mScreenFirstRow comprise the screen, while the
* [ ... ] [ ... ] * mActiveTranscriptRows lines ending at mScreenFirstRow-1 form the transcript (as a circular buffer).
* [ 0 (visible screen starts here) ] <-----> [ mScreenFirstRow ] *
* [ ... ] [ ... ] * External Internal:
* [ mScreenRows-1 ] [ mScreenFirstRow + mScreenRows-1 ] *
* [ ... ] [ ... ]
* [ -mActiveTranscriptRows ] [ mScreenFirstRow - mActiveTranscriptRows ]
* [ ... ] [ ... ]
* [ 0 (visible screen starts here) ] [ mScreenFirstRow ]
* [ ... ] [ ... ]
* [ mScreenRows-1 ] [ mScreenFirstRow + mScreenRows-1 ]
* </pre> * </pre>
* *
* @param externalRow a row in the external coordinate system. * @param externalRow a row in the external coordinate system.
@@ -424,4 +433,14 @@ public final class TerminalBuffer {
} }
} }
public void clearTranscript() {
if (mScreenFirstRow < mActiveTranscriptRows) {
Arrays.fill(mLines, mTotalRows + mScreenFirstRow - mActiveTranscriptRows, mTotalRows, null);
Arrays.fill(mLines, 0, mScreenFirstRow, null);
} else {
Arrays.fill(mLines, mScreenFirstRow - mActiveTranscriptRows, mScreenFirstRow, null);
}
mActiveTranscriptRows = 0;
}
} }

View File

@@ -56,8 +56,6 @@ public final class TerminalEmulator {
private static final int ESC_SELECT_LEFT_PAREN = 3; private static final int ESC_SELECT_LEFT_PAREN = 3;
/** Escape processing: Have seen ESC and a character-set-select ) char */ /** Escape processing: Have seen ESC and a character-set-select ) char */
private static final int ESC_SELECT_RIGHT_PAREN = 4; private static final int ESC_SELECT_RIGHT_PAREN = 4;
/** Escape processing: Have seen ESC and a character-set-select + char */
// private static final int ESC_SELECT_PLUS = 5;
/** Escape processing: "ESC [" or CSI (Control Sequence Introducer). */ /** Escape processing: "ESC [" or CSI (Control Sequence Introducer). */
private static final int ESC_CSI = 6; private static final int ESC_CSI = 6;
/** Escape processing: ESC [ ? */ /** Escape processing: ESC [ ? */
@@ -225,6 +223,7 @@ public final class TerminalEmulator {
private byte mUtf8ToFollow, mUtf8Index; private byte mUtf8ToFollow, mUtf8Index;
private final byte[] mUtf8InputBuffer = new byte[4]; private final byte[] mUtf8InputBuffer = new byte[4];
private int mLastEmittedCodePoint = -1;
public final TerminalColors mColors = new TerminalColors(); public final TerminalColors mColors = new TerminalColors();
@@ -302,6 +301,11 @@ public final class TerminalEmulator {
* @param mouseButton one of the MOUSE_* constants of this class. * @param mouseButton one of the MOUSE_* constants of this class.
*/ */
public void sendMouseEvent(int mouseButton, int column, int row, boolean pressed) { public void sendMouseEvent(int mouseButton, int column, int row, boolean pressed) {
if (column < 1) column = 1;
if (column > mColumns) column = mColumns;
if (row < 1) row = 1;
if (row > mRows) row = mRows;
if (mouseButton == MOUSE_LEFT_BUTTON_MOVED && !isDecsetInternalBitSet(DECSET_BIT_MOUSE_TRACKING_BUTTON_EVENT)) { if (mouseButton == MOUSE_LEFT_BUTTON_MOVED && !isDecsetInternalBitSet(DECSET_BIT_MOUSE_TRACKING_BUTTON_EVENT)) {
// Do not send tracking. // Do not send tracking.
} else if (isDecsetInternalBitSet(DECSET_BIT_MOUSE_PROTOCOL_SGR)) { } else if (isDecsetInternalBitSet(DECSET_BIT_MOUSE_PROTOCOL_SGR)) {
@@ -309,7 +313,7 @@ public final class TerminalEmulator {
} else { } else {
mouseButton = pressed ? mouseButton : 3; // 3 for release of all buttons. mouseButton = pressed ? mouseButton : 3; // 3 for release of all buttons.
// Clip to screen, and clip to the limits of 8-bit data. // Clip to screen, and clip to the limits of 8-bit data.
boolean out_of_bounds = column < 1 || row < 1 || column > mColumns || row > mRows || column > 255 - 32 || row > 255 - 32; boolean out_of_bounds = column > 255 - 32 || row > 255 - 32;
if (!out_of_bounds) { if (!out_of_bounds) {
byte[] data = {'\033', '[', 'M', (byte) (32 + mouseButton), (byte) (32 + column), (byte) (32 + row)}; byte[] data = {'\033', '[', 'M', (byte) (32 + mouseButton), (byte) (32 + column), (byte) (32 + row)};
mSession.write(data, 0, data.length); mSession.write(data, 0, data.length);
@@ -421,10 +425,11 @@ public final class TerminalEmulator {
mUtf8Index = mUtf8ToFollow = 0; mUtf8Index = mUtf8ToFollow = 0;
if (codePoint >= 0x80 && codePoint <= 0x9F) { if (codePoint >= 0x80 && codePoint <= 0x9F) {
// Sequence decoded to a C1 control character which is the same as escape followed by // Sequence decoded to a C1 control character which we ignore. They are
// ((code & 0x7F) + 0x40). // not used nowadays and increases the risk of messing up the terminal state
processCodePoint(/* escape (hexadecimal=0x1B, octal=033): */27); // on binary input. XTerm does not allow them in utf-8:
processCodePoint((codePoint & 0x7F) + 0x40); // "It is not possible to use a C1 control obtained from decoding the
// UTF-8 text" - http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
} else { } else {
switch (Character.getType(codePoint)) { switch (Character.getType(codePoint)) {
case Character.UNASSIGNED: case Character.UNASSIGNED:
@@ -634,6 +639,7 @@ public final class TerminalEmulator {
int bottom = Math.min(getArg(2, mRows, true) + 1, effectiveBottomMargin - 1) + effectiveTopMargin; int bottom = Math.min(getArg(2, mRows, true) + 1, effectiveBottomMargin - 1) + effectiveTopMargin;
int right = Math.min(getArg(3, mColumns, true) + 1, effectiveRightMargin - 1) + effectiveLeftMargin; int right = Math.min(getArg(3, mColumns, true) + 1, effectiveRightMargin - 1) + effectiveLeftMargin;
if (mArgIndex >= 4) { if (mArgIndex >= 4) {
if (mArgIndex >= mArgs.length) mArgIndex = mArgs.length - 1;
for (int i = 4; i <= mArgIndex; i++) { for (int i = 4; i <= mArgIndex; i++) {
int bits = 0; int bits = 0;
boolean setOrClear = true; // True if setting, false if clearing. boolean setOrClear = true; // True if setting, false if clearing.
@@ -967,6 +973,7 @@ public final class TerminalEmulator {
break; break;
case 'h': case 'h':
case 'l': case 'l':
if (mArgIndex >= mArgs.length) mArgIndex = mArgs.length - 1;
for (int i = 0; i <= mArgIndex; i++) for (int i = 0; i <= mArgIndex; i++)
doDecSetOrReset(b == 'h', mArgs[i]); doDecSetOrReset(b == 'h', mArgs[i]);
break; break;
@@ -983,6 +990,7 @@ public final class TerminalEmulator {
break; break;
case 'r': case 'r':
case 's': case 's':
if (mArgIndex >= mArgs.length) mArgIndex = mArgs.length - 1;
for (int i = 0; i <= mArgIndex; i++) { for (int i = 0; i <= mArgIndex; i++) {
int externalBit = mArgs[i]; int externalBit = mArgs[i];
int internalBit = mapDecSetBitToInternalBit(externalBit); int internalBit = mapDecSetBitToInternalBit(externalBit);
@@ -1189,12 +1197,20 @@ public final class TerminalEmulator {
} }
private void doLinefeed() { private void doLinefeed() {
boolean belowScrollingRegion = mCursorRow >= mBottomMargin;
int newCursorRow = mCursorRow + 1; int newCursorRow = mCursorRow + 1;
if (newCursorRow >= mBottomMargin) { if (belowScrollingRegion) {
scrollDownOneLine(); // Move down (but not scroll) as long as we are above the last row.
newCursorRow = mBottomMargin - 1; if (mCursorRow != mRows - 1) {
setCursorRow(newCursorRow);
}
} else {
if (newCursorRow == mBottomMargin) {
scrollDownOneLine();
newCursorRow = mBottomMargin - 1;
}
setCursorRow(newCursorRow);
} }
setCursorRow(newCursorRow);
} }
private void continueSequence(int state) { private void continueSequence(int state) {
@@ -1309,6 +1325,8 @@ public final class TerminalEmulator {
state.mSavedCursorRow = mCursorRow; state.mSavedCursorRow = mCursorRow;
state.mSavedCursorCol = mCursorCol; state.mSavedCursorCol = mCursorCol;
state.mSavedEffect = mEffect; state.mSavedEffect = mEffect;
state.mSavedForeColor = mForeColor;
state.mSavedBackColor = mBackColor;
state.mSavedDecFlags = mCurrentDecSetFlags; state.mSavedDecFlags = mCurrentDecSetFlags;
state.mUseLineDrawingG0 = mUseLineDrawingG0; state.mUseLineDrawingG0 = mUseLineDrawingG0;
state.mUseLineDrawingG1 = mUseLineDrawingG1; state.mUseLineDrawingG1 = mUseLineDrawingG1;
@@ -1320,6 +1338,8 @@ public final class TerminalEmulator {
SavedScreenState state = (mScreen == mMainBuffer) ? mSavedStateMain : mSavedStateAlt; SavedScreenState state = (mScreen == mMainBuffer) ? mSavedStateMain : mSavedStateAlt;
setCursorRowCol(state.mSavedCursorRow, state.mSavedCursorCol); setCursorRowCol(state.mSavedCursorRow, state.mSavedCursorCol);
mEffect = state.mSavedEffect; mEffect = state.mSavedEffect;
mForeColor = state.mSavedForeColor;
mBackColor = state.mSavedBackColor;
int mask = (DECSET_BIT_AUTOWRAP | DECSET_BIT_ORIGIN_MODE); int mask = (DECSET_BIT_AUTOWRAP | DECSET_BIT_ORIGIN_MODE);
mCurrentDecSetFlags = (mCurrentDecSetFlags & ~mask) | (state.mSavedDecFlags & mask); mCurrentDecSetFlags = (mCurrentDecSetFlags & ~mask) | (state.mSavedDecFlags & mask);
mUseLineDrawingG0 = state.mUseLineDrawingG0; mUseLineDrawingG0 = state.mUseLineDrawingG0;
@@ -1384,7 +1404,7 @@ public final class TerminalEmulator {
case 'I': // Cursor Horizontal Forward Tabulation (CHT). Move the active position n tabs forward. case 'I': // Cursor Horizontal Forward Tabulation (CHT). Move the active position n tabs forward.
setCursorCol(nextTabStop(getArg0(1))); setCursorCol(nextTabStop(getArg0(1)));
break; break;
case 'J': // "${CSI}${0,1,2}J" - Erase in Display (ED) case 'J': // "${CSI}${0,1,2,3}J" - Erase in Display (ED)
// ED ignores the scrolling margins. // ED ignores the scrolling margins.
switch (getArg0(0)) { switch (getArg0(0)) {
case 0: // Erase from the active position to the end of the screen, inclusive (default). case 0: // Erase from the active position to the end of the screen, inclusive (default).
@@ -1399,6 +1419,9 @@ public final class TerminalEmulator {
// move.. // move..
blockClear(0, 0, mColumns, mRows); blockClear(0, 0, mColumns, mRows);
break; break;
case 3: // Delete all lines saved in the scrollback buffer (xterm etc)
mMainBuffer.clearTranscript();
break;
default: default:
unknownSequence(b); unknownSequence(b);
return; return;
@@ -1503,6 +1526,11 @@ public final class TerminalEmulator {
case '`': // Horizontal position absolute (HPA - http://www.vt100.net/docs/vt510-rm/HPA). case '`': // Horizontal position absolute (HPA - http://www.vt100.net/docs/vt510-rm/HPA).
setCursorColRespectingOriginMode(getArg0(1) - 1); setCursorColRespectingOriginMode(getArg0(1) - 1);
break; break;
case 'b': // Repeat the preceding graphic character Ps times (REP).
if (mLastEmittedCodePoint == -1) break;
final int numRepeat = getArg0(1);
for (int i = 0; i < numRepeat; i++) emitCodePoint(mLastEmittedCodePoint);
break;
case 'c': // Primary Device Attributes (http://www.vt100.net/docs/vt510-rm/DA1) if argument is missing or zero. case 'c': // Primary Device Attributes (http://www.vt100.net/docs/vt510-rm/DA1) if argument is missing or zero.
// The important part that may still be used by some (tmux stores this value but does not currently use it) // The important part that may still be used by some (tmux stores this value but does not currently use it)
// is the first response parameter identifying the terminal service class, where we send 64 for "vt420". // is the first response parameter identifying the terminal service class, where we send 64 for "vt420".
@@ -1559,7 +1587,7 @@ public final class TerminalEmulator {
break; break;
case 'r': // "CSI${top};${bottom}r" - set top and bottom Margins (DECSTBM). case 'r': // "CSI${top};${bottom}r" - set top and bottom Margins (DECSTBM).
{ {
// http://www.vt100.net/docs/vt510-rm/DECSTBM // https://vt100.net/docs/vt510-rm/DECSTBM.html
// The top margin defaults to 1, the bottom margin defaults to mRows. // The top margin defaults to 1, the bottom margin defaults to mRows.
// The escape sequence numbers top 1..23, but we number top 0..22. // The escape sequence numbers top 1..23, but we number top 0..22.
// The escape sequence numbers bottom 2..24, and so do we (because we use a zero based numbering // The escape sequence numbers bottom 2..24, and so do we (because we use a zero based numbering
@@ -1568,6 +1596,7 @@ public final class TerminalEmulator {
// Also require that top + 2 <= bottom. // Also require that top + 2 <= bottom.
mTopMargin = Math.max(0, Math.min(getArg0(1) - 1, mRows - 2)); mTopMargin = Math.max(0, Math.min(getArg0(1) - 1, mRows - 2));
mBottomMargin = Math.max(mTopMargin + 2, Math.min(getArg1(mRows), mRows)); mBottomMargin = Math.max(mTopMargin + 2, Math.min(getArg1(mRows), mRows));
// DECSTBM moves the cursor to column 1, line 1 of the page respecting origin mode. // DECSTBM moves the cursor to column 1, line 1 of the page respecting origin mode.
setCursorPosition(0, 0); setCursorPosition(0, 0);
} }
@@ -1641,6 +1670,7 @@ public final class TerminalEmulator {
/** Select Graphic Rendition (SGR) - see http://en.wikipedia.org/wiki/ANSI_escape_code#graphics. */ /** Select Graphic Rendition (SGR) - see http://en.wikipedia.org/wiki/ANSI_escape_code#graphics. */
private void selectGraphicRendition() { private void selectGraphicRendition() {
if (mArgIndex >= mArgs.length) mArgIndex = mArgs.length - 1;
for (int i = 0; i <= mArgIndex; i++) { for (int i = 0; i <= mArgIndex; i++) {
int code = mArgs[i]; int code = mArgs[i];
if (code < 0) { if (code < 0) {
@@ -2051,6 +2081,7 @@ public final class TerminalEmulator {
buf.append(", escapeState="); buf.append(", escapeState=");
buf.append(mEscapeState); buf.append(mEscapeState);
boolean firstArg = true; boolean firstArg = true;
if (mArgIndex >= mArgs.length) mArgIndex = mArgs.length - 1;
for (int i = 0; i <= mArgIndex; i++) { for (int i = 0; i <= mArgIndex; i++) {
int value = mArgs[i]; int value = mArgs[i];
if (value >= 0) { if (value >= 0) {
@@ -2083,6 +2114,7 @@ public final class TerminalEmulator {
* @param codePoint The code point of the character to display * @param codePoint The code point of the character to display
*/ */
private void emitCodePoint(int codePoint) { private void emitCodePoint(int codePoint) {
mLastEmittedCodePoint = codePoint;
if (mUseLineDrawingUsesG0 ? mUseLineDrawingG0 : mUseLineDrawingG1) { if (mUseLineDrawingUsesG0 ? mUseLineDrawingG0 : mUseLineDrawingG1) {
// http://www.vt100.net/docs/vt102-ug/table5-15.html. // http://www.vt100.net/docs/vt102-ug/table5-15.html.
switch (codePoint) { switch (codePoint) {
@@ -2265,8 +2297,8 @@ public final class TerminalEmulator {
mBottomMargin = mRows; mBottomMargin = mRows;
mRightMargin = mColumns; mRightMargin = mColumns;
mAboutToAutoWrap = false; mAboutToAutoWrap = false;
mForeColor = TextStyle.COLOR_INDEX_FOREGROUND; mForeColor = mSavedStateMain.mSavedForeColor = mSavedStateAlt.mSavedForeColor = TextStyle.COLOR_INDEX_FOREGROUND;
mBackColor = TextStyle.COLOR_INDEX_BACKGROUND; mBackColor = mSavedStateMain.mSavedBackColor = mSavedStateAlt.mSavedBackColor = TextStyle.COLOR_INDEX_BACKGROUND;
setDefaultTabStops(); setDefaultTabStops();
mUseLineDrawingG0 = mUseLineDrawingG1 = false; mUseLineDrawingG0 = mUseLineDrawingG1 = false;
@@ -2309,6 +2341,9 @@ public final class TerminalEmulator {
public void paste(String text) { public void paste(String text) {
// First: Always remove escape key and C1 control characters [0x80,0x9F]: // First: Always remove escape key and C1 control characters [0x80,0x9F]:
text = text.replaceAll("(\u001B|[\u0080-\u009F])", ""); text = text.replaceAll("(\u001B|[\u0080-\u009F])", "");
// Second: Replace all newlines (\n) or CRLF (\r\n) with carriage returns (\r).
text = text.replaceAll("\r?\n", "\r");
// Then: Implement bracketed paste mode if enabled: // Then: Implement bracketed paste mode if enabled:
boolean bracketed = isDecsetInternalBitSet(DECSET_BIT_BRACKETED_PASTE_MODE); boolean bracketed = isDecsetInternalBitSet(DECSET_BIT_BRACKETED_PASTE_MODE);
if (bracketed) mSession.write("\033[200~"); if (bracketed) mSession.write("\033[200~");
@@ -2320,7 +2355,7 @@ public final class TerminalEmulator {
static final class SavedScreenState { static final class SavedScreenState {
/** Saved state of the cursor position, Used to implement the save/restore cursor position escape sequences. */ /** Saved state of the cursor position, Used to implement the save/restore cursor position escape sequences. */
int mSavedCursorRow, mSavedCursorCol; int mSavedCursorRow, mSavedCursorCol;
int mSavedEffect; int mSavedEffect, mSavedForeColor, mSavedBackColor;
int mSavedDecFlags; int mSavedDecFlags;
boolean mUseLineDrawingG0, mUseLineDrawingG1, mUseLineDrawingUsesG0 = true; boolean mUseLineDrawingG0, mUseLineDrawingG1, mUseLineDrawingUsesG0 = true;
} }

View File

@@ -4,7 +4,7 @@ import java.util.Arrays;
/** /**
* A row in a terminal, composed of a fixed number of cells. * A row in a terminal, composed of a fixed number of cells.
* <p/> * <p>
* The text in the row is stored in a char[] array, {@link #mText}, for quick access during rendering. * The text in the row is stored in a char[] array, {@link #mText}, for quick access during rendering.
*/ */
public final class TerminalRow { public final class TerminalRow {
@@ -21,6 +21,8 @@ public final class TerminalRow {
boolean mLineWrap; boolean mLineWrap;
/** The style bits of each cell in the row. See {@link TextStyle}. */ /** The style bits of each cell in the row. See {@link TextStyle}. */
final long[] mStyle; final long[] mStyle;
/** If this row might contain chars with width != 1, used for deactivating fast path */
boolean mHasNonOneWidthOrSurrogateChars;
/** Construct a blank row (containing only whitespace, ' ') with a specified style. */ /** Construct a blank row (containing only whitespace, ' ') with a specified style. */
public TerminalRow(int columns, long style) { public TerminalRow(int columns, long style) {
@@ -32,6 +34,7 @@ public final class TerminalRow {
/** NOTE: The sourceX2 is exclusive. */ /** NOTE: The sourceX2 is exclusive. */
public void copyInterval(TerminalRow line, int sourceX1, int sourceX2, int destinationX) { public void copyInterval(TerminalRow line, int sourceX1, int sourceX2, int destinationX) {
mHasNonOneWidthOrSurrogateChars |= line.mHasNonOneWidthOrSurrogateChars;
final int x1 = line.findStartOfColumn(sourceX1); final int x1 = line.findStartOfColumn(sourceX1);
final int x2 = line.findStartOfColumn(sourceX2); final int x2 = line.findStartOfColumn(sourceX2);
boolean startingFromSecondHalfOfWideChar = (sourceX1 > 0 && line.wideDisplayCharacterStartingAt(sourceX1 - 1)); boolean startingFromSecondHalfOfWideChar = (sourceX1 > 0 && line.wideDisplayCharacterStartingAt(sourceX1 - 1));
@@ -116,6 +119,7 @@ public final class TerminalRow {
Arrays.fill(mText, ' '); Arrays.fill(mText, ' ');
Arrays.fill(mStyle, style); Arrays.fill(mStyle, style);
mSpaceUsed = (short) mColumns; mSpaceUsed = (short) mColumns;
mHasNonOneWidthOrSurrogateChars = false;
} }
// https://github.com/steven676/Android-Terminal-Emulator/commit/9a47042620bec87617f0b4f5d50568535668fe26 // https://github.com/steven676/Android-Terminal-Emulator/commit/9a47042620bec87617f0b4f5d50568535668fe26
@@ -123,6 +127,17 @@ public final class TerminalRow {
mStyle[columnToSet] = style; mStyle[columnToSet] = style;
final int newCodePointDisplayWidth = WcWidth.width(codePoint); final int newCodePointDisplayWidth = WcWidth.width(codePoint);
// Fast path when we don't have any chars with width != 1
if (!mHasNonOneWidthOrSurrogateChars) {
if (codePoint >= Character.MIN_SUPPLEMENTARY_CODE_POINT || newCodePointDisplayWidth != 1) {
mHasNonOneWidthOrSurrogateChars = true;
} else {
mText[columnToSet] = (char) codePoint;
return;
}
}
final boolean newIsCombining = newCodePointDisplayWidth <= 0; final boolean newIsCombining = newCodePointDisplayWidth <= 0;
boolean wasExtraColForWideChar = (columnToSet > 0) && wideDisplayCharacterStartingAt(columnToSet - 1); boolean wasExtraColForWideChar = (columnToSet > 0) && wideDisplayCharacterStartingAt(columnToSet - 1);

View File

@@ -19,13 +19,13 @@ import java.util.UUID;
/** /**
* A terminal session, consisting of a process coupled to a terminal interface. * A terminal session, consisting of a process coupled to a terminal interface.
* <p/> * <p>
* The subprocess will be executed by the constructor, and when the size is made known by a call to * The subprocess will be executed by the constructor, and when the size is made known by a call to
* {@link #updateSize(int, int)} terminal emulation will begin and threads will be spawned to handle the subprocess I/O. * {@link #updateSize(int, int)} terminal emulation will begin and threads will be spawned to handle the subprocess I/O.
* All terminal emulation and callback methods will be performed on the main thread. * All terminal emulation and callback methods will be performed on the main thread.
* <p/> * <p>
* The child process may be exited forcefully by using the {@link #finishIfRunning()} method. * The child process may be exited forcefully by using the {@link #finishIfRunning()} method.
* <p/> * <p>
* NOTE: The terminal session may outlive the EmulatorView, so be careful with callbacks! * NOTE: The terminal session may outlive the EmulatorView, so be careful with callbacks!
*/ */
public final class TerminalSession extends TerminalOutput { public final class TerminalSession extends TerminalOutput {

View File

@@ -1,10 +1,13 @@
package com.termux.terminal; package com.termux.terminal;
/** /**
* <p>
* Encodes effects, foreground and background colors into a 64 bit long, which are stored for each cell in a terminal * Encodes effects, foreground and background colors into a 64 bit long, which are stored for each cell in a terminal
* row in {@link TerminalRow#mStyle}. * row in {@link TerminalRow#mStyle}.
* <p/> * </p>
* <p>
* The bit layout is: * The bit layout is:
* </p>
* - 16 flags (11 currently used). * - 16 flags (11 currently used).
* - 24 for foreground color (only 9 first bits if a color index). * - 24 for foreground color (only 9 first bits if a color index).
* - 24 for background color (only 9 first bits if a color index). * - 24 for background color (only 9 first bits if a color index).
@@ -20,9 +23,10 @@ public final class TextStyle {
public final static int CHARACTER_ATTRIBUTE_STRIKETHROUGH = 1 << 6; public final static int CHARACTER_ATTRIBUTE_STRIKETHROUGH = 1 << 6;
/** /**
* The selective erase control functions (DECSED and DECSEL) can only erase characters defined as erasable. * The selective erase control functions (DECSED and DECSEL) can only erase characters defined as erasable.
* <p/> * <p>
* This bit is set if DECSCA (Select Character Protection Attribute) has been used to define the characters that * This bit is set if DECSCA (Select Character Protection Attribute) has been used to define the characters that
* come after it as erasable from the screen. * come after it as erasable from the screen.
* </p>
*/ */
public final static int CHARACTER_ATTRIBUTE_PROTECTED = 1 << 7; public final static int CHARACTER_ATTRIBUTE_PROTECTED = 1 << 7;
/** Dim colors. Also known as faint or half intensity. */ /** Dim colors. Also known as faint or half intensity. */

View File

@@ -57,7 +57,7 @@ static int create_subprocess(JNIEnv* env,
tcsetattr(ptm, TCSANOW, &tios); tcsetattr(ptm, TCSANOW, &tios);
/** Set initial winsize. */ /** Set initial winsize. */
struct winsize sz = { .ws_row = rows, .ws_col = columns }; struct winsize sz = { .ws_row = (unsigned short) rows, .ws_col = (unsigned short) columns };
ioctl(ptm, TIOCSWINSZ, &sz); ioctl(ptm, TIOCSWINSZ, &sz);
pid_t pid = fork(); pid_t pid = fork();
@@ -180,7 +180,7 @@ JNIEXPORT jint JNICALL Java_com_termux_terminal_JNI_createSubprocess(
JNIEXPORT void JNICALL Java_com_termux_terminal_JNI_setPtyWindowSize(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint fd, jint rows, jint cols) JNIEXPORT void JNICALL Java_com_termux_terminal_JNI_setPtyWindowSize(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint fd, jint rows, jint cols)
{ {
struct winsize sz = { .ws_row = rows, .ws_col = cols }; struct winsize sz = { .ws_row = (unsigned short) rows, .ws_col = (unsigned short) cols };
ioctl(fd, TIOCSWINSZ, &sz); ioctl(fd, TIOCSWINSZ, &sz);
} }
@@ -194,7 +194,7 @@ JNIEXPORT void JNICALL Java_com_termux_terminal_JNI_setPtyUTF8Mode(JNIEnv* TERMU
} }
} }
JNIEXPORT int JNICALL Java_com_termux_terminal_JNI_waitFor(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint pid) JNIEXPORT jint JNICALL Java_com_termux_terminal_JNI_waitFor(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint pid)
{ {
int status; int status;
waitpid(pid, &status, 0); waitpid(pid, &status, 0);

View File

@@ -17,13 +17,13 @@ public class ByteQueueTest extends TestCase {
public void testCompleteWrites() throws Exception { public void testCompleteWrites() throws Exception {
ByteQueue q = new ByteQueue(10); ByteQueue q = new ByteQueue(10);
assertEquals(true, q.write(new byte[]{1, 2, 3}, 0, 3)); assertTrue(q.write(new byte[]{1, 2, 3}, 0, 3));
byte[] arr = new byte[10]; byte[] arr = new byte[10];
assertEquals(3, q.read(arr, true)); 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)); assertTrue(q.write(new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, 0, 10));
assertEquals(10, q.read(arr, true)); 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);
} }
@@ -43,7 +43,7 @@ public class ByteQueueTest extends TestCase {
public void testWriteNotesClosing() throws Exception { public void testWriteNotesClosing() throws Exception {
ByteQueue q = new ByteQueue(10); ByteQueue q = new ByteQueue(10);
q.close(); q.close();
assertEquals(false, q.write(new byte[]{1, 2, 3}, 0, 3)); assertFalse(q.write(new byte[]{1, 2, 3}, 0, 3));
} }
public void testReadNonBlocking() throws Exception { public void testReadNonBlocking() throws Exception {

View File

@@ -0,0 +1,65 @@
package com.termux.terminal;
/** "\033[" is the Control Sequence Introducer char sequence (CSI). */
public class ControlSequenceIntroducerTest extends TerminalTestCase {
/** CSI Ps P Scroll down Ps lines (default = 1) (SD). */
public void testCsiT() {
withTerminalSized(4, 6).enterString("1\r\n2\r\n3\r\nhi\033[2Tyo\r\nA\r\nB").assertLinesAre(" ", " ", "1 ", "2 yo", "A ",
"Bi ");
// Default value (1):
withTerminalSized(4, 6).enterString("1\r\n2\r\n3\r\nhi\033[Tyo\r\nA\r\nB").assertLinesAre(" ", "1 ", "2 ", "3 yo", "Ai ",
"B ");
}
/** CSI Ps S Scroll up Ps lines (default = 1) (SU). */
public void testCsiS() {
// The behaviour here is a bit inconsistent between terminals - this is how the OS X Terminal.app does it:
withTerminalSized(3, 4).enterString("1\r\n2\r\n3\r\nhi\033[2Sy").assertLinesAre("3 ", "hi ", " ", " y");
// Default value (1):
withTerminalSized(3, 4).enterString("1\r\n2\r\n3\r\nhi\033[Sy").assertLinesAre("2 ", "3 ", "hi ", " y");
}
/** 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 ", " ");
}
/** CSI Pm m Set SGR parameter(s) from semicolon-separated list Pm. */
public void testCsiSGRParameters() {
// Set more parameters (19) than supported (16). Additional parameters should be silently consumed.
withTerminalSized(3, 2).enterString("\033[0;38;2;255;255;255;48;2;0;0;0;1;2;3;4;5;7;8;9mabc").assertLinesAre("abc", " ");
}
/** CSI Ps b Repeat the preceding graphic character Ps times (REP). */
public void testRepeat() {
withTerminalSized(3, 2).enterString("a\033[b").assertLinesAre("aa ", " ");
withTerminalSized(3, 2).enterString("a\033[2b").assertLinesAre("aaa", " ");
// When no char has been output we ignore REP:
withTerminalSized(3, 2).enterString("\033[b").assertLinesAre(" ", " ");
// This shows that REP outputs the last emitted code point and not the one relative to the
// current cursor position:
withTerminalSized(5, 2).enterString("abcde\033[2G\033[2b\n").assertLinesAre("aeede", " ");
}
/** CSI 3 J Clear scrollback (xterm, libvte; non-standard). */
public void testCsi3J() {
withTerminalSized(3, 2).enterString("a\r\nb\r\nc\r\nd");
assertEquals("a\nb\nc\nd", mTerminal.getScreen().getTranscriptText());
enterString("\033[3J");
assertEquals("c\nd", mTerminal.getScreen().getTranscriptText());
withTerminalSized(3, 2).enterString("Lorem_ipsum");
assertEquals("Lorem_ipsum", mTerminal.getScreen().getTranscriptText());
enterString("\033[3J");
assertEquals("ipsum", mTerminal.getScreen().getTranscriptText());
withTerminalSized(3, 2).enterString("w\r\nx\r\ny\r\nz\033[?1049h\033[3J\033[?1049l");
assertEquals("y\nz", mTerminal.getScreen().getTranscriptText());
}
}

View File

@@ -1,6 +1,6 @@
package com.termux.terminal; package com.termux.terminal;
import junit.framework.Assert; import org.junit.Assert;
public class CursorAndScreenTest extends TerminalTestCase { public class CursorAndScreenTest extends TerminalTestCase {
@@ -227,4 +227,44 @@ public class CursorAndScreenTest extends TerminalTestCase {
withTerminalSized(3, 3).enterString("\b\b\b\bhi").assertLinesAre("hi ", " ", " "); withTerminalSized(3, 3).enterString("\b\b\b\bhi").assertLinesAre("hi ", " ", " ");
} }
public void testCursorSaveRestoreLocation() {
// DEC save/restore
withTerminalSized(4, 2).enterString("t\0337est\r\nme\0338ry ").assertLinesAre("try ", "me ");
// ANSI.SYS save/restore
withTerminalSized(4, 2).enterString("t\033[sest\r\nme\033[ury ").assertLinesAre("try ", "me ");
// Alternate screen enter/exit
withTerminalSized(4, 2).enterString("t\033[?1049h\033[Hest\r\nme").assertLinesAre("est ", "me ").enterString("\033[?1049lry").assertLinesAre("try ", " ");
}
public void testCursorSaveRestoreTextStyle() {
long s;
// DEC save/restore
withTerminalSized(4, 2).enterString("\033[31;42;4m..\0337\033[36;47;24m\0338..");
s = getStyleAt(0, 3);
Assert.assertEquals(1, TextStyle.decodeForeColor(s));
Assert.assertEquals(2, TextStyle.decodeBackColor(s));
Assert.assertEquals(TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE, TextStyle.decodeEffect(s));
// ANSI.SYS save/restore
withTerminalSized(4, 2).enterString("\033[31;42;4m..\033[s\033[36;47;24m\033[u..");
s = getStyleAt(0, 3);
Assert.assertEquals(1, TextStyle.decodeForeColor(s));
Assert.assertEquals(2, TextStyle.decodeBackColor(s));
Assert.assertEquals(TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE, TextStyle.decodeEffect(s));
// Alternate screen enter/exit
withTerminalSized(4, 2);
enterString("\033[31;42;4m..\033[?1049h\033[H\033[36;47;24m.");
s = getStyleAt(0, 0);
Assert.assertEquals(6, TextStyle.decodeForeColor(s));
Assert.assertEquals(7, TextStyle.decodeBackColor(s));
Assert.assertEquals(0, TextStyle.decodeEffect(s));
enterString("\033[?1049l..");
s = getStyleAt(0, 3);
Assert.assertEquals(1, TextStyle.decodeForeColor(s));
Assert.assertEquals(2, TextStyle.decodeBackColor(s));
Assert.assertEquals(TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE, TextStyle.decodeEffect(s));
}
} }

Some files were not shown because too many files have changed in this diff Show More