Compare commits

..

143 Commits
v0.32 ... v0.44

Author SHA1 Message Date
Fredrik Fornwall
4f40d5a26a Bump version to 0.44 2016-12-26 04:07:44 +01:00
Fredrik Fornwall
df92896eef Add Android 7.1 launcher shortcut for new session 2016-12-26 04:07:03 +01:00
Fredrik Fornwall
4c93cb42f1 Use constant for intent extra key 2016-12-26 00:57:09 +01:00
Fredrik Fornwall
34afb9de43 Give up on using vector drawables for launch icons
Instead use svg files in art/ and a script to build the png files.
2016-12-24 01:05:33 +01:00
Fredrik Fornwall
b6ea29d260 Remove duplicate updateNotification() calls 2016-12-24 00:25:24 +01:00
Fredrik Fornwall
289d58a2f0 Try it with launcher icons in drawable-anydpi 2016-12-10 22:51:11 -05:00
Fredrik Fornwall
0501ce924b gradle: Update android gradle plugin 2016-12-07 20:26:09 -05:00
Fredrik Fornwall
d12256f5e5 Add flags to imeOptions
Add IME_FLAG_NO_ENTER_ACTION and IME_ACTION_NONE. I haven't
encountered any issue without them, but specifying them is correct.
2016-12-04 17:32:40 +01:00
Fredrik Fornwall
fcbc036f92 Update gradle to 3.2.1 2016-12-04 17:26:26 +01:00
Fredrik Fornwall
70d5839334 Bump version to 0.43 for preview testing 2016-12-04 04:38:35 +01:00
Fredrik Fornwall
9c19540759 Keep track of background tasks
Also merge wake lock and wifi lock into one, and make it exposed to
termux-wake-lock and termux-wake-unlock commands.
2016-12-04 04:37:13 +01:00
Fredrik Fornwall
9fe0e49473 Render cursor color&shapes (underline and ibeam)
Allow the cursor to be colored by the theme, and support rendering
underline and ibeam cursor styles.
2016-12-04 01:06:50 +01:00
Fredrik Fornwall
357b17e972 Improve setup of symlinks to external storage
The context.getExternalFilesDirs(null) call may return several
elements, and some of them may be null.
2016-12-04 01:04:19 +01:00
Fredrik Fornwall
6334470f81 Try to handle Samsung keyboard better
The stock Samsung keyboard with 'Auto check spelling' enabled may
send multiple backspaces. Note that this auto-correction of
spelling will not work good in general with a terminal, so should
be disabled (or another keyboard used) when using Termux.
2016-11-28 00:26:37 +01:00
Fredrik Fornwall
b8cdd59c68 Use gradle build, not assembly, for coverity 2016-11-25 03:55:07 +01:00
Fredrik Fornwall
6cf36fffd7 Fix typo on coverity setup 2016-11-25 03:43:16 +01:00
Fredrik Fornwall
f10ecd4db5 Try to fix coverity scans in travis 2016-11-25 03:17:50 +01:00
Fredrik Fornwall
26457e8443 Tweak fillcolor in icons for easier svg generation 2016-11-25 02:14:54 +01:00
Fredrik Fornwall
6702846c7c Update build tools version in travis 2016-11-24 23:58:43 +01:00
Fredrik Fornwall
0c6180bbb1 Update gradle deps 2016-11-24 23:26:27 +01:00
Fredrik Fornwall
fcf07f6a19 Coverity: Specify "branch_pattern: master" 2016-11-24 20:15:31 +01:00
Fredrik Fornwall
e272e3b3b2 Update README a bit 2016-11-24 20:05:28 +01:00
Fredrik Fornwall
cdf0e72145 Enable coverity scan for all branches 2016-11-24 19:36:38 +01:00
Fredrik Fornwall
70245eb78c Use BackgroundJob.setupProcessArgs for sessions 2016-11-23 01:55:29 +01:00
Fredrik Fornwall
ee7631dfac Add a round icon for android-25 2016-11-23 01:38:17 +01:00
Fredrik Fornwall
5ecf5d12d1 Tweak the icon somewhat 2016-11-23 01:37:58 +01:00
Fredrik Fornwall
0c8cd90f4e Use $PREFIX/bin/sh for script file without shebang
Also try to handle #!(/usr)/bin/foo shebangs.
2016-11-20 16:43:27 +01:00
Fredrik Fornwall
e1ea68913f Use to a vector notification icon 2016-11-19 22:33:45 +01:00
Fredrik Fornwall
dde854eba7 Switch to a vector icon 2016-11-19 22:18:26 +01:00
Fredrik Fornwall
07a4607c04 Merge pull request #193 from friederbluemle/update-gradle-wrapper
Update Gradle wrapper to 3.2
2016-11-19 21:04:10 +01:00
Fredrik Fornwall
883be37b98 Add a shake animation on a terminal bell 2016-11-19 15:41:24 +01:00
Frieder Bluemle
d939d3d927 Update Gradle wrapper to 3.2 2016-11-18 18:01:00 +08:00
Fredrik Fornwall
a0fa51eb92 Start work on background jobs 2016-10-26 02:26:44 +02:00
Fredrik Fornwall
755513bb33 Update gradle setup 2016-10-20 01:00:28 +02:00
Fredrik Fornwall
8ad7a6669c Switch a commit() to apply() for shared prefs 2016-10-16 00:13:35 +02:00
Fredrik Fornwall
60f7aada9e Remove logging at info level 2016-10-16 00:13:04 +02:00
Fredrik Fornwall
6aa0492434 Add @NonNull annotation 2016-10-16 00:12:42 +02:00
Fredrik Fornwall
019aa44837 Update Android Gradle Plugin 2016-10-10 22:23:39 +02:00
Fredrik Fornwall
8d3d5e147f Remove outdated JNI building instructions 2016-09-29 01:13:45 +02:00
Fredrik Fornwall
44197b90e2 Update .travis.yml 2016-09-29 00:34:12 +02:00
Fredrik Fornwall
794c7ee333 Remove deprecated android:singleLine attribute 2016-09-27 00:30:05 +02:00
Fredrik Fornwall
0457ddbc69 Update build.gradle with latest versions 2016-09-27 00:29:47 +02:00
Fredrik Fornwall
283792af5e Refresh listview after changing session name 2016-09-27 00:29:08 +02:00
Fredrik Fornwall
be7cfa603a Fix gradle syntax for Android Studio 2016-09-25 01:15:49 +02:00
Fredrik Fornwall
3480bf7346 Switch from cmake to ndk-build
We switch from cmake to ndk-build to make it easier for builders
in not requiring an additional tool installed. The JNI build is
so simple so we don't really need much of a build tool anyway.
2016-09-24 20:22:10 +02:00
Fredrik Fornwall
8314a2756c Setup NDK in travis 2016-09-23 08:57:40 +02:00
Fredrik Fornwall
4de82d9fe0 Update gradle to 3.1 2016-09-21 22:34:44 +02:00
Fredrik Fornwall
d658e16801 Use the new cmake support to build JNI code 2016-09-21 22:30:20 +02:00
Fredrik Fornwall
26dcd5af88 Update gradle and Android Studio 2016-09-19 23:26:49 +02:00
Fredrik Fornwall
8056013082 Bump version to 0.42 2016-09-16 23:18:51 +02:00
Fredrik Fornwall
8e90545c4b Remove comment from build.gradle 2016-09-16 23:18:34 +02:00
Fredrik Fornwall
426ddbacbd Remove useless casts 2016-09-16 23:17:47 +02:00
Fredrik Fornwall
7e1f8a551f Change VolumeUp+H to generate ~
Using VolumeUp+H to generate a tilde (~) is more useful than
sending the home key.

Fixes #151.
2016-09-16 23:15:27 +02:00
Fredrik Fornwall
e169af0447 Change shortcuts from Ctrl+Shift to Ctrl+Alt
This works on more language layouts and devices.

Fixes #145.
2016-09-16 23:12:56 +02:00
Fredrik Fornwall
a2cb3fafee Fix numpad 0 and . key handling
Fixes #146.
2016-09-05 23:11:25 +02:00
Fredrik Fornwall
166710f14a Bump version to 0.40 2016-09-04 19:00:29 +02:00
Fredrik Fornwall
c1a9b7726f Tweak InputConnection implementation 2016-09-04 18:56:28 +02:00
Fredrik Fornwall
afb339e9d8 Format code 2016-08-30 13:47:30 +02:00
Fredrik Fornwall
64c23f498f Implement true (24-bit) color 2016-08-27 00:32:38 +02:00
Fredrik Fornwall
1dc92b2a12 Remove unused imports 2016-08-22 16:54:55 +02:00
Fredrik Fornwall
990a957383 Bump android support library version 2016-08-22 16:53:42 +02:00
Fredrik Fornwall
7bb64d724c Update android plugin for gradle 2016-08-16 10:31:10 +02:00
Fredrik Fornwall
4609dd71c6 Switch KEYCODE_HOME -> KEYCODE_MOVE_HOME in tests 2016-08-12 06:13:26 +02:00
Fredrik Fornwall
8d00f22d4c Catch KEYCODE_MOVE_HOME and not KEYCODE_HOME
The KEYCODE_HOME event is handled by the system and never delivered
to applications, it's KEYCODE_MOVE_HOME (FN+LeftArrow) we want to
handle ourselves and send as an escape sequence.

Fixes #138.
2016-08-12 04:18:09 +02:00
Fredrik Fornwall
5532421ab2 Check arches in order of preference
The documentation for Build.SUPPORTED_ABIS says:
"An ordered list of ABIs supported by this device. The most preferred
ABI is the first element in the list."

Respect that preference when checking for which arch to install
packages for.

Fixes #131.
2016-08-08 23:22:47 +02:00
Fredrik Fornwall
d2b27978e2 Bump version for v0.39 2016-08-08 23:19:49 +02:00
Fredrik Fornwall
30b05e9ab2 Merge pull request #132 from michalbednarski/extrakeysview-alignment
Make ExtraKeysView work on Android 5
2016-08-08 23:10:10 +02:00
Michał Bednarski
c350318c77 Make ExtraKeysView work on Android 5
This is done by explicitly specifying alignment as GridLayout.FILL
as I have figured out that this was fixed in Android 6 in commit
6dafd87fb4%5E%21/#F0
which set default alignment to FILL if weight is nonzero
2016-08-08 10:16:47 +02:00
Fredrik Fornwall
c9b49cef58 Bump version to 0.38 2016-08-05 00:00:53 +02:00
Fredrik Fornwall
f9c642c672 Support Unicode 9 for wcwidth (don't squash emojis) 2016-08-04 23:58:09 +02:00
Fredrik Fornwall
c0a5e5f57a Switch to TYPE_NULL as input type
This fixes #126 where the previous input type put some keyboards into
word mode (no direct echo). The workaround for Google Pinyin does not
seem to be necessary no more.

Also fix backspace after entering emojis on some keyboards (Swype).
2016-08-04 23:56:17 +02:00
Fredrik Fornwall
dfdc9b37e1 Allow predictive text area input to remove session
Fixes #124
2016-08-04 23:27:42 +02:00
Fredrik Fornwall
dfb22e6050 Make user-configurable shortcuts case insensitive 2016-08-04 18:11:50 +02:00
Fredrik Fornwall
b95d84fe13 Use absolute reference for android.R (lint check) 2016-08-02 17:34:13 +02:00
Fredrik Fornwall
a73228b109 Fix Enter to finish session in more cases
Detect the Enter key to finish a session not only on KeyEvent:s,
but also when the IME uses InputConnection.commitText() to send
\n. This seems to be triggered more after switching to the Uri
input type. Closes #124

Also bump app version for a quick release.
2016-08-01 06:37:49 +02:00
Fredrik Fornwall
eaeb0930f4 Enable x86_64 and bump version code 2016-08-01 01:47:51 +02:00
Fredrik Fornwall
95a50096cb Remove attempt of icons at popup menu actions 2016-08-01 00:30:11 +02:00
Fredrik Fornwall
8caeab470e Tweak the IME mode (from password to URI)
This makes more sense and avoids the extra number row when using
the Google keyboard. Closes #87.
2016-07-31 22:55:54 +02:00
Fredrik Fornwall
6b62e65154 Some minor AS lint warnings tweaks 2016-07-31 22:28:17 +02:00
Fredrik Fornwall
fb7dc21c18 Prepare for final build 2016-07-31 22:13:50 +02:00
Fredrik Fornwall
d0abd17091 Add version check before field usage 2016-07-31 22:13:24 +02:00
Fredrik Fornwall
0550dbff9d Fix backspace in combination with Alt and Ctrl 2016-07-27 00:27:21 +02:00
Fredrik Fornwall
9d7ed21f27 Fix Ctrl+/ to send same as Ctrl+_ 2016-07-27 00:26:50 +02:00
Fredrik Fornwall
7e2cbd969a Update android build tools to 24.0.1 2016-07-23 17:09:15 +02:00
Fredrik Fornwall
f9842f22fb Update gradle wrapper to 2.14.1 2016-07-23 17:08:54 +02:00
Fredrik Fornwall
962a43743c Update for android studio file 2016-07-23 16:52:49 +02:00
Fredrik Fornwall
ef892fca0b Update versions of support dependencies 2016-07-23 16:52:24 +02:00
Fredrik Fornwall
2bf9e7b205 Do not send mouse up event after scrolling
This fixes an issue where e.g. in tmux, when pressing the finger
on an upper pane and dragging it down to scroll and releasing it
further down on another pane, the upper pane lost focus to to a
mouse click being sent to the pane below.
2016-07-22 00:47:47 +02:00
Fredrik Fornwall
bc158252d6 Use static imports throughout 2016-07-22 00:23:23 +02:00
Fredrik Fornwall
b16f11cd87 Formatting for .travis.yml 2016-07-04 23:08:22 +02:00
Fredrik Fornwall
f57232b40e Use jdk8 for travis build 2016-07-04 22:55:24 +02:00
Fredrik Fornwall
f156ce259e Update travis configuration for androi-24 2016-07-04 22:24:38 +02:00
Fredrik Fornwall
2db6923bc4 Reformat code project-wide (getting rid of tabs) 2016-06-28 01:03:03 +02:00
Fredrik Fornwall
d72fd579ee Various updates mainly for extra keys 2016-06-28 00:56:30 +02:00
Fredrik Fornwall
964c0b7b4f Cleanup imports 2016-06-26 22:39:46 +02:00
Fredrik Fornwall
a049ea50d7 Update android studio lint configurations 2016-06-26 22:38:52 +02:00
Fredrik Fornwall
95a0878e10 Update gradle configuration 2016-06-26 22:38:36 +02:00
Fredrik Fornwall
5566b13073 Remove stray character 2016-06-26 22:37:12 +02:00
Fredrik Fornwall
9519727f38 Enable installation of x86-64 packages 2016-06-22 01:31:21 +02:00
Fredrik Fornwall
33d1477d4a Remove KeyboardModifiers 2016-06-22 00:24:42 +02:00
Fredrik Fornwall
1cc7829847 Update version 2016-06-22 00:24:18 +02:00
Fredrik Fornwall
d17bbab8ee Strings update for process killing 2016-06-22 00:23:57 +02:00
Fredrik Fornwall
a020d7c484 Add a wcwidth test 2016-06-22 00:23:38 +02:00
Fredrik Fornwall
9be6470d19 Add .idea/inspectionProfiles/ 2016-06-22 00:23:18 +02:00
Fredrik Fornwall
491240ee3f Fix MockTerminalOutput to implement all methods 2016-06-08 16:09:42 +02:00
Fredrik Fornwall
599aaff723 Update android gradle plugin 2016-06-08 16:09:23 +02:00
Fredrik Fornwall
20d57908a7 Make cursor visible by forcing to text color 2016-06-08 02:13:14 +02:00
Fredrik Fornwall
2104252244 Change session exit detection
Previously we waited for all opened file descriptors to the terminal
to be closed. This caused problem when e.g. running "sleep 900 &"
and then exiting the shell, with sleep keeping the session alive and
had to be killed manually (killing the process group did not help -
the shell had already exited and was in zombie state). This is also
what most other terminal emulators do.

Relatedly, switch to sending SIGKILL to force quit a session instead
of SIGHUP, since SIGHUP can be ignored.
2016-06-08 01:37:08 +02:00
Fredrik Fornwall
f047160fd6 Tweak layout for extra keys view 2016-06-06 01:16:20 +02:00
Fredrik Fornwall
a2ebcdcf49 Extra keys view: Implement sending text 2016-06-06 00:56:42 +02:00
Fredrik Fornwall
0861be363b Remove some inspect code warnings 2016-05-20 10:46:48 +02:00
Fredrik Fornwall
d1c0b6abdc Add initial support for extra keys 2016-05-20 10:44:23 +02:00
Fredrik Fornwall
8714800c6b Add an extra keys view 2016-05-20 10:41:38 +02:00
Fredrik Fornwall
042fbfaea3 TerminalView: Start support for extra keys 2016-05-20 10:41:07 +02:00
Fredrik Fornwall
08d6d1706d Add pref for showing extra keys 2016-05-20 10:36:20 +02:00
Fredrik Fornwall
cf19d43bb7 Gradle build updates
- Switch to using gradle to build jni lib.
- Enable proguard minification.
- Add the Android support library.
2016-05-20 10:30:25 +02:00
Fredrik Fornwall
f86c7a85d3 Update .idea config 2016-05-20 10:10:14 +02:00
Fredrik Fornwall
887d7810f6 Update build tools versions for travis 2016-05-20 10:09:39 +02:00
Fredrik Fornwall
5be3099a5b Update build tools SDK version 2016-05-13 00:18:51 +02:00
Fredrik Fornwall
bdd5c80fca Commit the text on finishComposingText()
This handles e.g. text written with hand writing input methods
as mentioned in #91.
2016-05-09 15:39:11 +02:00
Fredrik Fornwall
cc7b6cba13 Change minimum cols&rows from 8 to 4
This avoids e.g. the keyboard overlapping the terminal in setups
that can actually happen. Closes #88.
2016-05-04 22:44:38 +02:00
Fredrik Fornwall
ff2f77c427 Mark reset() private 2016-05-04 22:27:43 +02:00
Fredrik Fornwall
afaa91b2ca Update gradle 2016-04-28 11:07:43 +02:00
Fredrik Fornwall
46da1fc833 termux.c: Re-indent whole file with vim 2016-04-23 12:21:36 +02:00
Fredrik Fornwall
746dc750df Bump version to 0.34 2016-04-22 03:48:28 +02:00
Fredrik Fornwall
7db1f6c5a1 Add explicit handling of switch constant 2016-04-22 03:30:09 +02:00
Fredrik Fornwall
b7f3fdf528 Update android-annotations version 2016-04-22 03:28:50 +02:00
Fredrik Fornwall
6e7f777d04 Remove support for ACTION_GET_CONTENT
As we have a document provider now we can remove ACTION_GET_CONTENT.

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

- http://developer.android.com/guide/topics/providers/document-provider.html#43
2016-04-22 03:26:23 +02:00
Fredrik Fornwall
fc15bd2355 Documents provider: Remove FLAG_SUPPORTS_RECENTS 2016-04-22 02:51:13 +02:00
Fredrik Fornwall
a87cbdd70c Make VolumeUp+x send Alt+x 2016-04-22 02:20:10 +02:00
Fredrik Fornwall
fb7f7d249e Ignore warnings about setExecutable(true) failing 2016-04-22 02:17:41 +02:00
Fredrik Fornwall
026d0b495e Expose files through the Storage Access Framework
This allows e.g. external editors to edit files in the Termux home
folder. Fixes #79.
2016-04-22 02:13:50 +02:00
Fredrik Fornwall
533fa60516 Remove unused imports 2016-04-22 02:00:36 +02:00
Fredrik Fornwall
dc086a1e0b Tweak button ordering on the file received dialog 2016-04-16 23:02:20 +02:00
Fredrik Fornwall
2a056aeb2e Match less intents for receiving files
Be liberal when accepting SEND intents, but restrict to text files
for VIEW intents.
2016-04-16 23:01:19 +02:00
Fredrik Fornwall
9e70ebc2a6 Build native libraries for 64-bit arm 2016-04-16 21:22:04 +02:00
Fredrik Fornwall
9686127f81 Fix installer to check supported abi:s
This fixes installation on e.g. the Samsung Galaxy S5 Neo which has
a 64-bit cpu but no 64-bit runtime available (closes #69).
2016-04-16 21:18:21 +02:00
Fredrik Fornwall
395c36ee83 Fix typo 2016-04-11 14:13:02 +02:00
Fredrik Fornwall
906ff24e76 Avoid repetition of home path constant 2016-04-11 14:12:21 +02:00
Fredrik Fornwall
c8af974852 Bump version in preparation for 0.33 2016-04-11 14:06:23 +02:00
Fredrik Fornwall
481339e2f5 Do not force chooser when opening url 2016-04-11 14:05:31 +02:00
Fredrik Fornwall
b2ecae63a8 Update to Android Studio 2.0 2016-04-11 13:50:14 +02:00
Fredrik Fornwall
a67f798f2f Update the android gradle plugin 2016-04-11 13:48:24 +02:00
92 changed files with 8397 additions and 10193 deletions

1
.gitignore vendored
View File

@@ -6,6 +6,7 @@
build/ build/
*.apk *.apk
*.so *.so
.externalNativeBuild
# Crashlytics configuations # Crashlytics configuations
com_crashlytics_export_strings.xml com_crashlytics_export_strings.xml

View File

@@ -77,7 +77,7 @@
<match> <match>
<AND> <AND>
<NAME>xmlns:android</NAME> <NAME>xmlns:android</NAME>
<XML_NAMESPACE>Namespace:</XML_NAMESPACE> <XML_NAMESPACE>^$</XML_NAMESPACE>
</AND> </AND>
</match> </match>
</rule> </rule>
@@ -87,7 +87,7 @@
<match> <match>
<AND> <AND>
<NAME>xmlns:.*</NAME> <NAME>xmlns:.*</NAME>
<XML_NAMESPACE>Namespace:</XML_NAMESPACE> <XML_NAMESPACE>^$</XML_NAMESPACE>
</AND> </AND>
</match> </match>
<order>BY_NAME</order> <order>BY_NAME</order>
@@ -224,6 +224,6 @@
</codeStyleSettings> </codeStyleSettings>
</value> </value>
</option> </option>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="TermuxCodeStyle" /> <option name="PREFERRED_PROJECT_CODE_STYLE" value="Default (1)" />
</component> </component>
</project> </project>

2
.idea/gradle.xml generated
View File

@@ -6,13 +6,13 @@
<option name="disableWrapperSourceDistributionNotification" value="true" /> <option name="disableWrapperSourceDistributionNotification" value="true" />
<option name="distributionType" value="DEFAULT_WRAPPED" /> <option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="1.8" />
<option name="modules"> <option name="modules">
<set> <set>
<option value="$PROJECT_DIR$" /> <option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" /> <option value="$PROJECT_DIR$/app" />
</set> </set>
</option> </option>
<option name="resolveModulePerSourceSet" value="false" />
</GradleProjectSettings> </GradleProjectSettings>
</option> </option>
</component> </component>

View File

@@ -1,15 +1,71 @@
<component name="InspectionProjectProfileManager"> <component name="InspectionProjectProfileManager">
<profile version="1.0"> <profile version="1.0">
<option name="myName" value="Project Default" /> <option name="myName" value="Project Default" />
<inspection_tool class="AndroidLintGoogleAppIndexingWarning" enabled="false" level="WARNING" enabled_by_default="false" /> <inspection_tool class="AndroidLintLogConditional" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="EmptyStatementBody" 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" /> <option name="m_reportEmptyBlocks" value="true" />
<option name="commentsAreContent" value="true" />
</inspection_tool> </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"> <inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" /> <option name="processCode" value="true" />
<option name="processLiterals" value="true" /> <option name="processLiterals" value="true" />
<option name="processComments" value="true" /> <option name="processComments" value="true" />
</inspection_tool> </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> </profile>
</component> </component>

View File

@@ -1,5 +1,6 @@
sudo: true
language: android language: android
sudo: false jdk: oraclejdk8
env: env:
global: global:
@@ -11,10 +12,15 @@ android:
components: components:
- platform-tools - platform-tools
- tools - tools
- build-tools-23.0.2 - build-tools-25.0.1
- android-23 - android-25
- extra-android-m2repository - extra-android-m2repository
before_install:
- git clone https://github.com/urho3d/android-ndk.git $HOME/android-ndk
- export ANDROID_NDK_HOME=$HOME/android-ndk
- echo -n | openssl s_client -connect scan.coverity.com:443 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' | sudo tee -a /etc/ssl/certs/ca-certificates.crt
script: script:
- ./gradlew testDebugUnitTest - ./gradlew testDebugUnitTest
@@ -25,5 +31,5 @@ addons:
description: "Terminal emulator and Linux environment for Android" description: "Terminal emulator and Linux environment for Android"
notification_email: fredrik@fornwall.net notification_email: fredrik@fornwall.net
build_command_prepend: "./gradlew clean" build_command_prepend: "./gradlew clean"
build_command: "./gradlew assemble" build_command: "./gradlew build"
branch_pattern: coverity_scan branch_pattern: master

View File

@@ -3,25 +3,19 @@ Termux app
[![Travis build status](https://travis-ci.org/termux/termux-app.svg?branch=master)](https://travis-ci.org/termux/termux-app) [![Travis build status](https://travis-ci.org/termux/termux-app.svg?branch=master)](https://travis-ci.org/termux/termux-app)
[![Join the chat at https://gitter.im/termux/termux](https://badges.gitter.im/termux/termux.svg)](https://gitter.im/termux/termux) [![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 is an Android terminal app and Linux environment.
* [Termux on Google Play Store](https://play.google.com/store/apps/details?id=com.termux) * [Termux on Google Play Store](https://play.google.com/store/apps/details?id=com.termux)
* [Termux on F-Droid](https://f-droid.org/repository/browse/?fdid=com.termux) * [Termux on F-Droid](https://f-droid.org/repository/browse/?fdid=com.termux)
* [termux.com](http://termux.com)
* [Termux Help](http://termux.com/help/) * [Termux Help](http://termux.com/help/)
* [Termux app on GitHub](https://github.com/termux/termux-app)
* [Termux packages on GitHub](https://github.com/termux/termux-packages)
* [Termux Google+ community](http://termux.com/community/) * [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)
License License
======= =======
Released under [the GPLv3 license](https://www.gnu.org/licenses/gpl.html). Contains code from `Terminal Emulator for Android` which is released under [the Apache License 2.0](https://www.apache.org/licenses/). Released under [the GPLv3 license](https://www.gnu.org/licenses/gpl.html). Contains code from `Terminal Emulator for Android` which is released under [the Apache License 2.0](https://www.apache.org/licenses/).
Building JNI libraries
======================
Execute the `build-jnilibs.sh` script to build the required JNI libraries.
Terminal resources Terminal resources
================== ==================
* [XTerm control sequences](http://invisible-island.net/xterm/ctlseqs/ctlseqs.html) * [XTerm control sequences](http://invisible-island.net/xterm/ctlseqs/ctlseqs.html)

View File

@@ -1,33 +1,45 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
android { android {
compileSdkVersion 23 compileSdkVersion 25
buildToolsVersion "23.0.2" buildToolsVersion "25.0.1"
dependencies { dependencies {
compile 'com.android.support:support-annotations:23.1.1' compile 'com.android.support:support-annotations:25.0.1'
} compile "com.android.support:support-v4:25.0.1"
sourceSets {
main {
jni.srcDirs = []
}
} }
defaultConfig { defaultConfig {
applicationId "com.termux" applicationId "com.termux"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 23 targetSdkVersion 25
versionCode 32 versionCode 44
versionName "0.32" versionName "0.44"
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 { buildTypes {
release { release {
minifyEnabled false minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
} }
} }
externalNativeBuild {
ndkBuild {
path "src/main/jni/Android.mk"
}
}
} }
dependencies { dependencies {

View File

@@ -15,7 +15,8 @@
<application <application
android:allowBackup="true" android:allowBackup="true"
android:fullBackupContent="@xml/backupscheme" android:fullBackupContent="@xml/backupscheme"
android:icon="@drawable/ic_launcher" android:icon="@mipmap/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"
@@ -34,6 +35,7 @@
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" /> <category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter> </intent-filter>
<meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcuts" />
</activity> </activity>
<activity <activity
@@ -49,8 +51,8 @@
android:taskAffinity="com.termux.filereceiver" android:taskAffinity="com.termux.filereceiver"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:noHistory="true"> android:noHistory="true">
<!-- Accept multiple file types when sending. -->
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.SEND"/> <action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/*" /> <data android:mimeType="application/*" />
@@ -61,32 +63,28 @@
<data android:mimeType="text/*" /> <data android:mimeType="text/*" />
<data android:mimeType="video/*" /> <data android:mimeType="video/*" />
</intent-filter> </intent-filter>
</activity> <!-- Be more restrictive for viewing files, restricting ourselves to text files. -->
<activity
android:name="com.termux.filepicker.TermuxFilePickerActivity"
android:label="@string/application_name"
android:theme="@android:style/Theme.Material.Light.DarkActionBar"
android:noHistory="true">
<intent-filter> <intent-filter>
<!-- <action android:name="android.intent.action.VIEW"/>
http://stackoverflow.com/questions/6486716/using-intent-action-pick-for-specific-path
"That said, you should consider ACTION_PICK deprecated. The modern action is ACTION_GET_CONTENT
which is much better supported; you will find support of ACTION_PICK spotty and inconsistent.
Unfortunately ACTION_GET_CONTENT also does not let you specify a directory."
-->
<action android:name="android.intent.action.GET_CONTENT" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.OPENABLE" /> <data android:mimeType="text/*" />
<data android:mimeType="*/*" /> <data android:mimeType="application/json" />
<data android:mimeType="application/*xml*" />
<data android:mimeType="application/*latex*" />
<data android:mimeType="application/javascript" />
</intent-filter> </intent-filter>
</activity> </activity>
<provider android:authorities="com.termux.filepicker.provider" <provider
android:readPermission="com.termux.filepickder.READ" android:name=".filepicker.TermuxDocumentsProvider"
android:exported="true" android:authorities="com.termux.documents"
android:grantUriPermissions="true" android:grantUriPermissions="true"
android:name="com.termux.filepicker.TermuxFilePickerProvider" /> android:exported="true"
android:permission="android.permission.MANAGE_DOCUMENTS">
<intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
</intent-filter>
</provider>
<service <service
android:name="com.termux.app.TermuxService" android:name="com.termux.app.TermuxService"

View File

@@ -0,0 +1,195 @@
package com.termux.app;
import android.util.Log;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* A background job launched by Termux.
*/
public final class BackgroundJob {
private static final String LOG_TAG = "termux-task";
final Process mProcess;
public BackgroundJob(String cwd, String fileToExecute, final String[] args, final TermuxService service) {
String[] env = buildEnvironment(false, cwd);
if (cwd == null) cwd = TermuxService.HOME_PATH;
final String[] progArray = setupProcessArgs(fileToExecute, args);
final String processDescription = Arrays.toString(progArray);
Process process;
try {
process = Runtime.getRuntime().exec(progArray, env, new File(cwd));
} catch (IOException e) {
mProcess = null;
// TODO: Visible error message?
Log.e(LOG_TAG, "Failed running background job: " + processDescription, e);
return;
}
mProcess = process;
final int pid = getPid(mProcess);
new Thread() {
@Override
public void run() {
Log.i(LOG_TAG, "[" + pid + "] starting: " + processDescription);
InputStream stdout = mProcess.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(stdout, StandardCharsets.UTF_8));
String line;
try {
// FIXME: Long lines.
while ((line = reader.readLine()) != null) {
Log.i(LOG_TAG, "[" + pid + "] stdout: " + line);
}
} catch (IOException e) {
Log.e(LOG_TAG, "Error reading output", e);
}
try {
int exitCode = mProcess.waitFor();
service.onBackgroundJobExited(BackgroundJob.this);
if (exitCode == 0) {
Log.i(LOG_TAG, "[" + pid + "] exited normally");
} else {
Log.w(LOG_TAG, "[" + pid + "] exited with code: " + exitCode);
}
} catch (InterruptedException e) {
// Ignore.
}
}
}.start();
new Thread() {
@Override
public void run() {
InputStream stderr = mProcess.getErrorStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(stderr, StandardCharsets.UTF_8));
String line;
try {
// FIXME: Long lines.
while ((line = reader.readLine()) != null) {
Log.i(LOG_TAG, "[" + pid + "] stderr: " + line);
}
} catch (IOException e) {
// Ignore.
}
}
};
}
public static String[] buildEnvironment(boolean failSafe, String cwd) {
new File(TermuxService.HOME_PATH).mkdirs();
if (cwd == null) cwd = TermuxService.HOME_PATH;
final String termEnv = "TERM=xterm-256color";
final String homeEnv = "HOME=" + TermuxService.HOME_PATH;
final String prefixEnv = "PREFIX=" + TermuxService.PREFIX_PATH;
final String androidRootEnv = "ANDROID_ROOT=" + System.getenv("ANDROID_ROOT");
final String androidDataEnv = "ANDROID_DATA=" + System.getenv("ANDROID_DATA");
// EXTERNAL_STORAGE is needed for /system/bin/am to work on at least
// Samsung S7 - see https://plus.google.com/110070148244138185604/posts/gp8Lk3aCGp3.
final String externalStorageEnv = "EXTERNAL_STORAGE=" + System.getenv("EXTERNAL_STORAGE");
String[] env;
if (failSafe) {
// Keep the default path so that system binaries can be used in the failsafe session.
final String pathEnv = "PATH=" + System.getenv("PATH");
return new String[]{termEnv, homeEnv, prefixEnv, androidRootEnv, androidDataEnv, pathEnv, externalStorageEnv};
} else {
final String ps1Env = "PS1=$ ";
final String ldEnv = "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";
final String pwdEnv = "PWD=" + cwd;
return new String[]{termEnv, homeEnv, prefixEnv, ps1Env, ldEnv, langEnv, pathEnv, pwdEnv, androidRootEnv, androidDataEnv, externalStorageEnv};
}
}
public static int getPid(Process p) {
try {
Field f = p.getClass().getDeclaredField("pid");
f.setAccessible(true);
try {
return f.getInt(p);
} finally {
f.setAccessible(false);
}
} catch (Throwable e) {
return -1;
}
}
static String[] setupProcessArgs(String fileToExecute, String[] args) {
// The file to execute may either be:
// - An elf file, in which we execute it directly.
// - A script file without shebang, which we execute with our standard shell $PREFIX/bin/sh instead of the
// system /system/bin/sh. The system shell may vary and may not work at all due to LD_LIBRARY_PATH.
// - A file with shebang, which we try to handle with e.g. /bin/foo -> $PREFIX/bin/foo.
String interpreter = null;
try {
File file = new File(fileToExecute);
try (FileInputStream in = new FileInputStream(file)) {
byte[] buffer = new byte[256];
int bytesRead = in.read(buffer);
if (bytesRead > 4) {
if (buffer[0] == 0x7F && buffer[1] == 'E' && buffer[2] == 'L' && buffer[3] == 'F') {
// Elf file, do nothing.
} else if (buffer[0] == '#' && buffer[1] == '!') {
// Try to parse shebang.
StringBuilder builder = new StringBuilder();
for (int i = 2; i < bytesRead; i++) {
char c = (char) buffer[i];
if (c == ' ' || c == '\n') {
if (builder.length() == 0) {
// Skip whitespace after shebang.
continue;
} else {
// End of shebang.
String executable = builder.toString();
if (executable.startsWith("/usr") || executable.startsWith("/bin")) {
String[] parts = executable.split("/");
String binary = parts[parts.length - 1];
interpreter = TermuxService.PREFIX_PATH + "/bin/" + binary;
}
break;
}
} else {
builder.append(c);
}
}
} else {
// No shebang and no ELF, use standard shell.
interpreter = TermuxService.PREFIX_PATH + "/bin/sh";
}
}
}
} catch (IOException e) {
// Ignore.
}
List<String> result = new ArrayList<>();
if (interpreter != null) result.add(interpreter);
result.add(fileToExecute);
if (args != null) {
for (String arg : args) result.add(arg);
}
return result.toArray(new String[result.size()]);
}
}

View File

@@ -0,0 +1,177 @@
package com.termux.app;
import android.content.Context;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.View;
import android.widget.Button;
import android.widget.GridLayout;
import android.widget.ToggleButton;
import com.termux.R;
import com.termux.terminal.TerminalSession;
import com.termux.view.TerminalView;
/**
* A view showing extra keys (such as Escape, Ctrl, Alt) not normally available on an Android soft
* keyboard.
*/
public final class ExtraKeysView extends GridLayout {
private static final int TEXT_COLOR = 0xFFFFFFFF;
public ExtraKeysView(Context context, AttributeSet attrs) {
super(context, attrs);
reload();
}
static void sendKey(View view, String keyName) {
int keyCode = 0;
String chars = null;
switch (keyName) {
case "ESC":
keyCode = KeyEvent.KEYCODE_ESCAPE;
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 {
TerminalView terminalView = (TerminalView) view.findViewById(R.id.terminal_view);
TerminalSession session = terminalView.getCurrentSession();
if (session != null) session.write(chars);
}
}
private ToggleButton controlButton;
private ToggleButton altButton;
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;
}
public boolean readAltButton() {
if (altButton.isPressed()) return true;
boolean result = altButton.isChecked();
if (result) {
altButton.setChecked(false);
altButton.setTextColor(TEXT_COLOR);
}
return result;
}
public boolean readFnButton() {
if (fnButton.isPressed()) return true;
boolean result = fnButton.isChecked();
if (result) {
fnButton.setChecked(false);
fnButton.setTextColor(TEXT_COLOR);
}
return result;
}
void reload() {
altButton = controlButton = null;
removeAllViews();
String[][] buttons = {
{"ESC", "CTRL", "ALT", "TAB", "", "/", "|"}
};
final int rows = buttons.length;
final int cols = buttons[0].length;
setRowCount(rows);
setColumnCount(cols);
for (int row = 0; row < rows; row++) {
for (int col = 0; col < cols; col++) {
final String buttonText = buttons[row][col];
Button button;
switch (buttonText) {
case "CTRL":
button = controlButton = new ToggleButton(getContext(), null, android.R.attr.buttonBarButtonStyle);
button.setClickable(true);
break;
case "ALT":
button = altButton = new ToggleButton(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);
button.setTextColor(TEXT_COLOR);
final Button finalButton = button;
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
View root = getRootView();
switch (buttonText) {
case "CTRL":
case "ALT":
case "FN":
ToggleButton self = (ToggleButton) finalButton;
self.setChecked(self.isChecked());
self.setTextColor(self.isChecked() ? 0xFF80DEEA : TEXT_COLOR);
break;
default:
sendKey(root, buttonText);
break;
}
}
});
GridLayout.LayoutParams param = new GridLayout.LayoutParams();
param.height = param.width = 0;
param.rightMargin = param.topMargin = 0;
param.setGravity(Gravity.LEFT);
float weight = "▲▼◀▶".contains(buttonText) ? 0.7f : 1.f;
param.columnSpec = GridLayout.spec(col, GridLayout.FILL, weight);
param.rowSpec = GridLayout.spec(row, GridLayout.FILL, 1.f);
button.setLayoutParams(param);
addView(button);
}
}
}
}

View File

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

@@ -17,7 +17,6 @@ import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.content.ServiceConnection; import android.content.ServiceConnection;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.Typeface; import android.graphics.Typeface;
@@ -30,10 +29,14 @@ import android.os.IBinder;
import android.os.Vibrator; import android.os.Vibrator;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; 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;
import android.text.style.StyleSpan; import android.text.style.StyleSpan;
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;
@@ -41,38 +44,44 @@ 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.MotionEvent;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
import android.view.View.OnKeyListener;
import android.view.View.OnLongClickListener; 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;
import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemClickListener;
import android.widget.AdapterView.OnItemLongClickListener; import android.widget.AdapterView.OnItemLongClickListener;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ListView; import android.widget.ListView;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import com.termux.R; import com.termux.R;
import com.termux.drawer.DrawerLayout; import com.termux.terminal.EmulatorDebug;
import com.termux.terminal.TerminalColors;
import com.termux.terminal.TerminalSession; import com.termux.terminal.TerminalSession;
import com.termux.terminal.TerminalSession.SessionChangedCallback; import com.termux.terminal.TerminalSession.SessionChangedCallback;
import com.termux.view.TerminalKeyListener; import com.termux.terminal.TextStyle;
import com.termux.view.TerminalView; import com.termux.view.TerminalView;
import java.io.File;
import java.io.FileInputStream;
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.Properties;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
/** /**
* A terminal emulator activity. * A terminal emulator activity.
* * <p/>
* See * See
* <ul> * <ul>
* <li>http://www.mongrel-phones.com.au/default/how_to_make_a_local_service_and_bind_to_it_in_android</li> * <li>http://www.mongrel-phones.com.au/default/how_to_make_a_local_service_and_bind_to_it_in_android</li>
@@ -98,7 +107,11 @@ public final class TermuxActivity extends Activity implements ServiceConnection
private static final String RELOAD_STYLE_ACTION = "com.termux.app.reload_style"; private static final String RELOAD_STYLE_ACTION = "com.termux.app.reload_style";
/** The main view of the activity showing the terminal. Initialized in onCreate(). */ /** The main view of the activity showing the terminal. Initialized in onCreate(). */
@SuppressWarnings("NullableProblems") @NonNull TerminalView mTerminalView; @SuppressWarnings("NullableProblems")
@NonNull
TerminalView mTerminalView;
ExtraKeysView mExtraKeysView;
final FullScreenHelper mFullScreenHelper = new FullScreenHelper(this); final FullScreenHelper mFullScreenHelper = new FullScreenHelper(this);
@@ -123,10 +136,12 @@ public final class TermuxActivity extends Activity implements ServiceConnection
*/ */
boolean mIsVisible; boolean mIsVisible;
private 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();
private int mBellSoundId; int mBellSoundId;
Animation mOnBellAnimation;
private final BroadcastReceiver mBroadcastReceiever = new BroadcastReceiver() { private final BroadcastReceiver mBroadcastReceiever = new BroadcastReceiver() {
@Override @Override
@@ -134,20 +149,55 @@ public final class TermuxActivity extends Activity implements ServiceConnection
if (mIsVisible) { if (mIsVisible) {
String whatToReload = intent.getStringExtra(RELOAD_STYLE_ACTION); String whatToReload = intent.getStringExtra(RELOAD_STYLE_ACTION);
if ("storage".equals(whatToReload)) { if ("storage".equals(whatToReload)) {
if (ensureStoragePermissionGranted()) TermuxInstaller.setupStorageSymlinks(TermuxActivity.this); if (ensureStoragePermissionGranted())
TermuxInstaller.setupStorageSymlinks(TermuxActivity.this);
return; return;
} }
mTerminalView.checkForFontAndColors(); checkForFontAndColors();
mSettings.reloadFromProperties(TermuxActivity.this); mSettings.reloadFromProperties(TermuxActivity.this);
} }
} }
}; };
void checkForFontAndColors() {
try {
// Hard-coded paths since this file is used also in Termux:Float.
@SuppressLint("SdCardPath") File fontFile = new File("/data/data/com.termux/files/home/.termux/font.ttf");
@SuppressLint("SdCardPath") File colorsFile = new File("/data/data/com.termux/files/home/.termux/colors.properties");
final Properties props = new Properties();
if (colorsFile.isFile()) {
try (InputStream in = new FileInputStream(colorsFile)) {
props.load(in);
}
}
TerminalColors.COLOR_SCHEME.updateWith(props);
TerminalSession session = getCurrentTermSession();
if (session != null && session.getEmulator() != null) {
session.getEmulator().mColors.reset();
}
updateBackgroundColor();
final Typeface newTypeface = (fontFile.exists() && fontFile.length() > 0) ? Typeface.createFromFile(fontFile) : Typeface.MONOSPACE;
mTerminalView.setTypeface(newTypeface);
} catch (Exception e) {
Log.e(EmulatorDebug.LOG_TAG, "Error in checkForFontAndColors()", e);
}
}
void updateBackgroundColor() {
TerminalSession session = getCurrentTermSession();
if (session != null && session.getEmulator() != null) {
getWindow().getDecorView().setBackgroundColor(session.getEmulator().mColors.mCurrentColors[TextStyle.COLOR_INDEX_BACKGROUND]);
}
}
/** For processes to access shared internal storage (/sdcard) we need this permission. */ /** For processes to access shared internal storage (/sdcard) we need this permission. */
@TargetApi(Build.VERSION_CODES.M) @TargetApi(Build.VERSION_CODES.M)
public boolean ensureStoragePermissionGranted() { public boolean ensureStoragePermissionGranted() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (checkSelfPermission(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
return true; return true;
} else { } else {
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUESTCODE_PERMISSION_STORAGE); requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUESTCODE_PERMISSION_STORAGE);
@@ -163,130 +213,86 @@ public final class TermuxActivity extends Activity implements ServiceConnection
public void onCreate(Bundle bundle) { public void onCreate(Bundle bundle) {
super.onCreate(bundle); super.onCreate(bundle);
// Prevent overdraw: mOnBellAnimation = AnimationUtils.loadAnimation(this, R.anim.on_bell);
getWindow().getDecorView().setBackground(null);
mSettings = new TermuxPreferences(this);
setContentView(R.layout.drawer_layout); setContentView(R.layout.drawer_layout);
mTerminalView = (TerminalView) findViewById(R.id.terminal_view); mTerminalView = (TerminalView) findViewById(R.id.terminal_view);
mSettings = new TermuxPreferences(this); mTerminalView.setOnKeyListener(new TermuxKeyListener(this));
mTerminalView.setTextSize(mSettings.getFontSize()); mTerminalView.setTextSize(mSettings.getFontSize());
mFullScreenHelper.setImmersive(mSettings.isFullScreen()); mFullScreenHelper.setImmersive(mSettings.isFullScreen());
mTerminalView.requestFocus(); mTerminalView.requestFocus();
OnKeyListener keyListener = new OnKeyListener() { final ViewPager viewPager = (ViewPager) findViewById(R.id.viewpager);
if (mSettings.isShowExtraKeys()) viewPager.setVisibility(View.VISIBLE);
viewPager.setAdapter(new PagerAdapter() {
@Override @Override
public boolean onKey(View v, int keyCode, KeyEvent event) { public int getCount() {
if (event.getAction() != KeyEvent.ACTION_DOWN) return false; return 2;
}
final TerminalSession currentSession = getCurrentTermSession(); @Override
if (currentSession == null) return false; public boolean isViewFromObject(View view, Object object) {
return view == object;
}
if (keyCode == KeyEvent.KEYCODE_ENTER && !currentSession.isRunning()) { @Override
// Return pressed with finished session - remove it. public Object instantiateItem(ViewGroup collection, int position) {
currentSession.finishIfRunning(); LayoutInflater inflater = LayoutInflater.from(TermuxActivity.this);
View layout;
int index = mTermService.removeTermSession(currentSession); if (position == 0) {
mListViewAdapter.notifyDataSetChanged(); layout = mExtraKeysView = (ExtraKeysView) inflater.inflate(R.layout.extra_keys_main, collection, false);
if (mTermService.getSessions().isEmpty()) {
// There are no sessions to show, so finish the activity.
finish();
} else { } else {
if (index >= mTermService.getSessions().size()) { layout = inflater.inflate(R.layout.extra_keys_right, collection, false);
index = mTermService.getSessions().size() - 1; final EditText editText = (EditText) layout.findViewById(R.id.text_input);
editText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
TerminalSession session = getCurrentTermSession();
if (session != null) {
if (session.isRunning()) {
session.write(editText.getText().toString() + "\n");
} else {
removeFinishedSession(session);
} }
switchToSession(mTermService.getSessions().get(index)); editText.setText("");
}
return true;
} else if (!(event.isCtrlPressed() && event.isShiftPressed())) {
// Only hook shortcuts with Ctrl+Shift down.
return false;
}
// Get the unmodified code point:
int unicodeChar = event.getUnicodeChar(0);
if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN || unicodeChar == 'n'/* next */) {
int index = mTermService.getSessions().indexOf(currentSession);
if (++index >= mTermService.getSessions().size()) index = 0;
switchToSession(mTermService.getSessions().get(index));
} else if (keyCode == KeyEvent.KEYCODE_DPAD_UP || unicodeChar == 'p' /* previous */) {
int index = mTermService.getSessions().indexOf(currentSession);
if (--index < 0) index = mTermService.getSessions().size() - 1;
switchToSession(mTermService.getSessions().get(index));
} else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
getDrawer().openDrawer(Gravity.START);
} else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
getDrawer().closeDrawers();
} else if (unicodeChar == 'f'/* full screen */) {
toggleImmersive();
} else if (unicodeChar == 'k'/* keyboard */) {
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
} else if (unicodeChar == 'm'/* menu */) {
mTerminalView.showContextMenu();
} else if (unicodeChar == 'r'/* rename */) {
renameSession(currentSession);
} else if (unicodeChar == 'c'/* create */) {
addNewSession(false, null);
} else if (unicodeChar == 'u' /* urls */) {
showUrlSelection();
} else if (unicodeChar == 'v') {
doPaste();
} else if (unicodeChar == '+' || event.getUnicodeChar(KeyEvent.META_SHIFT_ON) == '+') {
// We also check for the shifted char here since shift may be required to produce '+',
// see https://github.com/termux/termux-api/issues/2
changeFontSize(true);
} else if (unicodeChar == '-') {
changeFontSize(false);
} else if (unicodeChar >= '1' && unicodeChar <= '9') {
int num = unicodeChar - '1';
if (mTermService.getSessions().size() > num) switchToSession(mTermService.getSessions().get(num));
} }
return true; return true;
} }
}; });
mTerminalView.setOnKeyListener(keyListener);
findViewById(R.id.left_drawer_list).setOnKeyListener(keyListener);
mTerminalView.setOnKeyListener(new TerminalKeyListener() {
@Override
public float onScale(float scale) {
if (scale < 0.9f || scale > 1.1f) {
boolean increase = scale > 1.f;
changeFontSize(increase);
return 1.0f;
} }
return scale; collection.addView(layout);
return layout;
} }
@Override @Override
public void onSingleTapUp(MotionEvent e) { public void destroyItem(ViewGroup collection, int position, Object view) {
InputMethodManager mgr = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); collection.removeView((View) view);
mgr.showSoftInput(mTerminalView, InputMethodManager.SHOW_IMPLICIT);
} }
});
viewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
@Override @Override
public boolean shouldBackButtonBeMappedToEscape() { public void onPageSelected(int position) {
return mSettings.mBackIsEscape; if (position == 0) {
mTerminalView.requestFocus();
} else {
final EditText editText = (EditText) viewPager.findViewById(R.id.text_input);
if (editText != null) editText.requestFocus();
} }
@Override
public void copyModeChanged(boolean copyMode) {
// Disable drawer while copying.
getDrawer().setDrawerLockMode(copyMode ? DrawerLayout.LOCK_MODE_LOCKED_CLOSED : DrawerLayout.LOCK_MODE_UNLOCKED);
} }
}); });
View newSessionButton = findViewById(R.id.new_session_button); View newSessionButton = findViewById(R.id.new_session_button);
newSessionButton.setOnClickListener(new OnClickListener() { newSessionButton.setOnClickListener(new OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
addNewSession(false, null); addNewSession(false, null);
} }
}); });
newSessionButton.setOnLongClickListener(new OnLongClickListener() { newSessionButton.setOnLongClickListener(new OnLongClickListener() {
@Override @Override
public boolean onLongClick(View v) { public boolean onLongClick(View v) {
@@ -316,18 +322,37 @@ public final class TermuxActivity extends Activity implements ServiceConnection
} }
}); });
findViewById(R.id.toggle_keyboard_button).setOnLongClickListener(new OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
toggleShowExtraKeys();
return true;
}
});
registerForContextMenu(mTerminalView); registerForContextMenu(mTerminalView);
Intent serviceIntent = new Intent(this, TermuxService.class); Intent serviceIntent = new Intent(this, TermuxService.class);
// Start the service and make it run regardless of who is bound to it: // Start the service and make it run regardless of who is bound to it:
startService(serviceIntent); startService(serviceIntent);
if (!bindService(serviceIntent, this, 0)) throw new RuntimeException("bindService() failed"); if (!bindService(serviceIntent, this, 0))
throw new RuntimeException("bindService() failed");
mTerminalView.checkForFontAndColors(); checkForFontAndColors();
mBellSoundId = mBellSoundPool.load(this, R.raw.bell, 1); mBellSoundId = mBellSoundPool.load(this, R.raw.bell, 1);
} }
void toggleShowExtraKeys() {
final ViewPager viewPager = (ViewPager) findViewById(R.id.viewpager);
final boolean showNow = mSettings.toggleShowExtraKeys(TermuxActivity.this);
viewPager.setVisibility(showNow ? View.VISIBLE : View.GONE);
if (showNow && viewPager.getCurrentItem() == 1) {
// Focus the text input view if just revealed.
findViewById(R.id.text_input).requestFocus();
}
}
/** /**
* Part of the {@link ServiceConnection} interface. The service is bound with * Part of the {@link ServiceConnection} interface. The service is bound with
* {@link #bindService(Intent, ServiceConnection, int)} in {@link #onCreate(Bundle)} which will cause a call to this * {@link #bindService(Intent, ServiceConnection, int)} in {@link #onCreate(Bundle)} which will cause a call to this
@@ -367,7 +392,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
// Show toast for non-current sessions that exit. // Show toast for non-current sessions that exit.
int indexOfSession = mTermService.getSessions().indexOf(finishedSession); int indexOfSession = mTermService.getSessions().indexOf(finishedSession);
// Verify that session was not removed before we got told about it finishing: // Verify that session was not removed before we got told about it finishing:
if (indexOfSession >= 0) showToast(toToastTitle(finishedSession) + " - exited", true); if (indexOfSession >= 0)
showToast(toToastTitle(finishedSession) + " - exited", true);
} }
mListViewAdapter.notifyDataSetChanged(); mListViewAdapter.notifyDataSetChanged();
} }
@@ -382,7 +408,10 @@ public final class TermuxActivity extends Activity implements ServiceConnection
@Override @Override
public void onBell(TerminalSession session) { public void onBell(TerminalSession session) {
if (mIsVisible) { if (!mIsVisible) return;
mTerminalView.startAnimation(mOnBellAnimation);
switch (mSettings.mBellBehaviour) { switch (mSettings.mBellBehaviour) {
case TermuxPreferences.BELL_BEEP: case TermuxPreferences.BELL_BEEP:
mBellSoundPool.play(mBellSoundId, 1.f, 1.f, 1, 0, 1.f); mBellSoundPool.play(mBellSoundId, 1.f, 1.f, 1, 0, 1.f);
@@ -390,9 +419,16 @@ public final class TermuxActivity extends Activity implements ServiceConnection
case TermuxPreferences.BELL_VIBRATE: case TermuxPreferences.BELL_VIBRATE:
((Vibrator) getSystemService(VIBRATOR_SERVICE)).vibrate(50); ((Vibrator) getSystemService(VIBRATOR_SERVICE)).vibrate(50);
break; break;
case TermuxPreferences.BELL_IGNORE:
// Ignore the bell character.
break;
} }
} }
@Override
public void onColorsChanged(TerminalSession changedSession) {
if (getCurrentTermSession() == changedSession) updateBackgroundColor();
} }
}; };
@@ -401,8 +437,9 @@ public final class TermuxActivity extends Activity implements ServiceConnection
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);
@NonNull
@Override @Override
public View getView(int position, View convertView, ViewGroup parent) { public View getView(int position, View convertView, @NonNull ViewGroup parent) {
View row = convertView; View row = convertView;
if (row == null) { if (row == null) {
LayoutInflater inflater = getLayoutInflater(); LayoutInflater inflater = getLayoutInflater();
@@ -484,10 +521,27 @@ public final class TermuxActivity extends Activity implements ServiceConnection
// The service connected while not in foreground - just bail out. // The service connected while not in foreground - just bail out.
finish(); finish();
} }
} else {
Intent i = getIntent();
if (i != null && i.getAction().equals(Intent.ACTION_RUN)) {
// Android 7.1 app shortcut from res/xml/shortcuts.xml.
addNewSession(false, null);
} else { } else {
switchToSession(getStoredCurrentSessionOrLast()); switchToSession(getStoredCurrentSessionOrLast());
} }
} }
}
public void switchToSession(boolean forward) {
TerminalSession currentSession = getCurrentTermSession();
int index = mTermService.getSessions().indexOf(currentSession);
if (forward) {
if (++index >= mTermService.getSessions().size()) index = 0;
} else {
if (--index < 0) index = mTermService.getSessions().size() - 1;
}
switchToSession(mTermService.getSessions().get(index));
}
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
void renameSession(final TerminalSession sessionToRename) { void renameSession(final TerminalSession sessionToRename) {
@@ -495,6 +549,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
@Override @Override
public void onTextSet(String text) { public void onTextSet(String text) {
sessionToRename.mSessionName = text; sessionToRename.mSessionName = text;
mListViewAdapter.notifyDataSetChanged();
} }
}, -1, null, -1, null, null); }, -1, null, -1, null, null);
} }
@@ -507,7 +562,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
} }
} }
@Nullable TerminalSession getCurrentTermSession() { @Nullable
TerminalSession getCurrentTermSession() {
return mTerminalView.getCurrentSession(); return mTerminalView.getCurrentSession();
} }
@@ -541,11 +597,12 @@ public final class TermuxActivity extends Activity implements ServiceConnection
@Override @Override
public void onBackPressed() { public void onBackPressed() {
if (getDrawer().isDrawerOpen(Gravity.START)) if (getDrawer().isDrawerOpen(Gravity.LEFT)) {
getDrawer().closeDrawers(); getDrawer().closeDrawers();
else } else {
finish(); finish();
} }
}
@Override @Override
public void onDestroy() { public void onDestroy() {
@@ -579,7 +636,10 @@ public final class TermuxActivity extends Activity implements ServiceConnection
/** Try switching to session and note about it, but do nothing if already displaying the session. */ /** Try switching to session and note about it, but do nothing if already displaying the session. */
void switchToSession(TerminalSession session) { void switchToSession(TerminalSession session) {
if (mTerminalView.attachSession(session)) noteSessionInfo(); if (mTerminalView.attachSession(session)) {
noteSessionInfo();
updateBackgroundColor();
}
} }
String toToastTitle(TerminalSession session) { String toToastTitle(TerminalSession session) {
@@ -616,7 +676,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
menu.add(Menu.NONE, CONTEXTMENU_SELECT_URL_ID, Menu.NONE, R.string.select_url); menu.add(Menu.NONE, CONTEXTMENU_SELECT_URL_ID, Menu.NONE, R.string.select_url);
menu.add(Menu.NONE, CONTEXTMENU_SHARE_TRANSCRIPT_ID, Menu.NONE, R.string.select_all_and_share); menu.add(Menu.NONE, CONTEXTMENU_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, R.string.kill_process).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_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_HELP_ID, Menu.NONE, R.string.help); menu.add(Menu.NONE, CONTEXTMENU_HELP_ID, Menu.NONE, R.string.help);
@@ -678,7 +738,13 @@ public final class TermuxActivity extends Activity implements ServiceConnection
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
dialog.dismiss(); dialog.dismiss();
String url = (String) urls[position]; String url = (String) urls[position];
startActivity(Intent.createChooser(new Intent(Intent.ACTION_VIEW, Uri.parse(url)), null)); Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
try {
startActivity(i, null);
} catch (ActivityNotFoundException e) {
// If no applications match, Android displays a system message.
startActivity(Intent.createChooser(i, null));
}
return true; return true;
} }
}); });
@@ -738,7 +804,7 @@ 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 android.content.DialogInterface.OnClickListener() { .setPositiveButton(R.string.styling_install, new DialogInterface.OnClickListener() {
@Override @Override
public void onClick(DialogInterface dialog, int which) { 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"))); startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://play.google.com/store/apps/details?id=com.termux.styling")));
@@ -781,7 +847,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
ClipData clipData = clipboard.getPrimaryClip(); ClipData clipData = clipboard.getPrimaryClip();
if (clipData == null) return; if (clipData == null) return;
CharSequence paste = clipData.getItemAt(0).coerceToText(this); CharSequence paste = clipData.getItemAt(0).coerceToText(this);
if (!TextUtils.isEmpty(paste)) getCurrentTermSession().getEmulator().paste(paste.toString()); if (!TextUtils.isEmpty(paste))
getCurrentTermSession().getEmulator().paste(paste.toString());
} }
/** The current session as stored or the last one if that does not exist. */ /** The current session as stored or the last one if that does not exist. */
@@ -801,4 +868,21 @@ public final class TermuxActivity extends Activity implements ServiceConnection
mLastToast.show(); mLastToast.show();
} }
public void removeFinishedSession(TerminalSession finishedSession) {
// Return pressed with finished session - remove it.
TermuxService service = mTermService;
int index = service.removeTermSession(finishedSession);
mListViewAdapter.notifyDataSetChanged();
if (mTermService.getSessions().isEmpty()) {
// There are no sessions to show, so finish the activity.
finish();
} else {
if (index >= service.getSessions().size()) {
index = service.getSessions().size() - 1;
}
switchToSession(service.getSessions().get(index));
}
}
} }

View File

@@ -15,7 +15,7 @@ import android.widget.RelativeLayout;
/** Basic embedded browser for viewing help pages. */ /** Basic embedded browser for viewing help pages. */
public final class TermuxHelpActivity extends Activity { public final class TermuxHelpActivity extends Activity {
private WebView mWebView; WebView mWebView;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {

View File

@@ -7,7 +7,9 @@ import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener; import android.content.DialogInterface.OnClickListener;
import android.content.DialogInterface.OnDismissListener; import android.content.DialogInterface.OnDismissListener;
import android.os.Build;
import android.os.Environment; import android.os.Environment;
import android.os.UserManager;
import android.system.Os; import android.system.Os;
import android.util.Log; import android.util.Log;
import android.util.Pair; import android.util.Pair;
@@ -23,27 +25,28 @@ import java.io.InputStreamReader;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream; import java.util.zip.ZipInputStream;
/** /**
* Install the Termux bootstrap packages if necessary by following the below steps: * Install the Termux bootstrap packages if necessary by following the below steps:
* * <p/>
* (1) If $PREFIX already exist, assume that it is correct and be done. Note that this relies on that we do not create a * (1) If $PREFIX already exist, assume that it is correct and be done. Note that this relies on that we do not create a
* broken $PREFIX folder below. * broken $PREFIX folder below.
* * <p/>
* (2) A progress dialog is shown with "Installing..." message and a spinner. * (2) A progress dialog is shown with "Installing..." message and a spinner.
* * <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/>
* (4) The architecture is determined and an appropriate bootstrap zip url is determined in {@link #determineZipUrl()}. * (4) The architecture is determined and an appropriate bootstrap zip url is determined in {@link #determineZipUrl()}.
* * <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: * continously encountering zip file entries:
* * <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/>
* (5.2) For every other zip entry, extract it into $STAGING_PREFIX and set execute permissions if necessary. * (5.2) For every other zip entry, extract it into $STAGING_PREFIX and set execute permissions if necessary.
*/ */
final class TermuxInstaller { final class TermuxInstaller {
@@ -52,7 +55,7 @@ final class TermuxInstaller {
static void setupIfNeeded(final Activity activity, final Runnable whenDone) { static void setupIfNeeded(final Activity activity, final Runnable whenDone) {
// Termux can only be run as the primary user (device owner) since only that // Termux can only be run as the primary user (device owner) since only that
// account has the expected file system paths. Verify that: // account has the expected file system paths. Verify that:
android.os.UserManager um = (android.os.UserManager) activity.getSystemService(Context.USER_SERVICE); UserManager um = (UserManager) activity.getSystemService(Context.USER_SERVICE);
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)
@@ -95,7 +98,8 @@ final class TermuxInstaller {
String line; String line;
while ((line = symlinksReader.readLine()) != null) { while ((line = symlinksReader.readLine()) != null) {
String[] parts = line.split(""); String[] parts = line.split("");
if (parts.length != 2) throw new RuntimeException("Malformed symlink line: " + line); if (parts.length != 2)
throw new RuntimeException("Malformed symlink line: " + line);
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));
@@ -104,7 +108,8 @@ final class TermuxInstaller {
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()) { if (zipEntry.isDirectory()) {
if (!targetFile.mkdirs()) throw new RuntimeException("Failed to create directory: " + targetFile.getAbsolutePath()); if (!targetFile.mkdirs())
throw new RuntimeException("Failed to create directory: " + targetFile.getAbsolutePath());
} else { } else {
try (FileOutputStream outStream = new FileOutputStream(targetFile)) { try (FileOutputStream outStream = new FileOutputStream(targetFile)) {
int readBytes; int readBytes;
@@ -120,7 +125,8 @@ final class TermuxInstaller {
} }
} }
if (symlinks.isEmpty()) throw new RuntimeException("No SYMLINKS.txt encountered"); if (symlinks.isEmpty())
throw new RuntimeException("No SYMLINKS.txt encountered");
for (Pair<String, String> symlink : symlinks) { for (Pair<String, String> symlink : symlinks) {
Os.symlink(symlink.first, symlink.second); Os.symlink(symlink.first, symlink.second);
} }
@@ -178,16 +184,28 @@ final class TermuxInstaller {
/** Get bootstrap zip url for this systems cpu architecture. */ /** Get bootstrap zip url for this systems cpu architecture. */
static URL determineZipUrl() throws MalformedURLException { static URL determineZipUrl() throws MalformedURLException {
String arch = System.getProperty("os.arch"); String archName = determineTermuxArchName();
if (arch.startsWith("armv8")) { return new URL("https://termux.net/bootstrap/bootstrap-" + archName + ".zip");
arch = "aarch64";
} else if (arch.startsWith("arm")) {
// Handle different arm variants such as armv7l:
arch = "arm";
} else if (arch.startsWith("x86")) { // "x86" on arcwelder, "x86_64" on 64-bit android.
arch = "i686";
} }
return new URL("https://termux.net/bootstrap/bootstrap-" + arch + ".zip");
private static String determineTermuxArchName() {
// Note that we cannot use System.getProperty("os.arch") since that may give e.g. "aarch64"
// while a 64-bit runtime may not be installed (like on the Samsung Galaxy S5 Neo).
// Instead we search through the supported abi:s on the device, see:
// http://developer.android.com/ndk/guides/abis.html
// Note that we search for abi:s in preferred order (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. */ /** Delete a folder and all its content or throw. */
@@ -239,9 +257,13 @@ final class TermuxInstaller {
Os.symlink(moviesDir.getAbsolutePath(), new File(storageDir, "movies").getAbsolutePath()); Os.symlink(moviesDir.getAbsolutePath(), new File(storageDir, "movies").getAbsolutePath());
final File[] dirs = context.getExternalFilesDirs(null); final File[] dirs = context.getExternalFilesDirs(null);
if (dirs != null && dirs.length >= 2) { if (dirs != null && dirs.length > 1) {
final File externalDir = dirs[1]; for (int i = 1; i < dirs.length; i++) {
Os.symlink(externalDir.getAbsolutePath(), new File(storageDir, "external").getAbsolutePath()); File dir = dirs[i];
if (dir == null) continue;
String symlinkName = "external-" + i;
Os.symlink(dir.getAbsolutePath(), new File(storageDir, symlinkName).getAbsolutePath());
}
} }
} catch (Exception e) { } catch (Exception e) {
Log.e(LOG_TAG, "Error setting up link", e); Log.e(LOG_TAG, "Error setting up link", e);

View File

@@ -0,0 +1,276 @@
package com.termux.app;
import android.content.Context;
import android.media.AudioManager;
import android.support.v4.widget.DrawerLayout;
import android.view.Gravity;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.inputmethod.InputMethodManager;
import com.termux.terminal.KeyHandler;
import com.termux.terminal.TerminalEmulator;
import com.termux.terminal.TerminalSession;
import com.termux.view.TerminalKeyListener;
import java.util.List;
public final class TermuxKeyListener implements TerminalKeyListener {
final TermuxActivity mActivity;
/** Keeping track of the special keys acting as Ctrl and Fn for the soft keyboard and other hardware keys. */
boolean mVirtualControlKeyDown, mVirtualFnKeyDown;
public TermuxKeyListener(TermuxActivity activity) {
this.mActivity = activity;
}
@Override
public float onScale(float scale) {
if (scale < 0.9f || scale > 1.1f) {
boolean increase = scale > 1.f;
mActivity.changeFontSize(increase);
return 1.0f;
}
return scale;
}
@Override
public void onSingleTapUp(MotionEvent e) {
InputMethodManager mgr = (InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE);
mgr.showSoftInput(mActivity.mTerminalView, InputMethodManager.SHOW_IMPLICIT);
}
@Override
public boolean shouldBackButtonBeMappedToEscape() {
return mActivity.mSettings.mBackIsEscape;
}
@Override
public void copyModeChanged(boolean copyMode) {
// Disable drawer while copying.
mActivity.getDrawer().setDrawerLockMode(copyMode ? DrawerLayout.LOCK_MODE_LOCKED_CLOSED : DrawerLayout.LOCK_MODE_UNLOCKED);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent e, TerminalSession currentSession) {
if (handleVirtualKeys(keyCode, e, true)) return true;
if (keyCode == KeyEvent.KEYCODE_ENTER && !currentSession.isRunning()) {
mActivity.removeFinishedSession(currentSession);
return true;
} else if (e.isCtrlPressed() && e.isAltPressed()) {
// Get the unmodified code point:
int unicodeChar = e.getUnicodeChar(0);
if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN || unicodeChar == 'n'/* next */) {
mActivity.switchToSession(true);
} else if (keyCode == KeyEvent.KEYCODE_DPAD_UP || unicodeChar == 'p' /* previous */) {
mActivity.switchToSession(false);
} else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
mActivity.getDrawer().openDrawer(Gravity.LEFT);
} else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
mActivity.getDrawer().closeDrawers();
} else if (unicodeChar == 'f'/* full screen */) {
mActivity.toggleImmersive();
} else if (unicodeChar == 'k'/* keyboard */) {
InputMethodManager imm = (InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
} else if (unicodeChar == 'm'/* menu */) {
mActivity.mTerminalView.showContextMenu();
} else if (unicodeChar == 'r'/* rename */) {
mActivity.renameSession(currentSession);
} else if (unicodeChar == 'c'/* create */) {
mActivity.addNewSession(false, null);
} else if (unicodeChar == 'u' /* urls */) {
mActivity.showUrlSelection();
} else if (unicodeChar == 'v') {
mActivity.doPaste();
} else if (unicodeChar == '+' || e.getUnicodeChar(KeyEvent.META_SHIFT_ON) == '+') {
// We also check for the shifted char here since shift may be required to produce '+',
// see https://github.com/termux/termux-api/issues/2
mActivity.changeFontSize(true);
} else if (unicodeChar == '-') {
mActivity.changeFontSize(false);
} else if (unicodeChar >= '1' && unicodeChar <= '9') {
int num = unicodeChar - '1';
TermuxService service = mActivity.mTermService;
if (service.getSessions().size() > num)
mActivity.switchToSession(service.getSessions().get(num));
}
return true;
}
return false;
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent e) {
return handleVirtualKeys(keyCode, e, false);
}
@Override
public boolean readControlKey() {
return (mActivity.mExtraKeysView != null && mActivity.mExtraKeysView.readControlButton()) || mVirtualControlKeyDown;
}
@Override
public boolean readAltKey() {
return (mActivity.mExtraKeysView != null && mActivity.mExtraKeysView.readAltButton());
}
@Override
public boolean onCodePoint(final int codePoint, boolean ctrlDown, TerminalSession session) {
if (mVirtualFnKeyDown) {
int resultingKeyCode = -1;
int resultingCodePoint = -1;
boolean altDown = false;
int lowerCase = Character.toLowerCase(codePoint);
switch (lowerCase) {
// Arrow keys.
case 'w':
resultingKeyCode = KeyEvent.KEYCODE_DPAD_UP;
break;
case 'a':
resultingKeyCode = KeyEvent.KEYCODE_DPAD_LEFT;
break;
case 's':
resultingKeyCode = KeyEvent.KEYCODE_DPAD_DOWN;
break;
case 'd':
resultingKeyCode = KeyEvent.KEYCODE_DPAD_RIGHT;
break;
// Page up and down.
case 'p':
resultingKeyCode = KeyEvent.KEYCODE_PAGE_UP;
break;
case 'n':
resultingKeyCode = KeyEvent.KEYCODE_PAGE_DOWN;
break;
// Some special keys:
case 't':
resultingKeyCode = KeyEvent.KEYCODE_TAB;
break;
case 'i':
resultingKeyCode = KeyEvent.KEYCODE_INSERT;
break;
case 'h':
resultingCodePoint = '~';
break;
// Special characters to input.
case 'u':
resultingCodePoint = '_';
break;
case 'l':
resultingCodePoint = '|';
break;
// Function keys.
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
resultingKeyCode = (codePoint - '1') + KeyEvent.KEYCODE_F1;
break;
case '0':
resultingKeyCode = KeyEvent.KEYCODE_F10;
break;
// Other special keys.
case 'e':
resultingCodePoint = /*Escape*/ 27;
break;
case '.':
resultingCodePoint = /*^.*/ 28;
break;
case 'b': // alt+b, jumping backward in readline.
case 'f': // alf+f, jumping forward in readline.
case 'x': // alt+x, common in emacs.
resultingCodePoint = lowerCase;
altDown = true;
break;
// Volume control.
case 'v':
resultingCodePoint = -1;
AudioManager audio = (AudioManager) mActivity.getSystemService(Context.AUDIO_SERVICE);
audio.adjustSuggestedStreamVolume(AudioManager.ADJUST_SAME, AudioManager.USE_DEFAULT_STREAM_TYPE, AudioManager.FLAG_SHOW_UI);
break;
// Writing mode:
case 'q':
mActivity.toggleShowExtraKeys();
break;
}
if (resultingKeyCode != -1) {
TerminalEmulator term = session.getEmulator();
session.write(KeyHandler.getCode(resultingKeyCode, 0, term.isCursorKeysApplicationMode(), term.isKeypadApplicationMode()));
} else if (resultingCodePoint != -1) {
session.writeCodePoint(altDown, resultingCodePoint);
}
return true;
} else if (ctrlDown) {
if (codePoint == 106 /* Ctrl+j or \n */ && !session.isRunning()) {
mActivity.removeFinishedSession(session);
return true;
}
List<TermuxPreferences.KeyboardShortcut> shortcuts = mActivity.mSettings.shortcuts;
if (!shortcuts.isEmpty()) {
int codePointLowerCase = Character.toLowerCase(codePoint);
for (int i = shortcuts.size() - 1; i >= 0; i--) {
TermuxPreferences.KeyboardShortcut shortcut = shortcuts.get(i);
if (codePointLowerCase == shortcut.codePoint) {
switch (shortcut.shortcutAction) {
case TermuxPreferences.SHORTCUT_ACTION_CREATE_SESSION:
mActivity.addNewSession(false, null);
return true;
case TermuxPreferences.SHORTCUT_ACTION_PREVIOUS_SESSION:
mActivity.switchToSession(false);
return true;
case TermuxPreferences.SHORTCUT_ACTION_NEXT_SESSION:
mActivity.switchToSession(true);
return true;
case TermuxPreferences.SHORTCUT_ACTION_RENAME_SESSION:
mActivity.renameSession(mActivity.getCurrentTermSession());
return true;
}
}
}
}
}
return false;
}
/** Handle dedicated volume buttons as virtual keys if applicable. */
private boolean handleVirtualKeys(int keyCode, KeyEvent event, boolean down) {
InputDevice inputDevice = event.getDevice();
if (inputDevice != null && inputDevice.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC) {
// Do not steal dedicated buttons from a full external keyboard.
return false;
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
mVirtualControlKeyDown = down;
return true;
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
mVirtualFnKeyDown = down;
return true;
}
return false;
}
}

View File

@@ -14,13 +14,16 @@ import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties; import java.util.Properties;
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 {} public @interface AsciiBellBehaviour {
}
static final int BELL_VIBRATE = 1; static final int BELL_VIBRATE = 1;
static final int BELL_BEEP = 2; static final int BELL_BEEP = 2;
@@ -30,6 +33,7 @@ final class TermuxPreferences {
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 FULLSCREEN_KEY = "fullscreen";
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 SHOW_WELCOME_DIALOG_KEY = "intro_dialog"; private static final String SHOW_WELCOME_DIALOG_KEY = "intro_dialog";
@@ -40,7 +44,8 @@ final class TermuxPreferences {
@AsciiBellBehaviour @AsciiBellBehaviour
int mBellBehaviour = BELL_VIBRATE; int mBellBehaviour = BELL_VIBRATE;
boolean mBackIsEscape = true; boolean mBackIsEscape;
boolean mShowExtraKeys;
TermuxPreferences(Context context) { TermuxPreferences(Context context) {
reloadFromProperties(context); reloadFromProperties(context);
@@ -53,6 +58,7 @@ final class TermuxPreferences {
MIN_FONTSIZE = (int) (4f * dipInPixels); MIN_FONTSIZE = (int) (4f * dipInPixels);
mFullScreen = prefs.getBoolean(FULLSCREEN_KEY, false); mFullScreen = prefs.getBoolean(FULLSCREEN_KEY, false);
mShowExtraKeys = prefs.getBoolean(SHOW_EXTRA_KEYS_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);
@@ -73,8 +79,17 @@ final class TermuxPreferences {
void setFullScreen(Context context, boolean newValue) { void setFullScreen(Context context, boolean newValue) {
mFullScreen = newValue; mFullScreen = newValue;
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(FULLSCREEN_KEY, newValue).apply();
prefs.edit().putBoolean(FULLSCREEN_KEY, newValue).apply(); }
boolean isShowExtraKeys() {
return mShowExtraKeys;
}
boolean toggleShowExtraKeys(Context context) {
mShowExtraKeys = !mShowExtraKeys;
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(SHOW_EXTRA_KEYS_KEY, mShowExtraKeys).apply();
return mShowExtraKeys;
} }
int getFontSize() { int getFontSize() {
@@ -90,7 +105,7 @@ final class TermuxPreferences {
} }
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).commit(); PreferenceManager.getDefaultSharedPreferences(context).edit().putString(TermuxPreferences.CURRENT_SESSION_KEY, session.mHandle).apply();
} }
static TerminalSession getCurrentSession(TermuxActivity context) { static TerminalSession getCurrentSession(TermuxActivity context) {
@@ -112,7 +127,10 @@ final class TermuxPreferences {
public void reloadFromProperties(Context context) { public void reloadFromProperties(Context context) {
try { try {
File propsFile = new File(TermuxService.HOME_PATH + "/.config/termux/termux.properties"); 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(); 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)) {
@@ -133,10 +151,57 @@ final class TermuxPreferences {
} }
mBackIsEscape = "escape".equals(props.getProperty("back-key", "back")); 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) { } catch (Exception e) {
Toast.makeText(context, "Error loading properties: " + e.getMessage(), Toast.LENGTH_LONG).show(); 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;
public static final int SHORTCUT_ACTION_NEXT_SESSION = 2;
public static final int SHORTCUT_ACTION_PREVIOUS_SESSION = 3;
public static final int SHORTCUT_ACTION_RENAME_SESSION = 4;
public final static class KeyboardShortcut {
public KeyboardShortcut(int codePoint, int shortcutAction) {
this.codePoint = codePoint;
this.shortcutAction = shortcutAction;
}
final int codePoint;
final int shortcutAction;
}
final List<KeyboardShortcut> shortcuts = new ArrayList<>();
private void parseAction(String name, int shortcutAction, Properties props) {
String value = props.getProperty(name);
if (value == null) return;
String[] parts = value.toLowerCase().trim().split("\\+");
String input = parts.length == 2 ? parts[1].trim() : null;
if (!(parts.length == 2 && parts[0].trim().equals("ctrl")) || input.isEmpty() || input.length() > 2) {
Log.e("termux", "Keyboard shortcut '" + name + "' is not Ctrl+<something>");
return;
}
char c = input.charAt(0);
int codePoint = c;
if (Character.isLowSurrogate(c)) {
if (input.length() != 2 || Character.isHighSurrogate(input.charAt(1))) {
Log.e("termux", "Keyboard shortcut '" + name + "' is not Ctrl+<something>");
return;
} else {
codePoint = Character.toCodePoint(input.charAt(1), c);
}
}
shortcuts.add(new KeyboardShortcut(codePoint, shortcutAction));
}
} }

View File

@@ -1,15 +1,5 @@
package com.termux.app; package com.termux.app;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import com.termux.R;
import com.termux.terminal.EmulatorDebug;
import com.termux.terminal.TerminalSession;
import com.termux.terminal.TerminalSession.SessionChangedCallback;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Notification; import android.app.Notification;
import android.app.NotificationManager; import android.app.NotificationManager;
@@ -21,20 +11,31 @@ 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.Handler;
import android.os.IBinder; import android.os.IBinder;
import android.os.PowerManager; import android.os.PowerManager;
import android.util.Log; import android.util.Log;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import com.termux.R;
import com.termux.terminal.EmulatorDebug;
import com.termux.terminal.TerminalSession;
import com.termux.terminal.TerminalSession.SessionChangedCallback;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/** /**
* A service holding a list of terminal sessions, {@link #mTerminalSessions}, showing a foreground notification while * A service holding a list of terminal sessions, {@link #mTerminalSessions}, showing a foreground notification while
* running so that it is not terminated. The user interacts with the session through {@link TermuxActivity}, but this * running so that it is not terminated. The user interacts with the session through {@link TermuxActivity}, but this
* service may outlive the activity when the user or the system disposes of the activity. In that case the user may * service may outlive the activity when the user or the system disposes of the activity. In that case the user may
* restart {@link TermuxActivity} later to yet again access the sessions. * restart {@link TermuxActivity} later to yet again access the sessions.
* * <p/>
* In order to keep both terminal sessions and spawned processes (who may outlive the terminal sessions) alive as long * In order to keep both terminal sessions and spawned processes (who may outlive the terminal sessions) alive as long
* as wanted by the user this service is a foreground service, {@link Service#startForeground(int, Notification)}. * as wanted by the user this service is a foreground service, {@link Service#startForeground(int, Notification)}.
* * <p/>
* Optionally may hold a wake and a wifi lock, in which case that is shown in the notification - see * Optionally may hold a wake and a wifi lock, in which case that is shown in the notification - see
* {@link #buildNotification()}. * {@link #buildNotification()}.
*/ */
@@ -48,18 +49,16 @@ public final class TermuxService extends Service implements SessionChangedCallba
private static final int NOTIFICATION_ID = 1337; private static final int NOTIFICATION_ID = 1337;
/** Intent action to stop the service. */
private static final String ACTION_STOP_SERVICE = "com.termux.service_stop"; private static final String ACTION_STOP_SERVICE = "com.termux.service_stop";
/** Intent action to toggle the wake lock, {@link #mWakeLock}, which this service may hold. */ private static final String ACTION_LOCK_WAKE = "com.termux.service_wake_lock";
private static final String ACTION_LOCK_WAKE = "com.termux.service_toggle_wake_lock"; private static final String ACTION_UNLOCK_WAKE = "com.termux.service_wake_unlock";
/** Intent action to toggle the wifi lock, {@link #mWifiLock}, which this service may hold. */
private static final String ACTION_LOCK_WIFI = "com.termux.service_toggle_wifi_lock";
/** Intent action to launch a new terminal session. Executed from TermuxWidgetProvider. */ /** Intent action to launch a new terminal session. Executed from TermuxWidgetProvider. */
public static final String ACTION_EXECUTE = "com.termux.service_execute"; public static final String ACTION_EXECUTE = "com.termux.service_execute";
public static final String EXTRA_ARGUMENTS = "com.termux.execute.arguments"; public static final String EXTRA_ARGUMENTS = "com.termux.execute.arguments";
public static final String EXTRA_CURRENT_WORKING_DIRECTORY = "com.termux.execute.cwd"; public static final String EXTRA_CURRENT_WORKING_DIRECTORY = "com.termux.execute.cwd";
private static final String EXTRA_EXECUTE_IN_BACKGROUND = "com.termux.execute.background";
/** This service is only bound from inside the same process and never uses IPC. */ /** This service is only bound from inside the same process and never uses IPC. */
class LocalBinder extends Binder { class LocalBinder extends Binder {
@@ -68,17 +67,22 @@ public final class TermuxService extends Service implements SessionChangedCallba
private final IBinder mBinder = new LocalBinder(); private final IBinder mBinder = new LocalBinder();
private final Handler mHandler = new Handler();
/** /**
* The terminal sessions which this service manages. * The terminal sessions which this service manages.
* * <p/>
* Note that this list is observed by {@link TermuxActivity#mListViewAdapter}, so any changes must be made on the UI * Note that this list is observed by {@link TermuxActivity#mListViewAdapter}, so any changes must be made on the UI
* thread and followed by a call to {@link ArrayAdapter#notifyDataSetChanged()} }. * thread and followed by a call to {@link ArrayAdapter#notifyDataSetChanged()} }.
*/ */
final List<TerminalSession> mTerminalSessions = new ArrayList<>(); final List<TerminalSession> mTerminalSessions = new ArrayList<>();
final List<BackgroundJob> mBackgroundTasks = new ArrayList<>();
/** Note that the service may often outlive the activity, so need to clear this reference. */ /** Note that the service may often outlive the activity, so need to clear this reference. */
SessionChangedCallback mSessionChangeCallback; SessionChangedCallback mSessionChangeCallback;
/** The wake lock and wifi lock are always acquired and released together. */
private PowerManager.WakeLock mWakeLock; private PowerManager.WakeLock mWakeLock;
private WifiManager.WifiLock mWifiLock; private WifiManager.WifiLock mWifiLock;
@@ -99,26 +103,35 @@ public final class TermuxService extends Service implements SessionChangedCallba
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, EmulatorDebug.LOG_TAG); mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, EmulatorDebug.LOG_TAG);
mWakeLock.acquire(); mWakeLock.acquire();
} else {
mWakeLock.release();
mWakeLock = null;
}
updateNotification();
} else if (ACTION_LOCK_WIFI.equals(action)) {
if (mWifiLock == null) {
WifiManager wm = (WifiManager) getSystemService(Context.WIFI_SERVICE); WifiManager wm = (WifiManager) 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();
} else {
updateNotification();
}
} else if (ACTION_UNLOCK_WAKE.equals(action)) {
if (mWakeLock != null) {
mWakeLock.release();
mWakeLock = null;
mWifiLock.release(); mWifiLock.release();
mWifiLock = null; mWifiLock = null;
}
updateNotification(); updateNotification();
}
} else if (ACTION_EXECUTE.equals(action)) { } else if (ACTION_EXECUTE.equals(action)) {
Uri executableUri = intent.getData(); Uri executableUri = intent.getData();
String executablePath = (executableUri == null ? null : executableUri.getPath()); String executablePath = (executableUri == null ? null : executableUri.getPath());
String[] arguments = (executableUri == null ? null : intent.getStringArrayExtra(EXTRA_ARGUMENTS)); String[] arguments = (executableUri == null ? null : intent.getStringArrayExtra(EXTRA_ARGUMENTS));
String cwd = intent.getStringExtra(EXTRA_CURRENT_WORKING_DIRECTORY); String cwd = intent.getStringExtra(EXTRA_CURRENT_WORKING_DIRECTORY);
if (intent.getBooleanExtra(EXTRA_EXECUTE_IN_BACKGROUND, false)) {
BackgroundJob task = new BackgroundJob(cwd, executablePath, arguments, this);
mBackgroundTasks.add(task);
updateNotification();
} else {
TerminalSession newSession = createTermSession(executablePath, arguments, cwd, false); TerminalSession newSession = createTermSession(executablePath, arguments, cwd, false);
// 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".
@@ -132,8 +145,9 @@ public final class TermuxService extends Service implements SessionChangedCallba
// Make the newly created session the current one to be displayed: // Make the newly created session the current one to be displayed:
TermuxPreferences.storeCurrentSession(this, newSession); TermuxPreferences.storeCurrentSession(this, newSession);
// Launch the main Termux app, which will now show to current session: // Launch the main Termux app, which will now show the current session:
startActivity(new Intent(this, TermuxActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); startActivity(new Intent(this, TermuxActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
}
} else if (action != null) { } else if (action != null) {
Log.e(EmulatorDebug.LOG_TAG, "Unknown TermuxService action: '" + action + "'"); Log.e(EmulatorDebug.LOG_TAG, "Unknown TermuxService action: '" + action + "'");
} }
@@ -155,8 +169,8 @@ public final class TermuxService extends Service implements SessionChangedCallba
/** 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() { private void updateNotification() {
if (mWakeLock == null && mWifiLock == null && getSessions().isEmpty()) { if (mWakeLock == null && mTerminalSessions.isEmpty() && mBackgroundTasks.isEmpty()) {
// Exit if we are updating after the user disabled all locks with no sessions. // Exit if we are updating after the user disabled all locks with no sessions or tasks running.
stopSelf(); stopSelf();
} else { } else {
((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).notify(NOTIFICATION_ID, buildNotification()); ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).notify(NOTIFICATION_ID, buildNotification());
@@ -171,18 +185,15 @@ public final class TermuxService extends Service implements SessionChangedCallba
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notifyIntent, 0); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notifyIntent, 0);
int sessionCount = mTerminalSessions.size(); int sessionCount = mTerminalSessions.size();
String contentText = sessionCount + " terminal session" + (sessionCount == 1 ? "" : "s"); int taskCount = mBackgroundTasks.size();
String contentText = sessionCount + " session" + (sessionCount == 1 ? "" : "s");
boolean wakeLockHeld = mWakeLock != null; if (taskCount > 0) {
boolean wifiLockHeld = mWifiLock != null; contentText += ", " + taskCount + " task" + (taskCount == 1 ? "" : "s");
if (wakeLockHeld && wifiLockHeld) {
contentText += " (wake&wifi lock held)";
} else if (wakeLockHeld) {
contentText += " (wake lock held)";
} else if (wifiLockHeld) {
contentText += " (wifi lock held)";
} }
final boolean wakeLockHeld = mWakeLock != null;
if (wakeLockHeld) contentText += " (wake lock held)";
Notification.Builder builder = new Notification.Builder(this); Notification.Builder builder = new Notification.Builder(this);
builder.setContentTitle(getText(R.string.application_name)); builder.setContentTitle(getText(R.string.application_name));
builder.setContentText(contentText); builder.setContentText(contentText);
@@ -192,7 +203,7 @@ public final class TermuxService extends Service implements SessionChangedCallba
// 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 minimal priority since this is just a background service notification:
builder.setPriority((wakeLockHeld || wifiLockHeld) ? Notification.PRIORITY_HIGH : Notification.PRIORITY_MIN); builder.setPriority((wakeLockHeld) ? Notification.PRIORITY_HIGH : Notification.PRIORITY_MIN);
// No need to show a timestamp: // No need to show a timestamp:
builder.setShowWhen(false); builder.setShowWhen(false);
@@ -204,13 +215,13 @@ public final class TermuxService extends Service implements SessionChangedCallba
Intent exitIntent = new Intent(this, TermuxService.class).setAction(ACTION_STOP_SERVICE); Intent exitIntent = new Intent(this, TermuxService.class).setAction(ACTION_STOP_SERVICE);
builder.addAction(android.R.drawable.ic_delete, res.getString(R.string.notification_action_exit), PendingIntent.getService(this, 0, exitIntent, 0)); builder.addAction(android.R.drawable.ic_delete, res.getString(R.string.notification_action_exit), PendingIntent.getService(this, 0, exitIntent, 0));
Intent toggleWakeLockIntent = new Intent(this, TermuxService.class).setAction(ACTION_LOCK_WAKE); String newWakeAction = wakeLockHeld ? ACTION_UNLOCK_WAKE : ACTION_LOCK_WAKE;
builder.addAction(android.R.drawable.ic_lock_lock, res.getString(R.string.notification_action_wakelock), Intent toggleWakeLockIntent = new Intent(this, TermuxService.class).setAction(newWakeAction);
PendingIntent.getService(this, 0, toggleWakeLockIntent, 0)); String actionTitle = res.getString(wakeLockHeld ?
R.string.notification_action_wake_unlock :
Intent toggleWifiLockIntent = new Intent(this, TermuxService.class).setAction(ACTION_LOCK_WIFI); R.string.notification_action_wake_lock);
builder.addAction(android.R.drawable.ic_lock_lock, res.getString(R.string.notification_action_wifilock), int actionIcon = wakeLockHeld ? android.R.drawable.ic_lock_idle_lock : android.R.drawable.ic_lock_lock;
PendingIntent.getService(this, 0, toggleWifiLockIntent, 0)); builder.addAction(actionIcon, actionTitle, PendingIntent.getService(this, 0, toggleWakeLockIntent, 0));
return builder.build(); return builder.build();
} }
@@ -236,30 +247,9 @@ public final class TermuxService extends Service implements SessionChangedCallba
if (cwd == null) cwd = HOME_PATH; if (cwd == null) cwd = HOME_PATH;
final String termEnv = "TERM=xterm-256color"; String[] env = BackgroundJob.buildEnvironment(failSafe, cwd);
final String homeEnv = "HOME=" + HOME_PATH; boolean isLoginShell = false;
final String prefixEnv = "PREFIX=" + PREFIX_PATH;
final String androidRootEnv = "ANDROID_ROOT=" + System.getenv("ANDROID_ROOT");
final String androidDataEnv = "ANDROID_DATA=" + System.getenv("ANDROID_DATA");
// EXTERNAL_STORAGE is needed for /system/bin/am to work on at least
// Samsung S7 - see https://plus.google.com/110070148244138185604/posts/gp8Lk3aCGp3.
final String externalStorageEnv = "EXTERNAL_STORAGE=" + System.getenv("EXTERNAL_STORAGE");
String[] env;
if (failSafe) {
// Keep the default path so that system binaries can be used in the failsafe session.
final String pathEnv = "PATH=" + System.getenv("PATH");
env = new String[] { termEnv, homeEnv, prefixEnv, androidRootEnv, androidDataEnv, pathEnv, externalStorageEnv };
} else {
final String ps1Env = "PS1=$ ";
final String ldEnv = "LD_LIBRARY_PATH=" + PREFIX_PATH + "/lib";
final String langEnv = "LANG=en_US.UTF-8";
final String pathEnv = "PATH=" + PREFIX_PATH + "/bin:" + PREFIX_PATH + "/bin/applets";
final String pwdEnv = "PWD=" + cwd;
env = new String[] { termEnv, homeEnv, prefixEnv, ps1Env, ldEnv, langEnv, pathEnv, pwdEnv, androidRootEnv, androidDataEnv, externalStorageEnv };
}
String shellName;
if (executablePath == null) { if (executablePath == null) {
File shell = new File(HOME_PATH, ".termux/shell"); File shell = new File(HOME_PATH, ".termux/shell");
if (shell.exists()) { if (shell.exists()) {
@@ -290,23 +280,18 @@ public final class TermuxService extends Service implements SessionChangedCallba
// Fall back to system shell as last resort: // Fall back to system shell as last resort:
executablePath = "/system/bin/sh"; executablePath = "/system/bin/sh";
} }
isLoginShell = true;
}
String[] parts = executablePath.split("/"); String[] processArgs = BackgroundJob.setupProcessArgs(executablePath, arguments);
shellName = "-" + parts[parts.length - 1]; executablePath = processArgs[0];
} else {
int lastSlashIndex = executablePath.lastIndexOf('/'); int lastSlashIndex = executablePath.lastIndexOf('/');
shellName = lastSlashIndex == -1 ? executablePath : executablePath.substring(lastSlashIndex + 1); String processName = (isLoginShell ? "-" : "") +
} (lastSlashIndex == -1 ? executablePath : executablePath.substring(lastSlashIndex + 1));
String[] args; String[] args = new String[processArgs.length];
if (arguments == null) { args[0] = processName;
args = new String[] { shellName }; if (processArgs.length > 1) System.arraycopy(processArgs, 1, args, 1, processArgs.length - 1);
} else {
args = new String[arguments.length + 1];
args[0] = shellName;
System.arraycopy(arguments, 0, args, 1, arguments.length);
}
TerminalSession session = new TerminalSession(executablePath, cwd, args, env, this); TerminalSession session = new TerminalSession(executablePath, cwd, args, env, this);
mTerminalSessions.add(session); mTerminalSessions.add(session);
@@ -334,7 +319,8 @@ public final class TermuxService extends Service implements SessionChangedCallba
@Override @Override
public void onSessionFinished(final TerminalSession finishedSession) { public void onSessionFinished(final TerminalSession finishedSession) {
if (mSessionChangeCallback != null) mSessionChangeCallback.onSessionFinished(finishedSession); if (mSessionChangeCallback != null)
mSessionChangeCallback.onSessionFinished(finishedSession);
} }
@Override @Override
@@ -352,4 +338,18 @@ public final class TermuxService extends Service implements SessionChangedCallba
if (mSessionChangeCallback != null) mSessionChangeCallback.onBell(session); if (mSessionChangeCallback != null) mSessionChangeCallback.onBell(session);
} }
@Override
public void onColorsChanged(TerminalSession session) {
if (mSessionChangeCallback != null) mSessionChangeCallback.onColorsChanged(session);
}
public void onBackgroundJobExited(final BackgroundJob task) {
mHandler.post(new Runnable() {
@Override
public void run() {
mBackgroundTasks.remove(task);
updateNotification();
}
});
}
} }

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -1,106 +0,0 @@
package com.termux.filepicker;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.ListActivity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.MenuItem;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import com.termux.R;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/** Activity allowing picking files from the $HOME folder. */
public class TermuxFilePickerActivity extends ListActivity {
@SuppressLint("SdCardPath")
final String TERMUX_HOME = "/data/data/com.termux/files/home";
private File mCurrentDirectory;
private final List<File> mFiles = new ArrayList<>();
private final List<String> mFileNames = new ArrayList<>();
private ArrayAdapter mAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.file_picker);
mAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, mFileNames);
enterDirectory(new File(TERMUX_HOME));
setListAdapter(mAdapter);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
enterDirectory(mCurrentDirectory.getParentFile());
return true;
} else {
return super.onOptionsItemSelected(item);
}
}
@Override
protected void onListItemClick(ListView l, View v, int position, long id) {
super.onListItemClick(l, v, position, id);
File requestFile = mFiles.get(position);
if (requestFile.isDirectory()) {
enterDirectory(requestFile);
} else {
Uri returnUri = Uri.withAppendedPath(Uri.parse("content://com.termux.filepicker.provider/"), requestFile.getAbsolutePath());
Intent returnIntent = new Intent().setData(returnUri);
returnIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
setResult(Activity.RESULT_OK, returnIntent);
finish();
}
}
void enterDirectory(File directory) {
getActionBar().setDisplayHomeAsUpEnabled(!directory.getAbsolutePath().equals(TERMUX_HOME));
String title = directory.getAbsolutePath() + "/";
if (title.startsWith(TERMUX_HOME)) {
title = "~" + title.substring(TERMUX_HOME.length(), title.length());
}
setTitle(title);
mCurrentDirectory = directory;
mFiles.clear();
mFileNames.clear();
mFiles.addAll(Arrays.asList(mCurrentDirectory.listFiles()));
Collections.sort(mFiles, new Comparator<File>() {
@Override
public int compare(File f1, File f2) {
final String n1 = f1.getName();
final String n2 = f2.getName();
// Display dot folders last:
if (n1.startsWith(".") && !n2.startsWith(".")) {
return 1;
} else if (n2.startsWith(".") && !n1.startsWith(".")) {
return -1;
}
return n1.compareToIgnoreCase(n2);
}
});
for (File file : mFiles) {
mFileNames.add(file.getName() + (file.isDirectory() ? "/" : ""));
}
mAdapter.notifyDataSetChanged();
}
}

View File

@@ -1,65 +0,0 @@
package com.termux.filepicker;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.support.annotation.NonNull;
import android.webkit.MimeTypeMap;
import java.io.File;
import java.io.FileNotFoundException;
/** Provider of files content uris picked from {@link com.termux.filepicker.TermuxFilePickerActivity}. */
public class TermuxFilePickerProvider extends ContentProvider {
@Override
public boolean onCreate() {
return true;
}
@Override
public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
return null;
}
@Override
public String getType(@NonNull Uri uri) {
String contentType = null;
String path = uri.getPath();
int lastDotIndex = path.lastIndexOf('.');
String possibleFileExtension = path.substring(lastDotIndex + 1, path.length());
if (possibleFileExtension.contains("/")) {
// The dot was in the path, so not a file extension.
} else {
MimeTypeMap mimeTypes = MimeTypeMap.getSingleton();
// Lower casing makes it work with e.g. "JPG":
contentType = mimeTypes.getMimeTypeFromExtension(possibleFileExtension.toLowerCase());
}
if (contentType == null) contentType = "application/octet-stream";
return contentType;
}
@Override
public Uri insert(@NonNull Uri uri, ContentValues values) {
return null;
}
@Override
public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {
return 0;
}
@Override
public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) {
return 0;
}
@Override
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
File file = new File(uri.getPath());
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
}
}

View File

@@ -35,7 +35,7 @@ public class TermuxFileReceiverActivity extends Activity {
* name input dialog to be implicitly dismissed, and we do not want to finish the activity directly * name input dialog to be implicitly dismissed, and we do not want to finish the activity directly
* when showing the error dialog. * when showing the error dialog.
*/ */
private boolean mFinishOnDismissNameDialog = true; boolean mFinishOnDismissNameDialog = true;
@Override @Override
protected void onResume() { protected void onResume() {
@@ -119,25 +119,7 @@ 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 DialogUtils.textInput(this, R.string.file_received_title, attachmentFileName, R.string.file_received_edit_button, new DialogUtils.TextSetListener() {
, android.R.string.ok, new DialogUtils.TextSetListener() {
@Override
public void onTextSet(final String text) {
if (saveStreamWithName(in, text) == null) return;
finish();
}
}, R.string.file_received_open_folder_button, new DialogUtils.TextSetListener() {
@Override
public void onTextSet(String text) {
if (saveStreamWithName(in, text) == null) return;
Intent executeIntent = new Intent(TermuxService.ACTION_EXECUTE);
executeIntent.putExtra(TermuxService.EXTRA_CURRENT_WORKING_DIRECTORY, TERMUX_RECEIVEDIR);
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
startService(executeIntent);
finish();
}
}, R.string.file_received_edit_button, new DialogUtils.TextSetListener() {
@Override @Override
public void onTextSet(String text) { public void onTextSet(String text) {
File outFile = saveStreamWithName(in, text); File outFile = saveStreamWithName(in, text);
@@ -151,6 +133,7 @@ public class TermuxFileReceiverActivity extends Activity {
} }
// Do this for the user if necessary: // Do this for the user if necessary:
//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();
@@ -161,6 +144,24 @@ public class TermuxFileReceiverActivity extends Activity {
startService(executeIntent); startService(executeIntent);
finish(); finish();
} }
},
R.string.file_received_open_folder_button, new DialogUtils.TextSetListener() {
@Override
public void onTextSet(String text) {
if (saveStreamWithName(in, text) == null) return;
Intent executeIntent = new Intent(TermuxService.ACTION_EXECUTE);
executeIntent.putExtra(TermuxService.EXTRA_CURRENT_WORKING_DIRECTORY, TERMUX_RECEIVEDIR);
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
startService(executeIntent);
finish();
}
},
android.R.string.cancel, new DialogUtils.TextSetListener() {
@Override
public void onTextSet(final String text) {
finish();
}
}, new DialogInterface.OnDismissListener() { }, new DialogInterface.OnDismissListener() {
@Override @Override
public void onDismiss(DialogInterface dialog) { public void onDismiss(DialogInterface dialog) {
@@ -201,6 +202,7 @@ public class TermuxFileReceiverActivity extends Activity {
} }
// Do this for the user if necessary: // Do this for the user if necessary:
//noinspection ResultOfMethodCallIgnored
urlOpenerProgramFile.setExecutable(true); urlOpenerProgramFile.setExecutable(true);
final Uri urlOpenerProgramUri = new Uri.Builder().scheme("file").path(URL_OPENER_PROGRAM).build(); final Uri urlOpenerProgramUri = new Uri.Builder().scheme("file").path(URL_OPENER_PROGRAM).build();

View File

@@ -53,7 +53,7 @@ final class ByteQueue {
/** /**
* Attempt to write the specified portion of the provided buffer to the queue. * Attempt to write the specified portion of the provided buffer to the queue.
* * <p/>
* Returns whether the output was totally written, false if it was closed before. * Returns whether the output was totally written, false if it was closed before.
*/ */
public boolean write(byte[] buffer, int offset, int lengthToWrite) { public boolean write(byte[] buffer, int offset, int lengthToWrite) {

View File

@@ -12,19 +12,14 @@ final class JNI {
/** /**
* Create a subprocess. Differs from {@link ProcessBuilder} in that a pseudoterminal is used to communicate with the * Create a subprocess. Differs from {@link ProcessBuilder} in that a pseudoterminal is used to communicate with the
* subprocess. * subprocess.
* * <p/>
* Callers are responsible for calling {@link #close(int)} on the returned file descriptor. * Callers are responsible for calling {@link #close(int)} on the returned file descriptor.
* *
* @param cmd * @param cmd The command to execute
* The command to execute * @param cwd The current working directory for the executed command
* @param cwd * @param args An array of arguments to the command
* The current working directory for the executed command * @param envVars An array of strings of the form "VAR=value" to be added to the environment of the process
* @param args * @param processId A one-element array to which the process ID of the started process will be written.
* An array of arguments to the command
* @param envVars
* An array of strings of the form "VAR=value" to be added to the environment of the process
* @param processId
* A one-element array to which the process ID of the started process will be written.
* @return the file descriptor resulting from opening /dev/ptmx master device. The sub process will have opened the * @return the file descriptor resulting from opening /dev/ptmx master device. The sub process will have opened the
* slave device counterpart (/dev/pts/$N) and have it as stdint, stdout and stderr. * slave device counterpart (/dev/pts/$N) and have it as stdint, stdout and stderr.
*/ */
@@ -40,15 +35,6 @@ final class JNI {
*/ */
public static native int waitFor(int processId); public static native int waitFor(int processId);
/**
* Send SIGHUP to a process group.
*
* There exists a kill(2) system call wrapper in {@link android.os.Process#sendSignal(int, int)}, but that makes a
* "if (pid > 0)" check so cannot be used for sending to a process group:
* https://android.googlesource.com/platform/frameworks/base/+/donut-release/core/jni/android_util_Process.cpp
*/
public static native void hangupProcessGroup(int processId);
/** Close a file descriptor through the close(2) system call. */ /** Close a file descriptor through the close(2) system call. */
public static native void close(int fileDescriptor); public static native void close(int fileDescriptor);

View File

@@ -1,5 +1,9 @@
package com.termux.terminal; package com.termux.terminal;
import java.util.HashMap;
import java.util.Map;
import static android.view.KeyEvent.KEYCODE_BACK;
import static android.view.KeyEvent.KEYCODE_BREAK; import static android.view.KeyEvent.KEYCODE_BREAK;
import static android.view.KeyEvent.KEYCODE_DEL; import static android.view.KeyEvent.KEYCODE_DEL;
import static android.view.KeyEvent.KEYCODE_DPAD_CENTER; import static android.view.KeyEvent.KEYCODE_DPAD_CENTER;
@@ -24,6 +28,7 @@ import static android.view.KeyEvent.KEYCODE_F9;
import static android.view.KeyEvent.KEYCODE_FORWARD_DEL; import static android.view.KeyEvent.KEYCODE_FORWARD_DEL;
import static android.view.KeyEvent.KEYCODE_INSERT; import static android.view.KeyEvent.KEYCODE_INSERT;
import static android.view.KeyEvent.KEYCODE_MOVE_END; import static android.view.KeyEvent.KEYCODE_MOVE_END;
import static android.view.KeyEvent.KEYCODE_MOVE_HOME;
import static android.view.KeyEvent.KEYCODE_NUMPAD_0; import static android.view.KeyEvent.KEYCODE_NUMPAD_0;
import static android.view.KeyEvent.KEYCODE_NUMPAD_1; import static android.view.KeyEvent.KEYCODE_NUMPAD_1;
import static android.view.KeyEvent.KEYCODE_NUMPAD_2; import static android.view.KeyEvent.KEYCODE_NUMPAD_2;
@@ -45,14 +50,9 @@ import static android.view.KeyEvent.KEYCODE_NUMPAD_SUBTRACT;
import static android.view.KeyEvent.KEYCODE_NUM_LOCK; import static android.view.KeyEvent.KEYCODE_NUM_LOCK;
import static android.view.KeyEvent.KEYCODE_PAGE_DOWN; import static android.view.KeyEvent.KEYCODE_PAGE_DOWN;
import static android.view.KeyEvent.KEYCODE_PAGE_UP; import static android.view.KeyEvent.KEYCODE_PAGE_UP;
import static android.view.KeyEvent.KEYCODE_SPACE;
import static android.view.KeyEvent.KEYCODE_SYSRQ; import static android.view.KeyEvent.KEYCODE_SYSRQ;
import static android.view.KeyEvent.KEYCODE_TAB; import static android.view.KeyEvent.KEYCODE_TAB;
import static android.view.KeyEvent.KEYCODE_HOME;
import java.util.HashMap;
import java.util.Map;
import android.view.KeyEvent;
public final class KeyHandler { public final class KeyHandler {
@@ -61,11 +61,12 @@ public final class KeyHandler {
public static final int KEYMOD_SHIFT = 0x20000000; public static final int KEYMOD_SHIFT = 0x20000000;
private static final Map<String, Integer> TERMCAP_TO_KEYCODE = new HashMap<>(); private static final Map<String, Integer> TERMCAP_TO_KEYCODE = new HashMap<>();
static { static {
// terminfo: http://pubs.opengroup.org/onlinepubs/7990989799/xcurses/terminfo.html // terminfo: http://pubs.opengroup.org/onlinepubs/7990989799/xcurses/terminfo.html
// termcap: http://man7.org/linux/man-pages/man5/termcap.5.html // termcap: http://man7.org/linux/man-pages/man5/termcap.5.html
TERMCAP_TO_KEYCODE.put("%i", KEYMOD_SHIFT | KEYCODE_DPAD_RIGHT); TERMCAP_TO_KEYCODE.put("%i", KEYMOD_SHIFT | KEYCODE_DPAD_RIGHT);
TERMCAP_TO_KEYCODE.put("#2", KEYMOD_SHIFT | KEYCODE_HOME); // Shifted home TERMCAP_TO_KEYCODE.put("#2", KEYMOD_SHIFT | KEYCODE_MOVE_HOME); // Shifted home
TERMCAP_TO_KEYCODE.put("#4", KEYMOD_SHIFT | KEYCODE_DPAD_LEFT); TERMCAP_TO_KEYCODE.put("#4", KEYMOD_SHIFT | KEYCODE_DPAD_LEFT);
TERMCAP_TO_KEYCODE.put("*7", KEYMOD_SHIFT | KEYCODE_MOVE_END); // Shifted end key TERMCAP_TO_KEYCODE.put("*7", KEYMOD_SHIFT | KEYCODE_MOVE_END); // Shifted end key
@@ -97,7 +98,7 @@ public final class KeyHandler {
TERMCAP_TO_KEYCODE.put("kb", KEYCODE_DEL); // backspace key TERMCAP_TO_KEYCODE.put("kb", KEYCODE_DEL); // backspace key
TERMCAP_TO_KEYCODE.put("kd", KEYCODE_DPAD_DOWN); // terminfo=kcud1, down-arrow key TERMCAP_TO_KEYCODE.put("kd", KEYCODE_DPAD_DOWN); // terminfo=kcud1, down-arrow key
TERMCAP_TO_KEYCODE.put("kh", KeyEvent.KEYCODE_HOME); TERMCAP_TO_KEYCODE.put("kh", KEYCODE_MOVE_HOME);
TERMCAP_TO_KEYCODE.put("kl", KEYCODE_DPAD_LEFT); TERMCAP_TO_KEYCODE.put("kl", KEYCODE_DPAD_LEFT);
TERMCAP_TO_KEYCODE.put("kr", KEYCODE_DPAD_RIGHT); TERMCAP_TO_KEYCODE.put("kr", KEYCODE_DPAD_RIGHT);
@@ -106,10 +107,10 @@ public final class KeyHandler {
// t_K3 <kPageUp> keypad page-up key // t_K3 <kPageUp> keypad page-up key
// t_K4 <kEnd> keypad end key // t_K4 <kEnd> keypad end key
// t_K5 <kPageDown> keypad page-down key // t_K5 <kPageDown> keypad page-down key
TERMCAP_TO_KEYCODE.put("K1", KeyEvent.KEYCODE_HOME); TERMCAP_TO_KEYCODE.put("K1", KEYCODE_MOVE_HOME);
TERMCAP_TO_KEYCODE.put("K3", KeyEvent.KEYCODE_PAGE_UP); TERMCAP_TO_KEYCODE.put("K3", KEYCODE_PAGE_UP);
TERMCAP_TO_KEYCODE.put("K4", KeyEvent.KEYCODE_MOVE_END); TERMCAP_TO_KEYCODE.put("K4", KEYCODE_MOVE_END);
TERMCAP_TO_KEYCODE.put("K5", KeyEvent.KEYCODE_PAGE_DOWN); TERMCAP_TO_KEYCODE.put("K5", KEYCODE_PAGE_DOWN);
TERMCAP_TO_KEYCODE.put("ku", KEYCODE_DPAD_UP); TERMCAP_TO_KEYCODE.put("ku", KEYCODE_DPAD_UP);
@@ -161,7 +162,9 @@ public final class KeyHandler {
case KEYCODE_DPAD_LEFT: case KEYCODE_DPAD_LEFT:
return (keyMode == 0) ? (cursorApp ? "\033OD" : "\033[D") : transformForModifiers("\033[1", keyMode, 'D'); return (keyMode == 0) ? (cursorApp ? "\033OD" : "\033[D") : transformForModifiers("\033[1", keyMode, 'D');
case KeyEvent.KEYCODE_HOME: case KEYCODE_MOVE_HOME:
// Note that KEYCODE_HOME is handled by the system and never delivered to applications.
// On a Logitech k810 keyboard KEYCODE_MOVE_HOME is sent by FN+LeftArrow.
return (keyMode == 0) ? (cursorApp ? "\033OH" : "\033[H") : transformForModifiers("\033[1", keyMode, 'H'); return (keyMode == 0) ? (cursorApp ? "\033OH" : "\033[H") : transformForModifiers("\033[1", keyMode, 'H');
case KEYCODE_MOVE_END: case KEYCODE_MOVE_END:
return (keyMode == 0) ? (cursorApp ? "\033OF" : "\033[F") : transformForModifiers("\033[1", keyMode, 'F'); return (keyMode == 0) ? (cursorApp ? "\033OF" : "\033[F") : transformForModifiers("\033[1", keyMode, 'F');
@@ -208,7 +211,7 @@ public final class KeyHandler {
return "\033[34~"; // Pause/Break return "\033[34~"; // Pause/Break
case KEYCODE_ESCAPE: case KEYCODE_ESCAPE:
case KeyEvent.KEYCODE_BACK: case KEYCODE_BACK:
return "\033"; return "\033";
case KEYCODE_INSERT: case KEYCODE_INSERT:
@@ -216,20 +219,18 @@ public final class KeyHandler {
case KEYCODE_FORWARD_DEL: case KEYCODE_FORWARD_DEL:
return transformForModifiers("\033[3", keyMode, '~'); return transformForModifiers("\033[3", keyMode, '~');
case KEYCODE_NUMPAD_DOT:
return keypadApplication ? "\033On" : "\033[3~";
case KEYCODE_PAGE_UP: case KEYCODE_PAGE_UP:
return "\033[5~"; return "\033[5~";
case KEYCODE_PAGE_DOWN: case KEYCODE_PAGE_DOWN:
return "\033[6~"; return "\033[6~";
case KEYCODE_DEL: case KEYCODE_DEL:
// Yes, this needs to U+007F and not U+0008! String prefix = ((keyMode & KEYMOD_ALT) == 0) ? "" : "\033";
return "\u007F"; // Just do what xterm and gnome-terminal does:
return prefix + (((keyMode & KEYMOD_CTRL) == 0) ? "\u007F" : "\u0008");
case KEYCODE_NUM_LOCK: case KEYCODE_NUM_LOCK:
return "\033OP"; return "\033OP";
case KeyEvent.KEYCODE_SPACE: case KEYCODE_SPACE:
// If ctrl is not down, return null so that it goes through normal input processing (which may e.g. cause a // If ctrl is not down, return null so that it goes through normal input processing (which may e.g. cause a
// combining accent to be written): // combining accent to be written):
return ((keyMode & KEYMOD_CTRL) == 0) ? null : "\0"; return ((keyMode & KEYMOD_CTRL) == 0) ? null : "\0";
@@ -247,12 +248,14 @@ public final class KeyHandler {
return keypadApplication ? transformForModifiers("\033O", keyMode, 'k') : "+"; return keypadApplication ? transformForModifiers("\033O", keyMode, 'k') : "+";
case KEYCODE_NUMPAD_COMMA: case KEYCODE_NUMPAD_COMMA:
return ","; return ",";
case KEYCODE_NUMPAD_DOT:
return keypadApplication ? "\033On" : ".";
case KEYCODE_NUMPAD_SUBTRACT: case KEYCODE_NUMPAD_SUBTRACT:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'm') : "-"; return keypadApplication ? transformForModifiers("\033O", keyMode, 'm') : "-";
case KEYCODE_NUMPAD_DIVIDE: case KEYCODE_NUMPAD_DIVIDE:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'o') : "/"; return keypadApplication ? transformForModifiers("\033O", keyMode, 'o') : "/";
case KEYCODE_NUMPAD_0: case KEYCODE_NUMPAD_0:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'p') : "1"; return keypadApplication ? transformForModifiers("\033O", keyMode, 'p') : "0";
case KEYCODE_NUMPAD_1: case KEYCODE_NUMPAD_1:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'q') : "1"; return keypadApplication ? transformForModifiers("\033O", keyMode, 'q') : "1";
case KEYCODE_NUMPAD_2: case KEYCODE_NUMPAD_2:

View File

@@ -3,7 +3,7 @@ package com.termux.terminal;
/** /**
* A circular buffer of {@link TerminalRow}:s which keeps notes about what is visible on a logical screen and the scroll * A circular buffer of {@link TerminalRow}:s which keeps notes about what is visible on a logical screen and the scroll
* history. * history.
* * <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 {
@@ -21,12 +21,9 @@ public final class TerminalBuffer {
/** /**
* Create a transcript screen. * Create a transcript screen.
* *
* @param columns * @param columns the width of the screen in characters.
* the width of the screen in characters. * @param totalRows the height of the entire text area, in rows of text.
* @param totalRows * @param screenRows the height of just the screen, not including the transcript that holds lines that have scrolled off
* the height of the entire text area, in rows of text.
* @param screenRows
* the height of just the screen, not including the transcript that holds lines that have scrolled off
* the top of the screen. * the top of the screen.
*/ */
public TerminalBuffer(int columns, int totalRows, int screenRows) { public TerminalBuffer(int columns, int totalRows, int screenRows) {
@@ -78,7 +75,8 @@ public final class TerminalBuffer {
if (c != ' ') lastPrintingCharIndex = i; if (c != ' ') lastPrintingCharIndex = i;
} }
} }
if (lastPrintingCharIndex != -1) builder.append(line, x1Index, lastPrintingCharIndex - x1Index + 1); if (lastPrintingCharIndex != -1)
builder.append(line, x1Index, lastPrintingCharIndex - x1Index + 1);
if (!rowLineWrap && row < selY2 && row < mScreenRows - 1) builder.append('\n'); if (!rowLineWrap && row < selY2 && row < mScreenRows - 1) builder.append('\n');
} }
return builder.toString(); return builder.toString();
@@ -94,15 +92,15 @@ 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> * <ul>
* <li>External coordinate system: -mActiveTranscriptRows to mScreenRows-1, with the screen being 0..mScreenRows-1. * <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 * <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). * mActiveTranscriptRows lines ending at mScreenFirstRow-1 form the transcript (as a circular buffer).
* </ul> * </ul>
* * <p/>
* External <---> Internal: * External <---> Internal:
* * <p/>
* <pre> * <pre>
* [ ... ] [ ... ] * [ ... ] [ ... ]
* [ -mActiveTranscriptRows ] [ mScreenFirstRow - mActiveTranscriptRows ] * [ -mActiveTranscriptRows ] [ mScreenFirstRow - mActiveTranscriptRows ]
@@ -112,8 +110,7 @@ public final class TerminalBuffer {
* [ mScreenRows-1 ] [ mScreenFirstRow + mScreenRows-1 ] * [ mScreenRows-1 ] [ mScreenFirstRow + mScreenRows-1 ]
* </pre> * </pre>
* *
* @param externalRow * @param externalRow a row in the external coordinate system.
* a row in the external coordinate system.
* @return The row corresponding to the input argument in the private coordinate system. * @return The row corresponding to the input argument in the private coordinate system.
*/ */
public int externalToInternalRow(int externalRow) { public int externalToInternalRow(int externalRow) {
@@ -139,14 +136,11 @@ public final class TerminalBuffer {
* Resize the screen which this transcript backs. Currently, this only works if the number of columns does not * Resize the screen which this transcript backs. Currently, this only works if the number of columns does not
* change or the rows expand (that is, it only works when shrinking the number of rows). * change or the rows expand (that is, it only works when shrinking the number of rows).
* *
* @param newColumns * @param newColumns The number of columns the screen should have.
* The number of columns the screen should have. * @param newRows The number of rows the screen should have.
* @param newRows * @param cursor An int[2] containing the (column, row) cursor location.
* The number of rows the screen should have.
* @param cursor
* An int[2] containing the (column, row) cursor location.
*/ */
public void resize(int newColumns, int newRows, int newTotalRows, int[] cursor, int currentStyle, boolean altScreen) { public void resize(int newColumns, int newRows, int newTotalRows, int[] cursor, long currentStyle, boolean altScreen) {
// newRows > mTotalRows should not normally happen since mTotalRows is TRANSCRIPT_ROWS (10000): // newRows > mTotalRows should not normally happen since mTotalRows is TRANSCRIPT_ROWS (10000):
if (newColumns == mColumns && newRows <= mTotalRows) { if (newColumns == mColumns && newRows <= mTotalRows) {
// Fast resize where just the rows changed. // Fast resize where just the rows changed.
@@ -238,11 +232,12 @@ public final class TerminalBuffer {
} else { } else {
for (int i = 0; i < oldLine.getSpaceUsed(); i++) for (int i = 0; i < oldLine.getSpaceUsed(); i++)
// NEWLY INTRODUCED BUG! Should not index oldLine.mStyle with char indices // NEWLY INTRODUCED BUG! Should not index oldLine.mStyle with char indices
if (oldLine.mText[i] != ' '/* || oldLine.mStyle[i] != currentStyle */) lastNonSpaceIndex = i + 1; if (oldLine.mText[i] != ' '/* || oldLine.mStyle[i] != currentStyle */)
lastNonSpaceIndex = i + 1;
} }
int currentOldCol = 0; int currentOldCol = 0;
int styleAtCol = 0; long styleAtCol = 0;
for (int i = 0; i < lastNonSpaceIndex; i++) { for (int i = 0; i < lastNonSpaceIndex; i++) {
// Note that looping over java character, not cells. // Note that looping over java character, not cells.
char c = oldLine.mText[i]; char c = oldLine.mText[i];
@@ -302,10 +297,8 @@ public final class TerminalBuffer {
* Block copy lines and associated metadata from one location to another in the circular buffer, taking wraparound * Block copy lines and associated metadata from one location to another in the circular buffer, taking wraparound
* into account. * into account.
* *
* @param srcInternal * @param srcInternal The first line to be copied.
* The first line to be copied. * @param len The number of lines to be copied.
* @param len
* The number of lines to be copied.
*/ */
private void blockCopyLinesDown(int srcInternal, int len) { private void blockCopyLinesDown(int srcInternal, int len) {
if (len == 0) return; if (len == 0) return;
@@ -324,14 +317,11 @@ public final class TerminalBuffer {
/** /**
* Scroll the screen down one line. To scroll the whole screen of a 24 line screen, the arguments would be (0, 24). * Scroll the screen down one line. To scroll the whole screen of a 24 line screen, the arguments would be (0, 24).
* *
* @param topMargin * @param topMargin First line that is scrolled.
* First line that is scrolled. * @param bottomMargin One line after the last line that is scrolled.
* @param bottomMargin * @param style the style for the newly exposed line.
* One line after the last line that is scrolled.
* @param style
* the style for the newly exposed line.
*/ */
public void scrollDownOneLine(int topMargin, int bottomMargin, int style) { public void scrollDownOneLine(int topMargin, int bottomMargin, long style) {
if (topMargin > bottomMargin - 1 || topMargin < 0 || bottomMargin > mScreenRows) if (topMargin > bottomMargin - 1 || topMargin < 0 || bottomMargin > mScreenRows)
throw new IllegalArgumentException("topMargin=" + topMargin + ", bottomMargin=" + bottomMargin + ", mScreenRows=" + mScreenRows); throw new IllegalArgumentException("topMargin=" + topMargin + ", bottomMargin=" + bottomMargin + ", mScreenRows=" + mScreenRows);
@@ -360,18 +350,12 @@ public final class TerminalBuffer {
* of the source and destination must be within the bounds of the screen, or else an InvalidParameterException will * of the source and destination must be within the bounds of the screen, or else an InvalidParameterException will
* be thrown. * be thrown.
* *
* @param sx * @param sx source X coordinate
* source X coordinate * @param sy source Y coordinate
* @param sy * @param w width
* source Y coordinate * @param h height
* @param w * @param dx destination X coordinate
* width * @param dy destination Y coordinate
* @param h
* height
* @param dx
* destination X coordinate
* @param dy
* destination Y coordinate
*/ */
public void blockCopy(int sx, int sy, int w, int h, int dx, int dy) { public void blockCopy(int sx, int sy, int w, int h, int dx, int dy) {
if (w == 0) return; if (w == 0) return;
@@ -390,7 +374,7 @@ public final class TerminalBuffer {
* InvalidParemeterException will be thrown. Typically this is called with a "val" argument of 32 to clear a block * InvalidParemeterException will be thrown. Typically this is called with a "val" argument of 32 to clear a block
* of characters. * of characters.
*/ */
public void blockSet(int sx, int sy, int w, int h, int val, int style) { public void blockSet(int sx, int sy, int w, int h, int val, long style) {
if (sx < 0 || sx + w > mColumns || sy < 0 || sy + h > mScreenRows) { if (sx < 0 || sx + w > mColumns || sy < 0 || sy + h > mScreenRows) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
"Illegal arguments! blockSet(" + sx + ", " + sy + ", " + w + ", " + h + ", " + val + ", " + mColumns + ", " + mScreenRows + ")"); "Illegal arguments! blockSet(" + sx + ", " + sy + ", " + w + ", " + h + ", " + val + ", " + mColumns + ", " + mScreenRows + ")");
@@ -404,14 +388,14 @@ public final class TerminalBuffer {
return (mLines[row] == null) ? (mLines[row] = new TerminalRow(mColumns, 0)) : mLines[row]; return (mLines[row] == null) ? (mLines[row] = new TerminalRow(mColumns, 0)) : mLines[row];
} }
public void setChar(int column, int row, int codePoint, int style) { public void setChar(int column, int row, int codePoint, long style) {
if (row >= mScreenRows || column >= mColumns) if (row >= mScreenRows || column >= mColumns)
throw new IllegalArgumentException("row=" + row + ", column=" + column + ", mScreenRows=" + mScreenRows + ", mColumns=" + mColumns); throw new IllegalArgumentException("row=" + row + ", column=" + column + ", mScreenRows=" + mScreenRows + ", mColumns=" + mColumns);
row = externalToInternalRow(row); row = externalToInternalRow(row);
allocateFullLineIfNecessary(row).setChar(column, codePoint, style); allocateFullLineIfNecessary(row).setChar(column, codePoint, style);
} }
public int getStyleAt(int externalRow, int column) { public long getStyleAt(int externalRow, int column) {
return allocateFullLineIfNecessary(externalToInternalRow(externalRow)).getStyle(column); return allocateFullLineIfNecessary(externalToInternalRow(externalRow)).getStyle(column);
} }
@@ -423,7 +407,7 @@ public final class TerminalBuffer {
int startOfLine = (rectangular || y == top) ? left : leftMargin; int startOfLine = (rectangular || y == top) ? left : leftMargin;
int endOfLine = (rectangular || y + 1 == bottom) ? right : rightMargin; int endOfLine = (rectangular || y + 1 == bottom) ? right : rightMargin;
for (int x = startOfLine; x < endOfLine; x++) { for (int x = startOfLine; x < endOfLine; x++) {
int currentStyle = line.getStyle(x); long currentStyle = line.getStyle(x);
int foreColor = TextStyle.decodeForeColor(currentStyle); int foreColor = TextStyle.decodeForeColor(currentStyle);
int backColor = TextStyle.decodeBackColor(currentStyle); int backColor = TextStyle.decodeBackColor(currentStyle);
int effect = TextStyle.decodeEffect(currentStyle); int effect = TextStyle.decodeEffect(currentStyle);

View File

@@ -57,7 +57,7 @@ public final class TerminalColorScheme {
0xff808080, 0xff8a8a8a, 0xff949494, 0xff9e9e9e, 0xffa8a8a8, 0xffb2b2b2, 0xffbcbcbc, 0xffc6c6c6, 0xffd0d0d0, 0xffdadada, 0xffe4e4e4, 0xffeeeeee, 0xff808080, 0xff8a8a8a, 0xff949494, 0xff9e9e9e, 0xffa8a8a8, 0xffb2b2b2, 0xffbcbcbc, 0xffc6c6c6, 0xffd0d0d0, 0xffdadada, 0xffe4e4e4, 0xffeeeeee,
// COLOR_INDEX_DEFAULT_FOREGROUND, COLOR_INDEX_DEFAULT_BACKGROUND and COLOR_INDEX_DEFAULT_CURSOR: // COLOR_INDEX_DEFAULT_FOREGROUND, COLOR_INDEX_DEFAULT_BACKGROUND and COLOR_INDEX_DEFAULT_CURSOR:
0xffffffff, 0xff000000, 0xffffffff }; 0xffffffff, 0xff000000, 0xffA9AAA9};
public final int[] mDefaultColors = new int[TextStyle.NUM_INDEXED_COLORS]; public final int[] mDefaultColors = new int[TextStyle.NUM_INDEXED_COLORS];
@@ -65,7 +65,7 @@ public final class TerminalColorScheme {
reset(); reset();
} }
public void reset() { private void reset() {
System.arraycopy(DEFAULT_COLORSCHEME, 0, mDefaultColors, 0, TextStyle.NUM_INDEXED_COLORS); System.arraycopy(DEFAULT_COLORSCHEME, 0, mDefaultColors, 0, TextStyle.NUM_INDEXED_COLORS);
} }
@@ -93,7 +93,8 @@ public final class TerminalColorScheme {
} }
int colorValue = TerminalColors.parse(value); int colorValue = TerminalColors.parse(value);
if (colorValue == 0) throw new IllegalArgumentException("Property '" + key + "' has invalid color: '" + value + "'"); if (colorValue == 0)
throw new IllegalArgumentException("Property '" + key + "' has invalid color: '" + value + "'");
mDefaultColors[colorIndex] = colorValue; mDefaultColors[colorIndex] = colorValue;
} }

View File

@@ -29,7 +29,7 @@ public final class TerminalColors {
/** /**
* Parse color according to http://manpages.ubuntu.com/manpages/intrepid/man3/XQueryColor.3.html * Parse color according to http://manpages.ubuntu.com/manpages/intrepid/man3/XQueryColor.3.html
* * <p/>
* Highest bit is set if successful, so return value is 0xFF${R}${G}${B}. Return 0 if failed. * Highest bit is set if successful, so return value is 0xFF${R}${G}${B}. Return 0 if failed.
*/ */
static int parse(String c) { static int parse(String c) {

View File

@@ -1,18 +1,18 @@
package com.termux.terminal; package com.termux.terminal;
import android.util.Base64;
import android.util.Log;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Arrays; import java.util.Arrays;
import java.util.Locale; import java.util.Locale;
import java.util.Objects; import java.util.Objects;
import java.util.Stack; import java.util.Stack;
import android.util.Base64;
import android.util.Log;
/** /**
* Renders text into a screen. Contains all the terminal-specific knowledge and state. Emulates a subset of the X Window * Renders text into a screen. Contains all the terminal-specific knowledge and state. Emulates a subset of the X Window
* System xterm terminal, which in turn is an emulator for a subset of the Digital Equipment Corporation vt100 terminal. * System xterm terminal, which in turn is an emulator for a subset of the Digital Equipment Corporation vt100 terminal.
* * <p>
* References: * References:
* <ul> * <ul>
* <li>http://invisible-island.net/xterm/ctlseqs/ctlseqs.html</li> * <li>http://invisible-island.net/xterm/ctlseqs/ctlseqs.html</li>
@@ -145,7 +145,7 @@ public final class TerminalEmulator {
/** /**
* The alternate screen buffer, exactly as large as the display and contains no additional saved lines (so that when * The alternate screen buffer, exactly as large as the display and contains no additional saved lines (so that when
* the alternate screen buffer is active, you cannot scroll back to view saved lines). * the alternate screen buffer is active, you cannot scroll back to view saved lines).
* * <p>
* See http://www.xfree86.org/current/ctlseqs.html#The%20Alternate%20Screen%20Buffer * See http://www.xfree86.org/current/ctlseqs.html#The%20Alternate%20Screen%20Buffer
*/ */
final TerminalBuffer mAltBuffer; final TerminalBuffer mAltBuffer;
@@ -206,10 +206,15 @@ public final class TerminalEmulator {
*/ */
private boolean mAboutToAutoWrap; private boolean mAboutToAutoWrap;
/** Foreground and background color indices, 0..255. */ /**
* Current foreground and background colors. Can either be a color index in [0,259] or a truecolor (24-bit) value.
* For a 24-bit value the top byte (0xff000000) is set.
*
* @see TextStyle
*/
int mForeColor, mBackColor; int mForeColor, mBackColor;
/** Current TextStyle effect */ /** Current {@link TextStyle} effect. */
private int mEffect; private int mEffect;
/** /**
@@ -294,8 +299,7 @@ public final class TerminalEmulator {
} }
/** /**
* @param mouseButton * @param mouseButton one of the MOUSE_* constants of this class.
* 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 (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)) {
@@ -390,10 +394,8 @@ public final class TerminalEmulator {
/** /**
* Accept bytes (typically from the pseudo-teletype) and process them. * Accept bytes (typically from the pseudo-teletype) and process them.
* *
* @param buffer * @param buffer a byte array containing the bytes to be processed
* a byte array containing the bytes to be processed * @param length the number of bytes in the array to process
* @param length
* the number of bytes in the array to process
*/ */
public void append(byte[] buffer, int length) { public void append(byte[] buffer, int length) {
for (int i = 0; i < length; i++) for (int i = 0; i < length; i++)
@@ -614,7 +616,7 @@ public final class TerminalEmulator {
int left = Math.min(getArg(argIndex++, 1, true) + effectiveLeftMargin, effectiveRightMargin + 1); int left = Math.min(getArg(argIndex++, 1, true) + effectiveLeftMargin, effectiveRightMargin + 1);
int bottom = Math.min(getArg(argIndex++, mRows, true) + effectiveTopMargin, effectiveBottomMargin); int bottom = Math.min(getArg(argIndex++, mRows, true) + effectiveTopMargin, effectiveBottomMargin);
int right = Math.min(getArg(argIndex, mColumns, true) + effectiveLeftMargin, effectiveRightMargin); int right = Math.min(getArg(argIndex, mColumns, true) + effectiveLeftMargin, effectiveRightMargin);
int style = getStyle(); long style = getStyle();
for (int row = top - 1; row < bottom; row++) for (int row = top - 1; row < bottom; row++)
for (int col = left - 1; col < right; col++) for (int col = left - 1; col < right; col++)
if (!selective || (TextStyle.decodeEffect(mScreen.getStyleAt(row, col)) & TextStyle.CHARACTER_ATTRIBUTE_PROTECTED) == 0) if (!selective || (TextStyle.decodeEffect(mScreen.getStyleAt(row, col)) & TextStyle.CHARACTER_ATTRIBUTE_PROTECTED) == 0)
@@ -626,7 +628,7 @@ public final class TerminalEmulator {
case 't': // "${CSI}${TOP}${LEFT}${BOTTOM}${RIGHT}${ATTRIBUTES}$t" case 't': // "${CSI}${TOP}${LEFT}${BOTTOM}${RIGHT}${ATTRIBUTES}$t"
// Reverse attributes in rectangular area (DECRARA - http://www.vt100.net/docs/vt510-rm/DECRARA). // Reverse attributes in rectangular area (DECRARA - http://www.vt100.net/docs/vt510-rm/DECRARA).
boolean reverse = b == 't'; boolean reverse = b == 't';
// FIXME: "coordinates of the rectangular area are affected by the setting of origin mode (DECOM)".s // FIXME: "coordinates of the rectangular area are affected by the setting of origin mode (DECOM)".
int top = Math.min(getArg(0, 1, true) - 1, effectiveBottomMargin) + effectiveTopMargin; int top = Math.min(getArg(0, 1, true) - 1, effectiveBottomMargin) + effectiveTopMargin;
int left = Math.min(getArg(1, 1, true) - 1, effectiveRightMargin) + effectiveLeftMargin; int left = Math.min(getArg(1, 1, true) - 1, effectiveRightMargin) + effectiveLeftMargin;
int bottom = Math.min(getArg(2, mRows, true) + 1, effectiveBottomMargin - 1) + effectiveTopMargin; int bottom = Math.min(getArg(2, mRows, true) + 1, effectiveBottomMargin - 1) + effectiveTopMargin;
@@ -720,7 +722,6 @@ public final class TerminalEmulator {
} }
break; break;
case ESC_PERCENT: case ESC_PERCENT:
Log.i(EmulatorDebug.LOG_TAG, "Ignoring character set sequence 'ESC % " + (char) b + "'");
break; break;
case ESC_OSC: case ESC_OSC:
doOsc(b); doOsc(b);
@@ -897,7 +898,8 @@ public final class TerminalEmulator {
} }
} }
} else { } else {
if (LOG_ESCAPE_SEQUENCES) Log.e(EmulatorDebug.LOG_TAG, "Unrecognized device control string: " + dcs); if (LOG_ESCAPE_SEQUENCES)
Log.e(EmulatorDebug.LOG_TAG, "Unrecognized device control string: " + dcs);
} }
finishSequence(); finishSequence();
} }
@@ -955,7 +957,7 @@ public final class TerminalEmulator {
unknownSequence(b); unknownSequence(b);
break; break;
} }
int style = getStyle(); long style = getStyle();
for (int row = startRow; row < endRow; row++) { for (int row = startRow; row < endRow; row++) {
for (int col = startCol; col < endCol; col++) { for (int col = startCol; col < endCol; col++) {
if ((TextStyle.decodeEffect(mScreen.getStyleAt(row, col)) & TextStyle.CHARACTER_ATTRIBUTE_PROTECTED) == 0) if ((TextStyle.decodeEffect(mScreen.getStyleAt(row, col)) & TextStyle.CHARACTER_ATTRIBUTE_PROTECTED) == 0)
@@ -1086,7 +1088,8 @@ public final class TerminalEmulator {
// Check if buffer size needs to be updated: // Check if buffer size needs to be updated:
if (resized) resizeScreen(); if (resized) resizeScreen();
// Clear new screen if alt buffer: // Clear new screen if alt buffer:
if (newScreen == mAltBuffer) newScreen.blockSet(0, 0, mColumns, mRows, ' ', getStyle()); if (newScreen == mAltBuffer)
newScreen.blockSet(0, 0, mColumns, mRows, ' ', getStyle());
} }
break; break;
} }
@@ -1688,11 +1691,10 @@ public final class TerminalEmulator {
} else if (code >= 30 && code <= 37) { } else if (code >= 30 && code <= 37) {
mForeColor = code - 30; mForeColor = code - 30;
} else if (code == 38 || code == 48) { } else if (code == 38 || code == 48) {
// ISO-8613-3 controls to set foreground (38) or background (48) colors. // Extended set foreground(38)/background (48) color.
// P_s = (38|48) ; 2 ; P_r ; P_g ; P_b => Set to RGB value in range (0-255). // This is followed by either "2;$R;$G;$B" to set a 24-bit color or
// P_s = (38|48) ; 5 ; P_s => Set to indexed color. // "5;$INDEX" to set an indexed color.
if (i + 2 <= mArgIndex) { if (i + 2 > mArgIndex) continue;
int color = -1;
int firstArg = mArgs[i + 1]; int firstArg = mArgs[i + 1];
if (firstArg == 2) { if (firstArg == 2) {
if (i + 4 > mArgIndex) { if (i + 4 > mArgIndex) {
@@ -1702,18 +1704,18 @@ public final class TerminalEmulator {
if (red < 0 || green < 0 || blue < 0 || red > 255 || green > 255 || blue > 255) { if (red < 0 || green < 0 || blue < 0 || red > 255 || green > 255 || blue > 255) {
finishSequenceAndLogError("Invalid RGB: " + red + "," + green + "," + blue); finishSequenceAndLogError("Invalid RGB: " + red + "," + green + "," + blue);
} else { } else {
// TODO: Implement 24 bit color. int argbColor = 0xff000000 | (red << 16) | (green << 8) | blue;
finishSequenceAndLogError("Unimplemented RGB: " + red + "," + green + "," + blue); if (code == 38) {
mForeColor = argbColor;
} else {
mBackColor = argbColor;
}
} }
i += 4; // "2;P_r;P_g;P_r" i += 4; // "2;P_r;P_g;P_r"
} }
} else if (firstArg == 5) { } else if (firstArg == 5) {
color = mArgs[i + 2]; int color = mArgs[i + 2];
i += 2; // "5;P_s" i += 2; // "5;P_s"
} else {
finishSequenceAndLogError("Invalid ISO-8613-3 SGR first argument: " + firstArg);
}
if (i != -1) {
if (color >= 0 && color < TextStyle.NUM_INDEXED_COLORS) { if (color >= 0 && color < TextStyle.NUM_INDEXED_COLORS) {
if (code == 38) { if (code == 38) {
mForeColor = color; mForeColor = color;
@@ -1723,7 +1725,8 @@ public final class TerminalEmulator {
} else { } else {
if (LOG_ESCAPE_SEQUENCES) Log.w(EmulatorDebug.LOG_TAG, "Invalid color index: " + color); if (LOG_ESCAPE_SEQUENCES) Log.w(EmulatorDebug.LOG_TAG, "Invalid color index: " + color);
} }
} } else {
finishSequenceAndLogError("Invalid ISO-8613-3 SGR first argument: " + firstArg);
} }
} else if (code == 39) { // Set default foreground color. } else if (code == 39) { // Set default foreground color.
mForeColor = TextStyle.COLOR_INDEX_FOREGROUND; mForeColor = TextStyle.COLOR_INDEX_FOREGROUND;
@@ -1736,7 +1739,8 @@ public final class TerminalEmulator {
} else if (code >= 100 && code <= 107) { // Bright background color (aixterm codes). } else if (code >= 100 && code <= 107) { // Bright background color (aixterm codes).
mBackColor = code - 100 + 8; mBackColor = code - 100 + 8;
} else { } else {
if (LOG_ESCAPE_SEQUENCES) Log.w(EmulatorDebug.LOG_TAG, String.format("SGR unknown code %d", code)); if (LOG_ESCAPE_SEQUENCES)
Log.w(EmulatorDebug.LOG_TAG, String.format("SGR unknown code %d", code));
} }
} }
} }
@@ -1816,6 +1820,7 @@ public final class TerminalEmulator {
return; return;
} else { } else {
mColors.tryParseColor(colorIndex, textParameter.substring(parsingPairStart, i)); mColors.tryParseColor(colorIndex, textParameter.substring(parsingPairStart, i));
mSession.onColorsChanged();
colorIndex = -1; colorIndex = -1;
parsingPairStart = -1; parsingPairStart = -1;
} }
@@ -1851,9 +1856,11 @@ public final class TerminalEmulator {
+ String.format(Locale.US, "%04x", b) + bellOrStringTerminator); + String.format(Locale.US, "%04x", b) + bellOrStringTerminator);
} else { } else {
mColors.tryParseColor(specialIndex, colorSpec); mColors.tryParseColor(specialIndex, colorSpec);
mSession.onColorsChanged();
} }
specialIndex++; specialIndex++;
if (endOfInput || (specialIndex > TextStyle.COLOR_INDEX_CURSOR) || ++charIndex >= textParameter.length()) break; if (endOfInput || (specialIndex > TextStyle.COLOR_INDEX_CURSOR) || ++charIndex >= textParameter.length())
break;
lastSemiIndex = charIndex; lastSemiIndex = charIndex;
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
// Ignore. // Ignore.
@@ -1877,6 +1884,7 @@ public final class TerminalEmulator {
// parameters are given, the entire table will be reset. // parameters are given, the entire table will be reset.
if (textParameter.isEmpty()) { if (textParameter.isEmpty()) {
mColors.reset(); mColors.reset();
mSession.onColorsChanged();
} else { } else {
int lastIndex = 0; int lastIndex = 0;
for (int charIndex = 0; ; charIndex++) { for (int charIndex = 0; ; charIndex++) {
@@ -1885,6 +1893,7 @@ public final class TerminalEmulator {
try { try {
int colorToReset = Integer.parseInt(textParameter.substring(lastIndex, charIndex)); int colorToReset = Integer.parseInt(textParameter.substring(lastIndex, charIndex));
mColors.reset(colorToReset); mColors.reset(colorToReset);
mSession.onColorsChanged();
if (endOfInput) break; if (endOfInput) break;
charIndex++; charIndex++;
lastIndex = charIndex; lastIndex = charIndex;
@@ -1899,6 +1908,7 @@ public final class TerminalEmulator {
case 111: // Reset background color. case 111: // Reset background color.
case 112: // Reset cursor color. case 112: // Reset cursor color.
mColors.reset(TextStyle.COLOR_INDEX_FOREGROUND + (value - 110)); mColors.reset(TextStyle.COLOR_INDEX_FOREGROUND + (value - 110));
mSession.onColorsChanged();
break; break;
case 119: // Reset highlight color. case 119: // Reset highlight color.
break; break;
@@ -1917,7 +1927,7 @@ public final class TerminalEmulator {
mScreen.blockSet(sx, sy, w, h, ' ', getStyle()); mScreen.blockSet(sx, sy, w, h, ' ', getStyle());
} }
private int getStyle() { private long getStyle() {
return TextStyle.encode(mForeColor, mBackColor, mEffect); return TextStyle.encode(mForeColor, mBackColor, mEffect);
} }
@@ -2070,8 +2080,7 @@ public final class TerminalEmulator {
/** /**
* Send a Unicode code point to the screen. * Send a Unicode code point to the screen.
* *
* @param codePoint * @param codePoint The code point of the character to display
* The code point of the character to display
*/ */
private void emitCodePoint(int codePoint) { private void emitCodePoint(int codePoint) {
if (mUseLineDrawingUsesG0 ? mUseLineDrawingG0 : mUseLineDrawingG1) { if (mUseLineDrawingUsesG0 ? mUseLineDrawingG0 : mUseLineDrawingG1) {
@@ -2202,13 +2211,15 @@ public final class TerminalEmulator {
if (mInsertMode && displayWidth > 0) { if (mInsertMode && displayWidth > 0) {
// Move character to right one space. // Move character to right one space.
int destCol = mCursorCol + displayWidth; int destCol = mCursorCol + displayWidth;
if (destCol < mRightMargin) mScreen.blockCopy(mCursorCol, mCursorRow, mRightMargin - destCol, 1, destCol, mCursorRow); if (destCol < mRightMargin)
mScreen.blockCopy(mCursorCol, mCursorRow, mRightMargin - destCol, 1, destCol, mCursorRow);
} }
int offsetDueToCombiningChar = ((displayWidth <= 0 && mCursorCol > 0 && !mAboutToAutoWrap) ? 1 : 0); int offsetDueToCombiningChar = ((displayWidth <= 0 && mCursorCol > 0 && !mAboutToAutoWrap) ? 1 : 0);
mScreen.setChar(mCursorCol - offsetDueToCombiningChar, mCursorRow, codePoint, getStyle()); mScreen.setChar(mCursorCol - offsetDueToCombiningChar, mCursorRow, codePoint, getStyle());
if (autoWrap && displayWidth > 0) mAboutToAutoWrap = (mCursorCol == mRightMargin - displayWidth); if (autoWrap && displayWidth > 0)
mAboutToAutoWrap = (mCursorCol == mRightMargin - displayWidth);
mCursorCol = Math.min(mCursorCol + displayWidth, mRightMargin - 1); mCursorCol = Math.min(mCursorCol + displayWidth, mRightMargin - 1);
} }
@@ -2273,6 +2284,7 @@ public final class TerminalEmulator {
mUtf8Index = mUtf8ToFollow = 0; mUtf8Index = mUtf8ToFollow = 0;
mColors.reset(); mColors.reset();
mSession.onColorsChanged();
} }
public String getSelectedText(int x1, int y1, int x2, int y2) { public String getSelectedText(int x1, int y1, int x2, int y2) {

View File

@@ -23,4 +23,6 @@ public abstract class TerminalOutput {
/** Notify the terminal client that a bell character (ASCII 7, bell, BEL, \a, ^G)) has been received. */ /** Notify the terminal client that a bell character (ASCII 7, bell, BEL, \a, ^G)) has been received. */
public abstract void onBell(); public abstract void onBell();
public abstract void onColorsChanged();
} }

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/>
* 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 {
@@ -20,13 +20,13 @@ public final class TerminalRow {
/** If this row has been line wrapped due to text output at the end of line. */ /** If this row has been line wrapped due to text output at the end of line. */
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 int[] mStyle; final long[] mStyle;
/** 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, int style) { public TerminalRow(int columns, long style) {
mColumns = columns; mColumns = columns;
mText = new char[(int) (SPARE_CAPACITY_FACTOR * columns)]; mText = new char[(int) (SPARE_CAPACITY_FACTOR * columns)];
mStyle = new int[columns]; mStyle = new long[columns];
clear(style); clear(style);
} }
@@ -112,14 +112,14 @@ public final class TerminalRow {
return false; return false;
} }
public void clear(int style) { public void clear(long style) {
Arrays.fill(mText, ' '); Arrays.fill(mText, ' ');
Arrays.fill(mStyle, style); Arrays.fill(mStyle, style);
mSpaceUsed = (short) mColumns; mSpaceUsed = (short) mColumns;
} }
// https://github.com/steven676/Android-Terminal-Emulator/commit/9a47042620bec87617f0b4f5d50568535668fe26 // https://github.com/steven676/Android-Terminal-Emulator/commit/9a47042620bec87617f0b4f5d50568535668fe26
public void setChar(int columnToSet, int codePoint, int style) { public void setChar(int columnToSet, int codePoint, long style) {
mStyle[columnToSet] = style; mStyle[columnToSet] = style;
final int newCodePointDisplayWidth = WcWidth.width(codePoint); final int newCodePointDisplayWidth = WcWidth.width(codePoint);
@@ -225,7 +225,7 @@ public final class TerminalRow {
return true; return true;
} }
public final int getStyle(int column) { public final long getStyle(int column) {
return mStyle[column]; return mStyle[column];
} }

View File

@@ -1,5 +1,13 @@
package com.termux.terminal; package com.termux.terminal;
import android.annotation.SuppressLint;
import android.os.Handler;
import android.os.Message;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
import android.util.Log;
import java.io.FileDescriptor; import java.io.FileDescriptor;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
@@ -9,20 +17,15 @@ import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.UUID; import java.util.UUID;
import android.annotation.SuppressLint;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
/** /**
* A terminal session, consisting of a process coupled to a terminal interface. * 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/>
* 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 {
@@ -38,6 +41,9 @@ public final class TerminalSession extends TerminalOutput {
void onClipboardText(TerminalSession session, String text); void onClipboardText(TerminalSession session, String text);
void onBell(TerminalSession session); void onBell(TerminalSession session);
void onColorsChanged(TerminalSession session);
} }
private static FileDescriptor wrapFileDescriptor(int fileDescriptor) { private static FileDescriptor wrapFileDescriptor(int fileDescriptor) {
@@ -163,13 +169,11 @@ public final class TerminalSession extends TerminalOutput {
/** /**
* Set the terminal emulator's window size and start terminal emulation. * Set the terminal emulator's window size and start terminal emulation.
* *
* @param columns * @param columns The number of columns in the terminal window.
* The number of columns in the terminal window. * @param rows The number of rows in the terminal window.
* @param rows
* The number of rows in the terminal window.
*/ */
public void initializeEmulator(int columns, int rows) { public void initializeEmulator(int columns, int rows) {
mEmulator = new TerminalEmulator(this, columns, rows, /* transcript= */5000); mEmulator = new TerminalEmulator(this, columns, rows, /* transcript= */2000);
int[] processId = new int[1]; int[] processId = new int[1];
mTerminalFileDescriptor = JNI.createSubprocess(mShellPath, mCwd, mArgs, mEnv, processId, rows, columns); mTerminalFileDescriptor = JNI.createSubprocess(mShellPath, mCwd, mArgs, mEnv, processId, rows, columns);
@@ -190,10 +194,6 @@ public final class TerminalSession extends TerminalOutput {
} }
} catch (Exception e) { } catch (Exception e) {
// Ignore, just shutting down. // Ignore, just shutting down.
} finally {
// Now wait for process exit:
int processExitCode = JNI.waitFor(mShellPid);
mMainThreadHandler.sendMessage(mMainThreadHandler.obtainMessage(MSG_PROCESS_EXITED, processExitCode));
} }
} }
}.start(); }.start();
@@ -213,6 +213,15 @@ public final class TerminalSession extends TerminalOutput {
} }
} }
}.start(); }.start();
new Thread("TermSessionWaiter[pid=" + mShellPid + "]") {
@Override
public void run() {
int processExitCode = JNI.waitFor(mShellPid);
mMainThreadHandler.sendMessage(mMainThreadHandler.obtainMessage(MSG_PROCESS_EXITED, processExitCode));
}
}.start();
} }
/** Write data to the shell process. */ /** Write data to the shell process. */
@@ -273,18 +282,14 @@ public final class TerminalSession extends TerminalOutput {
notifyScreenUpdate(); notifyScreenUpdate();
} }
/** /** Finish this terminal session by sending SIGKILL to the shell. */
* Finish this terminal session. Frees resources used by the terminal emulator and closes the attached
* <code>InputStream</code> and <code>OutputStream</code>.
*/
public void finishIfRunning() { public void finishIfRunning() {
if (isRunning()) { if (isRunning()) {
JNI.hangupProcessGroup(mShellPid); try {
// Stop the reader and writer threads, and close the I/O streams. Note that Os.kill(mShellPid, OsConstants.SIGKILL);
// cleanupResources() will be run later. } catch (ErrnoException e) {
mTerminalToProcessIOQueue.close(); Log.w("termux", "Failed sending SIGKILL: " + e.getMessage());
mProcessToTerminalIOQueue.close(); }
JNI.close(mTerminalFileDescriptor);
} }
} }
@@ -325,4 +330,13 @@ public final class TerminalSession extends TerminalOutput {
mChangeCallback.onBell(this); mChangeCallback.onBell(this);
} }
@Override
public void onColorsChanged() {
mChangeCallback.onColorsChanged(this);
}
public int getPid() {
return mShellPid;
}
} }

View File

@@ -1,11 +1,13 @@
package com.termux.terminal; package com.termux.terminal;
/** /**
* Encodes effects, foreground and background colors into a 32 bit integer, 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/>
* The foreground and background colors take 9 bits each, leaving (32-9-9)=14 bits for effect flags. Using 9 for now * The bit layout is:
* (the different CHARACTER_ATTRIBUTE_* bits). * - 16 flags (11 currently used).
* - 24 for foreground color (only 9 first bits if a color index).
* - 24 for background color (only 9 first bits if a color index).
*/ */
public final class TextStyle { public final class TextStyle {
@@ -18,13 +20,17 @@ 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/>
* 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.
*/ */
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. */
public final static int CHARACTER_ATTRIBUTE_DIM = 1 << 8; public final static int CHARACTER_ATTRIBUTE_DIM = 1 << 8;
/** If true (24-bit) color is used for the cell for foreground. */
private final static int CHARACTER_ATTRIBUTE_TRUECOLOR_FOREGROUND = 1 << 9;
/** If true (24-bit) color is used for the cell for foreground. */
private final static int CHARACTER_ATTRIBUTE_TRUECOLOR_BACKGROUND= 1 << 10;
public final static int COLOR_INDEX_FOREGROUND = 256; public final static int COLOR_INDEX_FOREGROUND = 256;
public final static int COLOR_INDEX_BACKGROUND = 257; public final static int COLOR_INDEX_BACKGROUND = 257;
@@ -34,22 +40,47 @@ public final class TextStyle {
public final static int NUM_INDEXED_COLORS = 259; public final static int NUM_INDEXED_COLORS = 259;
/** Normal foreground and background colors and no effects. */ /** Normal foreground and background colors and no effects. */
final static int NORMAL = encode(COLOR_INDEX_FOREGROUND, COLOR_INDEX_BACKGROUND, 0); final static long NORMAL = encode(COLOR_INDEX_FOREGROUND, COLOR_INDEX_BACKGROUND, 0);
static int encode(int foreColor, int backColor, int effect) { static long encode(int foreColor, int backColor, int effect) {
return ((effect & 0b111111111) << 18) | ((foreColor & 0b111111111) << 9) | (backColor & 0b111111111); long result = effect & 0b111111111;
if ((0xff000000 & foreColor) == 0xff000000) {
// 24-bit color.
result |= CHARACTER_ATTRIBUTE_TRUECOLOR_FOREGROUND | ((foreColor & 0x00ffffffL) << 40L);
} else {
// Indexed color.
result |= (foreColor & 0b111111111L) << 40;
}
if ((0xff000000 & backColor) == 0xff000000) {
// 24-bit color.
result |= CHARACTER_ATTRIBUTE_TRUECOLOR_BACKGROUND | ((backColor & 0x00ffffffL) << 16L);
} else {
// Indexed color.
result |= (backColor & 0b111111111L) << 16L;
} }
public static int decodeForeColor(int encodedColor) { return result;
return (encodedColor >> 9) & 0b111111111;
} }
public static int decodeBackColor(int encodedColor) { public static int decodeForeColor(long style) {
return encodedColor & 0b111111111; if ((style & CHARACTER_ATTRIBUTE_TRUECOLOR_FOREGROUND) == 0) {
} return (int) ((style >>> 40) & 0b111111111L);
} else {
public static int decodeEffect(int encodedColor) { return 0xff000000 | (int) ((style >>> 40) & 0x00ffffffL);
return (encodedColor >> 18) & 0b111111111; }
}
public static int decodeBackColor(long style) {
if ((style & CHARACTER_ATTRIBUTE_TRUECOLOR_BACKGROUND) == 0) {
return (int) ((style >>> 16) & 0b111111111L);
} else {
return 0xff000000 | (int) ((style >>> 16) & 0x00ffffffL);
}
}
public static int decodeEffect(long style) {
return (int) (style & 0b11111111111);
} }
} }

View File

@@ -1,102 +1,452 @@
package com.termux.terminal; package com.termux.terminal;
/** /**
* wcwidth() implementation from http://git.musl-libc.org/cgit/musl/tree/src/ctype * Implementation of wcwidth(3) for Unicode 9.
* *
* Modified to return 0 instead of -1. * Implementation from https://github.com/jquast/wcwidth but we return 0 for unprintable characters.
*/ */
public final class WcWidth { public final class WcWidth {
private static final short table[] = { 16, 16, 16, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 16, 16, 32, 16, 16, 16, 33, 34, 35, 36, 37, 38, // From https://github.com/jquast/wcwidth/blob/master/wcwidth/table_zero.py
39, 16, 16, 40, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 41, 42, 16, 16, 43, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, // t commit 0d7de112202cc8b2ebe9232ff4a5c954f19d561a (2016-07-02):
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, private static final int[][] ZERO_WIDTH = {
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, {0x0300, 0x036f}, // Combining Grave Accent ..Combining Latin Small Le
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 44, 16, 45, 46, 47, 48, 16, 16, 16, 16, 16, {0x0483, 0x0489}, // Combining Cyrillic Titlo..Combining Cyrillic Milli
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, {0x0591, 0x05bd}, // Hebrew Accent Etnahta ..Hebrew Point Meteg
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, {0x05bf, 0x05bf}, // Hebrew Point Rafe ..Hebrew Point Rafe
49, 16, 16, 50, 51, 16, 52, 16, 16, 16, 16, 16, 16, 16, 16, 53, 16, 16, 16, 16, 16, 54, 55, 16, 16, 16, 16, 56, 16, 16, 16, 16, 16, 16, 16, 16, 16, {0x05c1, 0x05c2}, // Hebrew Point Shin Dot ..Hebrew Point Sin Dot
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, {0x05c4, 0x05c5}, // Hebrew Mark Upper Dot ..Hebrew Mark Lower Dot
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, {0x05c7, 0x05c7}, // Hebrew Point Qamats Qata..Hebrew Point Qamats Qata
16, 16, 16, 16, 16, 57, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, {0x0610, 0x061a}, // Arabic Sign Sallallahou ..Arabic Small Kasra
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, {0x064b, 0x065f}, // Arabic Fathatan ..Arabic Wavy Hamza Below
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 58, 59, 16, 16, 16, 16, 16, 16, {0x0670, 0x0670}, // Arabic Letter Superscrip..Arabic Letter Superscrip
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, {0x06d6, 0x06dc}, // Arabic Small High Ligatu..Arabic Small High Seen
16, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, {0x06df, 0x06e4}, // Arabic Small High Rounde..Arabic Small High Madda
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, {0x06e7, 0x06e8}, // Arabic Small High Yeh ..Arabic Small High Noon
255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0x06ea, 0x06ed}, // Arabic Empty Centre Low ..Arabic Small Low Meem
248, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 254, 255, 255, 255, 255, 191, 182, 0, 0, 0, {0x0711, 0x0711}, // Syriac Letter Superscrip..Syriac Letter Superscrip
0, 0, 0, 0, 31, 0, 255, 7, 0, 0, 0, 0, 0, 248, 255, 255, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 191, 159, 61, 0, 0, 0, 128, 2, 0, 0, 0, {0x0730, 0x074a}, // Syriac Pthaha Above ..Syriac Barrekh
255, 255, 255, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 255, 1, 0, 0, 0, 0, 0, 0, 248, 15, 0, 0, 0, 192, 251, 239, 62, 0, 0, 0, 0, 0, 14, 0, 0, 0, 0, {0x07a6, 0x07b0}, // Thaana Abafili ..Thaana Sukun
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 240, 255, 255, 127, 7, 0, 0, 0, 0, 0, 0, 20, 254, 33, 254, 0, 12, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 16, 30, 32, 0, {0x07eb, 0x07f3}, // Nko Combining Sh||t High..Nko Combining Double Dot
0, 12, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 16, 134, 57, 2, 0, 0, 0, 35, 0, 6, 0, 0, 0, 0, 0, 0, 16, 190, 33, 0, 0, 12, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 144, {0x0816, 0x0819}, // Samaritan Mark In ..Samaritan Mark Dagesh
30, 32, 64, 0, 12, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 1, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 193, 61, 96, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, {0x081b, 0x0823}, // Samaritan Mark Epentheti..Samaritan Vowel Sign A
0, 0, 144, 64, 48, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 30, 32, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 92, 0, 0, 0, 0, 0, 0, 0, 0, {0x0825, 0x0827}, // Samaritan Vowel Sign Sho..Samaritan Vowel Sign U
0, 0, 0, 242, 7, 128, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 242, 27, 0, 63, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 160, 2, 0, 0, 0, 0, 0, 0, 254, {0x0829, 0x082d}, // Samaritan Vowel Sign Lon..Samaritan Mark Nequdaa
127, 223, 224, 255, 254, 255, 255, 255, 31, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 253, 102, 0, 0, 0, 195, 1, 0, 30, 0, 100, 32, 0, 32, 0, 0, {0x0859, 0x085b}, // Mandaic Affrication Mark..Mandaic Gemination Mark
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, {0x08d4, 0x08e1}, // (nil) ..
28, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 176, 63, 64, 254, 15, 32, 0, 0, 0, 0, 0, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0x08e3, 0x0902}, // Arabic Turned Damma Belo..Devanagari Sign Anusvara
0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 135, 1, 4, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0x093a, 0x093a}, // Devanagari Vowel Sign Oe..Devanagari Vowel Sign Oe
128, 1, 0, 0, 0, 0, 0, 0, 64, 127, 229, 31, 248, 159, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 208, 23, 4, 0, 0, 0, 0, {0x093c, 0x093c}, // Devanagari Sign Nukta ..Devanagari Sign Nukta
248, 15, 0, 3, 0, 0, 0, 60, 11, 0, 0, 0, 0, 0, 0, 64, 163, 3, 0, 0, 0, 0, 0, 0, 240, 207, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0x0941, 0x0948}, // Devanagari Vowel Sign U ..Devanagari Vowel Sign Ai
247, 255, 253, 33, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 127, 0, 0, 240, 0, 248, 0, 0, {0x094d, 0x094d}, // Devanagari Sign Virama ..Devanagari Sign Virama
0, 124, 0, 0, 0, 0, 0, 0, 31, 252, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0x0951, 0x0957}, // Devanagari Stress Sign U..Devanagari Vowel Sign Uu
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, {0x0962, 0x0963}, // Devanagari Vowel Sign Vo..Devanagari Vowel Sign Vo
255, 0, 0, 0, 0, 0, 60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, {0x0981, 0x0981}, // Bengali Sign Candrabindu..Bengali Sign Candrabindu
247, 63, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 68, 8, 0, 0, 96, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, {0x09bc, 0x09bc}, // Bengali Sign Nukta ..Bengali Sign Nukta
255, 255, 3, 0, 0, 0, 0, 0, 192, 63, 0, 0, 128, 255, 3, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 200, 19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 126, 102, {0x09c1, 0x09c4}, // Bengali Vowel Sign U ..Bengali Vowel Sign Vocal
0, 8, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 157, 193, 2, 0, 0, 0, 0, 48, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0x09cd, 0x09cd}, // Bengali Sign Virama ..Bengali Sign Virama
0, 0, 0, 0, 0, 0, 32, 33, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, {0x09e2, 0x09e3}, // Bengali Vowel Sign Vocal..Bengali Vowel Sign Vocal
127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0x0a01, 0x0a02}, // Gurmukhi Sign Adak Bindi..Gurmukhi Sign Bindi
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 110, 240, 0, {0x0a3c, 0x0a3c}, // Gurmukhi Sign Nukta ..Gurmukhi Sign Nukta
0, 0, 0, 0, 135, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 255, 127, 0, 0, 0, 0, 0, 0, 0, 3, 0, {0x0a41, 0x0a42}, // Gurmukhi Vowel Sign U ..Gurmukhi Vowel Sign Uu
0, 0, 0, 0, 120, 38, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 128, 239, 31, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 192, 127, 0, 0, 0, 0, 0, 0, 0, {0x0a47, 0x0a48}, // Gurmukhi Vowel Sign Ee ..Gurmukhi Vowel Sign Ai
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 40, 191, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0x0a4b, 0x0a4d}, // Gurmukhi Vowel Sign Oo ..Gurmukhi Sign Virama
0, 0, 128, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 3, 248, 255, 231, 15, 0, 0, 0, 60, 0, 0, 0, 0, 0, 0, {0x0a51, 0x0a51}, // Gurmukhi Sign Udaat ..Gurmukhi Sign Udaat
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }; {0x0a70, 0x0a71}, // Gurmukhi Tippi ..Gurmukhi Addak
{0x0a75, 0x0a75}, // Gurmukhi Sign Yakash ..Gurmukhi Sign Yakash
{0x0a81, 0x0a82}, // Gujarati Sign Candrabind..Gujarati Sign Anusvara
{0x0abc, 0x0abc}, // Gujarati Sign Nukta ..Gujarati Sign Nukta
{0x0ac1, 0x0ac5}, // Gujarati Vowel Sign U ..Gujarati Vowel Sign Cand
{0x0ac7, 0x0ac8}, // Gujarati Vowel Sign E ..Gujarati Vowel Sign Ai
{0x0acd, 0x0acd}, // Gujarati Sign Virama ..Gujarati Sign Virama
{0x0ae2, 0x0ae3}, // Gujarati Vowel Sign Voca..Gujarati Vowel Sign Voca
{0x0b01, 0x0b01}, // ||iya Sign Candrabindu ..||iya Sign Candrabindu
{0x0b3c, 0x0b3c}, // ||iya Sign Nukta ..||iya Sign Nukta
{0x0b3f, 0x0b3f}, // ||iya Vowel Sign I ..||iya Vowel Sign I
{0x0b41, 0x0b44}, // ||iya Vowel Sign U ..||iya Vowel Sign Vocalic
{0x0b4d, 0x0b4d}, // ||iya Sign Virama ..||iya Sign Virama
{0x0b56, 0x0b56}, // ||iya Ai Length Mark ..||iya Ai Length Mark
{0x0b62, 0x0b63}, // ||iya Vowel Sign Vocalic..||iya Vowel Sign Vocalic
{0x0b82, 0x0b82}, // Tamil Sign Anusvara ..Tamil Sign Anusvara
{0x0bc0, 0x0bc0}, // Tamil Vowel Sign Ii ..Tamil Vowel Sign Ii
{0x0bcd, 0x0bcd}, // Tamil Sign Virama ..Tamil Sign Virama
{0x0c00, 0x0c00}, // Telugu Sign Combining Ca..Telugu Sign Combining Ca
{0x0c3e, 0x0c40}, // Telugu Vowel Sign Aa ..Telugu Vowel Sign Ii
{0x0c46, 0x0c48}, // Telugu Vowel Sign E ..Telugu Vowel Sign Ai
{0x0c4a, 0x0c4d}, // Telugu Vowel Sign O ..Telugu Sign Virama
{0x0c55, 0x0c56}, // Telugu Length Mark ..Telugu Ai Length Mark
{0x0c62, 0x0c63}, // Telugu Vowel Sign Vocali..Telugu Vowel Sign Vocali
{0x0c81, 0x0c81}, // Kannada Sign Candrabindu..Kannada Sign Candrabindu
{0x0cbc, 0x0cbc}, // Kannada Sign Nukta ..Kannada Sign Nukta
{0x0cbf, 0x0cbf}, // Kannada Vowel Sign I ..Kannada Vowel Sign I
{0x0cc6, 0x0cc6}, // Kannada Vowel Sign E ..Kannada Vowel Sign E
{0x0ccc, 0x0ccd}, // Kannada Vowel Sign Au ..Kannada Sign Virama
{0x0ce2, 0x0ce3}, // Kannada Vowel Sign Vocal..Kannada Vowel Sign Vocal
{0x0d01, 0x0d01}, // Malayalam Sign Candrabin..Malayalam Sign Candrabin
{0x0d41, 0x0d44}, // Malayalam Vowel Sign U ..Malayalam Vowel Sign Voc
{0x0d4d, 0x0d4d}, // Malayalam Sign Virama ..Malayalam Sign Virama
{0x0d62, 0x0d63}, // Malayalam Vowel Sign Voc..Malayalam Vowel Sign Voc
{0x0dca, 0x0dca}, // Sinhala Sign Al-lakuna ..Sinhala Sign Al-lakuna
{0x0dd2, 0x0dd4}, // Sinhala Vowel Sign Ketti..Sinhala Vowel Sign Ketti
{0x0dd6, 0x0dd6}, // Sinhala Vowel Sign Diga ..Sinhala Vowel Sign Diga
{0x0e31, 0x0e31}, // Thai Character Mai Han-a..Thai Character Mai Han-a
{0x0e34, 0x0e3a}, // Thai Character Sara I ..Thai Character Phinthu
{0x0e47, 0x0e4e}, // Thai Character Maitaikhu..Thai Character Yamakkan
{0x0eb1, 0x0eb1}, // Lao Vowel Sign Mai Kan ..Lao Vowel Sign Mai Kan
{0x0eb4, 0x0eb9}, // Lao Vowel Sign I ..Lao Vowel Sign Uu
{0x0ebb, 0x0ebc}, // Lao Vowel Sign Mai Kon ..Lao Semivowel Sign Lo
{0x0ec8, 0x0ecd}, // Lao Tone Mai Ek ..Lao Niggahita
{0x0f18, 0x0f19}, // Tibetan Astrological Sig..Tibetan Astrological Sig
{0x0f35, 0x0f35}, // Tibetan Mark Ngas Bzung ..Tibetan Mark Ngas Bzung
{0x0f37, 0x0f37}, // Tibetan Mark Ngas Bzung ..Tibetan Mark Ngas Bzung
{0x0f39, 0x0f39}, // Tibetan Mark Tsa -phru ..Tibetan Mark Tsa -phru
{0x0f71, 0x0f7e}, // Tibetan Vowel Sign Aa ..Tibetan Sign Rjes Su Nga
{0x0f80, 0x0f84}, // Tibetan Vowel Sign Rever..Tibetan Mark Halanta
{0x0f86, 0x0f87}, // Tibetan Sign Lci Rtags ..Tibetan Sign Yang Rtags
{0x0f8d, 0x0f97}, // Tibetan Subjoined Sign L..Tibetan Subjoined Letter
{0x0f99, 0x0fbc}, // Tibetan Subjoined Letter..Tibetan Subjoined Letter
{0x0fc6, 0x0fc6}, // Tibetan Symbol Padma Gda..Tibetan Symbol Padma Gda
{0x102d, 0x1030}, // Myanmar Vowel Sign I ..Myanmar Vowel Sign Uu
{0x1032, 0x1037}, // Myanmar Vowel Sign Ai ..Myanmar Sign Dot Below
{0x1039, 0x103a}, // Myanmar Sign Virama ..Myanmar Sign Asat
{0x103d, 0x103e}, // Myanmar Consonant Sign M..Myanmar Consonant Sign M
{0x1058, 0x1059}, // Myanmar Vowel Sign Vocal..Myanmar Vowel Sign Vocal
{0x105e, 0x1060}, // Myanmar Consonant Sign M..Myanmar Consonant Sign M
{0x1071, 0x1074}, // Myanmar Vowel Sign Geba ..Myanmar Vowel Sign Kayah
{0x1082, 0x1082}, // Myanmar Consonant Sign S..Myanmar Consonant Sign S
{0x1085, 0x1086}, // Myanmar Vowel Sign Shan ..Myanmar Vowel Sign Shan
{0x108d, 0x108d}, // Myanmar Sign Shan Counci..Myanmar Sign Shan Counci
{0x109d, 0x109d}, // Myanmar Vowel Sign Aiton..Myanmar Vowel Sign Aiton
{0x135d, 0x135f}, // Ethiopic Combining Gemin..Ethiopic Combining Gemin
{0x1712, 0x1714}, // Tagalog Vowel Sign I ..Tagalog Sign Virama
{0x1732, 0x1734}, // Hanunoo Vowel Sign I ..Hanunoo Sign Pamudpod
{0x1752, 0x1753}, // Buhid Vowel Sign I ..Buhid Vowel Sign U
{0x1772, 0x1773}, // Tagbanwa Vowel Sign I ..Tagbanwa Vowel Sign U
{0x17b4, 0x17b5}, // Khmer Vowel Inherent Aq ..Khmer Vowel Inherent Aa
{0x17b7, 0x17bd}, // Khmer Vowel Sign I ..Khmer Vowel Sign Ua
{0x17c6, 0x17c6}, // Khmer Sign Nikahit ..Khmer Sign Nikahit
{0x17c9, 0x17d3}, // Khmer Sign Muusikatoan ..Khmer Sign Bathamasat
{0x17dd, 0x17dd}, // Khmer Sign Atthacan ..Khmer Sign Atthacan
{0x180b, 0x180d}, // Mongolian Free Variation..Mongolian Free Variation
{0x1885, 0x1886}, // Mongolian Letter Ali Gal..Mongolian Letter Ali Gal
{0x18a9, 0x18a9}, // Mongolian Letter Ali Gal..Mongolian Letter Ali Gal
{0x1920, 0x1922}, // Limbu Vowel Sign A ..Limbu Vowel Sign U
{0x1927, 0x1928}, // Limbu Vowel Sign E ..Limbu Vowel Sign O
{0x1932, 0x1932}, // Limbu Small Letter Anusv..Limbu Small Letter Anusv
{0x1939, 0x193b}, // Limbu Sign Mukphreng ..Limbu Sign Sa-i
{0x1a17, 0x1a18}, // Buginese Vowel Sign I ..Buginese Vowel Sign U
{0x1a1b, 0x1a1b}, // Buginese Vowel Sign Ae ..Buginese Vowel Sign Ae
{0x1a56, 0x1a56}, // Tai Tham Consonant Sign ..Tai Tham Consonant Sign
{0x1a58, 0x1a5e}, // Tai Tham Sign Mai Kang L..Tai Tham Consonant Sign
{0x1a60, 0x1a60}, // Tai Tham Sign Sakot ..Tai Tham Sign Sakot
{0x1a62, 0x1a62}, // Tai Tham Vowel Sign Mai ..Tai Tham Vowel Sign Mai
{0x1a65, 0x1a6c}, // Tai Tham Vowel Sign I ..Tai Tham Vowel Sign Oa B
{0x1a73, 0x1a7c}, // Tai Tham Vowel Sign Oa A..Tai Tham Sign Khuen-lue
{0x1a7f, 0x1a7f}, // Tai Tham Combining Crypt..Tai Tham Combining Crypt
{0x1ab0, 0x1abe}, // Combining Doubled Circum..Combining Parentheses Ov
{0x1b00, 0x1b03}, // Balinese Sign Ulu Ricem ..Balinese Sign Surang
{0x1b34, 0x1b34}, // Balinese Sign Rerekan ..Balinese Sign Rerekan
{0x1b36, 0x1b3a}, // Balinese Vowel Sign Ulu ..Balinese Vowel Sign Ra R
{0x1b3c, 0x1b3c}, // Balinese Vowel Sign La L..Balinese Vowel Sign La L
{0x1b42, 0x1b42}, // Balinese Vowel Sign Pepe..Balinese Vowel Sign Pepe
{0x1b6b, 0x1b73}, // Balinese Musical Symbol ..Balinese Musical Symbol
{0x1b80, 0x1b81}, // Sundanese Sign Panyecek ..Sundanese Sign Panglayar
{0x1ba2, 0x1ba5}, // Sundanese Consonant Sign..Sundanese Vowel Sign Pan
{0x1ba8, 0x1ba9}, // Sundanese Vowel Sign Pam..Sundanese Vowel Sign Pan
{0x1bab, 0x1bad}, // Sundanese Sign Virama ..Sundanese Consonant Sign
{0x1be6, 0x1be6}, // Batak Sign Tompi ..Batak Sign Tompi
{0x1be8, 0x1be9}, // Batak Vowel Sign Pakpak ..Batak Vowel Sign Ee
{0x1bed, 0x1bed}, // Batak Vowel Sign Karo O ..Batak Vowel Sign Karo O
{0x1bef, 0x1bf1}, // Batak Vowel Sign U F|| S..Batak Consonant Sign H
{0x1c2c, 0x1c33}, // Lepcha Vowel Sign E ..Lepcha Consonant Sign T
{0x1c36, 0x1c37}, // Lepcha Sign Ran ..Lepcha Sign Nukta
{0x1cd0, 0x1cd2}, // Vedic Tone Karshana ..Vedic Tone Prenkha
{0x1cd4, 0x1ce0}, // Vedic Sign Yajurvedic Mi..Vedic Tone Rigvedic Kash
{0x1ce2, 0x1ce8}, // Vedic Sign Visarga Svari..Vedic Sign Visarga Anuda
{0x1ced, 0x1ced}, // Vedic Sign Tiryak ..Vedic Sign Tiryak
{0x1cf4, 0x1cf4}, // Vedic Tone Candra Above ..Vedic Tone Candra Above
{0x1cf8, 0x1cf9}, // Vedic Tone Ring Above ..Vedic Tone Double Ring A
{0x1dc0, 0x1df5}, // Combining Dotted Grave A..Combining Up Tack Above
{0x1dfb, 0x1dff}, // (nil) ..Combining Right Arrowhea
{0x20d0, 0x20f0}, // Combining Left Harpoon A..Combining Asterisk Above
{0x2cef, 0x2cf1}, // Coptic Combining Ni Abov..Coptic Combining Spiritu
{0x2d7f, 0x2d7f}, // Tifinagh Consonant Joine..Tifinagh Consonant Joine
{0x2de0, 0x2dff}, // Combining Cyrillic Lette..Combining Cyrillic Lette
{0x302a, 0x302d}, // Ideographic Level Tone M..Ideographic Entering Ton
{0x3099, 0x309a}, // Combining Katakana-hirag..Combining Katakana-hirag
{0xa66f, 0xa672}, // Combining Cyrillic Vzmet..Combining Cyrillic Thous
{0xa674, 0xa67d}, // Combining Cyrillic Lette..Combining Cyrillic Payer
{0xa69e, 0xa69f}, // Combining Cyrillic Lette..Combining Cyrillic Lette
{0xa6f0, 0xa6f1}, // Bamum Combining Mark Koq..Bamum Combining Mark Tuk
{0xa802, 0xa802}, // Syloti Nagri Sign Dvisva..Syloti Nagri Sign Dvisva
{0xa806, 0xa806}, // Syloti Nagri Sign Hasant..Syloti Nagri Sign Hasant
{0xa80b, 0xa80b}, // Syloti Nagri Sign Anusva..Syloti Nagri Sign Anusva
{0xa825, 0xa826}, // Syloti Nagri Vowel Sign ..Syloti Nagri Vowel Sign
{0xa8c4, 0xa8c5}, // Saurashtra Sign Virama ..
{0xa8e0, 0xa8f1}, // Combining Devanagari Dig..Combining Devanagari Sig
{0xa926, 0xa92d}, // Kayah Li Vowel Ue ..Kayah Li Tone Calya Plop
{0xa947, 0xa951}, // Rejang Vowel Sign I ..Rejang Consonant Sign R
{0xa980, 0xa982}, // Javanese Sign Panyangga ..Javanese Sign Layar
{0xa9b3, 0xa9b3}, // Javanese Sign Cecak Telu..Javanese Sign Cecak Telu
{0xa9b6, 0xa9b9}, // Javanese Vowel Sign Wulu..Javanese Vowel Sign Suku
{0xa9bc, 0xa9bc}, // Javanese Vowel Sign Pepe..Javanese Vowel Sign Pepe
{0xa9e5, 0xa9e5}, // Myanmar Sign Shan Saw ..Myanmar Sign Shan Saw
{0xaa29, 0xaa2e}, // Cham Vowel Sign Aa ..Cham Vowel Sign Oe
{0xaa31, 0xaa32}, // Cham Vowel Sign Au ..Cham Vowel Sign Ue
{0xaa35, 0xaa36}, // Cham Consonant Sign La ..Cham Consonant Sign Wa
{0xaa43, 0xaa43}, // Cham Consonant Sign Fina..Cham Consonant Sign Fina
{0xaa4c, 0xaa4c}, // Cham Consonant Sign Fina..Cham Consonant Sign Fina
{0xaa7c, 0xaa7c}, // Myanmar Sign Tai Laing T..Myanmar Sign Tai Laing T
{0xaab0, 0xaab0}, // Tai Viet Mai Kang ..Tai Viet Mai Kang
{0xaab2, 0xaab4}, // Tai Viet Vowel I ..Tai Viet Vowel U
{0xaab7, 0xaab8}, // Tai Viet Mai Khit ..Tai Viet Vowel Ia
{0xaabe, 0xaabf}, // Tai Viet Vowel Am ..Tai Viet Tone Mai Ek
{0xaac1, 0xaac1}, // Tai Viet Tone Mai Tho ..Tai Viet Tone Mai Tho
{0xaaec, 0xaaed}, // Meetei Mayek Vowel Sign ..Meetei Mayek Vowel Sign
{0xaaf6, 0xaaf6}, // Meetei Mayek Virama ..Meetei Mayek Virama
{0xabe5, 0xabe5}, // Meetei Mayek Vowel Sign ..Meetei Mayek Vowel Sign
{0xabe8, 0xabe8}, // Meetei Mayek Vowel Sign ..Meetei Mayek Vowel Sign
{0xabed, 0xabed}, // Meetei Mayek Apun Iyek ..Meetei Mayek Apun Iyek
{0xfb1e, 0xfb1e}, // Hebrew Point Judeo-spani..Hebrew Point Judeo-spani
{0xfe00, 0xfe0f}, // Variation Select||-1 ..Variation Select||-16
{0xfe20, 0xfe2f}, // Combining Ligature Left ..Combining Cyrillic Titlo
{0x101fd, 0x101fd}, // Phaistos Disc Sign Combi..Phaistos Disc Sign Combi
{0x102e0, 0x102e0}, // Coptic Epact Thousands M..Coptic Epact Thousands M
{0x10376, 0x1037a}, // Combining Old Permic Let..Combining Old Permic Let
{0x10a01, 0x10a03}, // Kharoshthi Vowel Sign I ..Kharoshthi Vowel Sign Vo
{0x10a05, 0x10a06}, // Kharoshthi Vowel Sign E ..Kharoshthi Vowel Sign O
{0x10a0c, 0x10a0f}, // Kharoshthi Vowel Length ..Kharoshthi Sign Visarga
{0x10a38, 0x10a3a}, // Kharoshthi Sign Bar Abov..Kharoshthi Sign Dot Belo
{0x10a3f, 0x10a3f}, // Kharoshthi Virama ..Kharoshthi Virama
{0x10ae5, 0x10ae6}, // Manichaean Abbreviation ..Manichaean Abbreviation
{0x11001, 0x11001}, // Brahmi Sign Anusvara ..Brahmi Sign Anusvara
{0x11038, 0x11046}, // Brahmi Vowel Sign Aa ..Brahmi Virama
{0x1107f, 0x11081}, // Brahmi Number Joiner ..Kaithi Sign Anusvara
{0x110b3, 0x110b6}, // Kaithi Vowel Sign U ..Kaithi Vowel Sign Ai
{0x110b9, 0x110ba}, // Kaithi Sign Virama ..Kaithi Sign Nukta
{0x11100, 0x11102}, // Chakma Sign Candrabindu ..Chakma Sign Visarga
{0x11127, 0x1112b}, // Chakma Vowel Sign A ..Chakma Vowel Sign Uu
{0x1112d, 0x11134}, // Chakma Vowel Sign Ai ..Chakma Maayyaa
{0x11173, 0x11173}, // Mahajani Sign Nukta ..Mahajani Sign Nukta
{0x11180, 0x11181}, // Sharada Sign Candrabindu..Sharada Sign Anusvara
{0x111b6, 0x111be}, // Sharada Vowel Sign U ..Sharada Vowel Sign O
{0x111ca, 0x111cc}, // Sharada Sign Nukta ..Sharada Extra Sh||t Vowe
{0x1122f, 0x11231}, // Khojki Vowel Sign U ..Khojki Vowel Sign Ai
{0x11234, 0x11234}, // Khojki Sign Anusvara ..Khojki Sign Anusvara
{0x11236, 0x11237}, // Khojki Sign Nukta ..Khojki Sign Shadda
{0x1123e, 0x1123e}, // (nil) ..
{0x112df, 0x112df}, // Khudawadi Sign Anusvara ..Khudawadi Sign Anusvara
{0x112e3, 0x112ea}, // Khudawadi Vowel Sign U ..Khudawadi Sign Virama
{0x11300, 0x11301}, // Grantha Sign Combining A..Grantha Sign Candrabindu
{0x1133c, 0x1133c}, // Grantha Sign Nukta ..Grantha Sign Nukta
{0x11340, 0x11340}, // Grantha Vowel Sign Ii ..Grantha Vowel Sign Ii
{0x11366, 0x1136c}, // Combining Grantha Digit ..Combining Grantha Digit
{0x11370, 0x11374}, // Combining Grantha Letter..Combining Grantha Letter
{0x11438, 0x1143f}, // (nil) ..
{0x11442, 0x11444}, // (nil) ..
{0x11446, 0x11446}, // (nil) ..
{0x114b3, 0x114b8}, // Tirhuta Vowel Sign U ..Tirhuta Vowel Sign Vocal
{0x114ba, 0x114ba}, // Tirhuta Vowel Sign Sh||t..Tirhuta Vowel Sign Sh||t
{0x114bf, 0x114c0}, // Tirhuta Sign Candrabindu..Tirhuta Sign Anusvara
{0x114c2, 0x114c3}, // Tirhuta Sign Virama ..Tirhuta Sign Nukta
{0x115b2, 0x115b5}, // Siddham Vowel Sign U ..Siddham Vowel Sign Vocal
{0x115bc, 0x115bd}, // Siddham Sign Candrabindu..Siddham Sign Anusvara
{0x115bf, 0x115c0}, // Siddham Sign Virama ..Siddham Sign Nukta
{0x115dc, 0x115dd}, // Siddham Vowel Sign Alter..Siddham Vowel Sign Alter
{0x11633, 0x1163a}, // Modi Vowel Sign U ..Modi Vowel Sign Ai
{0x1163d, 0x1163d}, // Modi Sign Anusvara ..Modi Sign Anusvara
{0x1163f, 0x11640}, // Modi Sign Virama ..Modi Sign Ardhacandra
{0x116ab, 0x116ab}, // Takri Sign Anusvara ..Takri Sign Anusvara
{0x116ad, 0x116ad}, // Takri Vowel Sign Aa ..Takri Vowel Sign Aa
{0x116b0, 0x116b5}, // Takri Vowel Sign U ..Takri Vowel Sign Au
{0x116b7, 0x116b7}, // Takri Sign Nukta ..Takri Sign Nukta
{0x1171d, 0x1171f}, // Ahom Consonant Sign Medi..Ahom Consonant Sign Medi
{0x11722, 0x11725}, // Ahom Vowel Sign I ..Ahom Vowel Sign Uu
{0x11727, 0x1172b}, // Ahom Vowel Sign Aw ..Ahom Sign Killer
{0x11c30, 0x11c36}, // (nil) ..
{0x11c38, 0x11c3d}, // (nil) ..
{0x11c3f, 0x11c3f}, // (nil) ..
{0x11c92, 0x11ca7}, // (nil) ..
{0x11caa, 0x11cb0}, // (nil) ..
{0x11cb2, 0x11cb3}, // (nil) ..
{0x11cb5, 0x11cb6}, // (nil) ..
{0x16af0, 0x16af4}, // Bassa Vah Combining High..Bassa Vah Combining High
{0x16b30, 0x16b36}, // Pahawh Hmong Mark Cim Tu..Pahawh Hmong Mark Cim Ta
{0x16f8f, 0x16f92}, // Miao Tone Right ..Miao Tone Below
{0x1bc9d, 0x1bc9e}, // Duployan Thick Letter Se..Duployan Double Mark
{0x1d167, 0x1d169}, // Musical Symbol Combining..Musical Symbol Combining
{0x1d17b, 0x1d182}, // Musical Symbol Combining..Musical Symbol Combining
{0x1d185, 0x1d18b}, // Musical Symbol Combining..Musical Symbol Combining
{0x1d1aa, 0x1d1ad}, // Musical Symbol Combining..Musical Symbol Combining
{0x1d242, 0x1d244}, // Combining Greek Musical ..Combining Greek Musical
{0x1da00, 0x1da36}, // Signwriting Head Rim ..Signwriting Air Sucking
{0x1da3b, 0x1da6c}, // Signwriting Mouth Closed..Signwriting Excitement
{0x1da75, 0x1da75}, // Signwriting Upper Body T..Signwriting Upper Body T
{0x1da84, 0x1da84}, // Signwriting Location Hea..Signwriting Location Hea
{0x1da9b, 0x1da9f}, // Signwriting Fill Modifie..Signwriting Fill Modifie
{0x1daa1, 0x1daaf}, // Signwriting Rotation Mod..Signwriting Rotation Mod
{0x1e000, 0x1e006}, // (nil) ..
{0x1e008, 0x1e018}, // (nil) ..
{0x1e01b, 0x1e021}, // (nil) ..
{0x1e023, 0x1e024}, // (nil) ..
{0x1e026, 0x1e02a}, // (nil) ..
{0x1e8d0, 0x1e8d6}, // Mende Kikakui Combining ..Mende Kikakui Combining
{0x1e944, 0x1e94a}, // (nil) ..
{0xe0100, 0xe01ef}, // Variation Select||-17 ..Variation Select||-256
};
private static final short wtable[] = { 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 18, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, // https://github.com/jquast/wcwidth/blob/master/wcwidth/table_wide.py
16, 16, 16, 16, 16, 16, 19, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 20, 21, 22, 23, 24, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, // at commit 0d7de112202cc8b2ebe9232ff4a5c954f19d561a (2016-07-02):
17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 25, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, private static final int[][] WIDE_EASTASIAN = {
17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, {0x1100, 0x115f}, // Hangul Choseong Kiyeok ..Hangul Choseong Filler
17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 26, 16, 16, 16, 16, 27, 16, 16, 17, 17, 17, 17, 17, {0x231a, 0x231b}, // Watch ..Hourglass
17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, {0x2329, 0x232a}, // Left-pointing Angle Brac..Right-pointing Angle Bra
17, 28, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 17, 17, {0x23e9, 0x23ec}, // Black Right-pointing Dou..Black Down-pointing Doub
16, 16, 16, 29, 30, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, {0x23f0, 0x23f0}, // Alarm Clock ..Alarm Clock
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, {0x23f3, 0x23f3}, // Hourglass With Flowing S..Hourglass With Flowing S
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, {0x25fd, 0x25fe}, // White Medium Small Squar..Black Medium Small Squar
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, {0x2614, 0x2615}, // Umbrella With Rain Drops..Hot Beverage
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 31, 16, 16, 16, {0x2648, 0x2653}, // Aries ..Pisces
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, {0x267f, 0x267f}, // Wheelchair Symbol ..Wheelchair Symbol
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 32, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, {0x2693, 0x2693}, // Anch|| ..Anch||
16, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, {0x26a1, 0x26a1}, // High Voltage Sign ..High Voltage Sign
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, {0x26aa, 0x26ab}, // Medium White Circle ..Medium Black Circle
255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 248, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 252, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0x26bd, 0x26be}, // Soccer Ball ..Baseball
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 251, 255, 255, 255, 255, 255, 255, {0x26c4, 0x26c5}, // Snowman Without Snow ..Sun Behind Cloud
255, 255, 255, 255, 15, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, {0x26ce, 0x26ce}, // Ophiuchus ..Ophiuchus
255, 255, 63, 0, 0, 0, 255, 15, 255, 255, 255, 255, 255, 255, 255, 127, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127, 254, 255, 255, 255, {0x26d4, 0x26d4}, // No Entry ..No Entry
255, 255, 255, 255, 255, 255, 255, 255, 255, 224, 255, 255, 255, 255, 63, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127, 255, 255, {0x26ea, 0x26ea}, // Church ..Church
255, 255, 255, 7, 255, 255, 255, 255, 15, 0, 255, 255, 255, 255, 255, 127, 255, 255, 255, 255, 255, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, {0x26f2, 0x26f3}, // Fountain ..Flag In Hole
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, {0x26f5, 0x26f5}, // Sailboat ..Sailboat
255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, {0x26fa, 0x26fa}, // Tent ..Tent
255, 31, 255, 255, 255, 255, 255, 255, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 31, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0x26fd, 0x26fd}, // Fuel Pump ..Fuel Pump
0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 15, 0, 255, 255, 127, 248, {0x2705, 0x2705}, // White Heavy Check Mark ..White Heavy Check Mark
255, 255, 255, 255, 255, 15, 0, 0, 255, 3, 0, 0, 255, 255, 255, 255, 247, 255, 127, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 254, {0x270a, 0x270b}, // Raised Fist ..Raised Hand
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 127, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0x2728, 0x2728}, // Sparkles ..Sparkles
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 255, 255, 255, 255, 255, 7, 255, 1, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0x274c, 0x274c}, // Cross Mark ..Cross Mark
0, 0, 0, 0, 0, 0, 0, 0, 0 }; {0x274e, 0x274e}, // Negative Squared Cross M..Negative Squared Cross M
{0x2753, 0x2755}, // Black Question Mark ||na..White Exclamation Mark O
{0x2757, 0x2757}, // Heavy Exclamation Mark S..Heavy Exclamation Mark S
{0x2795, 0x2797}, // Heavy Plus Sign ..Heavy Division Sign
{0x27b0, 0x27b0}, // Curly Loop ..Curly Loop
{0x27bf, 0x27bf}, // Double Curly Loop ..Double Curly Loop
{0x2b1b, 0x2b1c}, // Black Large Square ..White Large Square
{0x2b50, 0x2b50}, // White Medium Star ..White Medium Star
{0x2b55, 0x2b55}, // Heavy Large Circle ..Heavy Large Circle
{0x2e80, 0x2e99}, // Cjk Radical Repeat ..Cjk Radical Rap
{0x2e9b, 0x2ef3}, // Cjk Radical Choke ..Cjk Radical C-simplified
{0x2f00, 0x2fd5}, // Kangxi Radical One ..Kangxi Radical Flute
{0x2ff0, 0x2ffb}, // Ideographic Description ..Ideographic Description
{0x3000, 0x303e}, // Ideographic Space ..Ideographic Variation In
{0x3041, 0x3096}, // Hiragana Letter Small A ..Hiragana Letter Small Ke
{0x3099, 0x30ff}, // Combining Katakana-hirag..Katakana Digraph Koto
{0x3105, 0x312d}, // Bopomofo Letter B ..Bopomofo Letter Ih
{0x3131, 0x318e}, // Hangul Letter Kiyeok ..Hangul Letter Araeae
{0x3190, 0x31ba}, // Ideographic Annotation L..Bopomofo Letter Zy
{0x31c0, 0x31e3}, // Cjk Stroke T ..Cjk Stroke Q
{0x31f0, 0x321e}, // Katakana Letter Small Ku..Parenthesized K||ean Cha
{0x3220, 0x3247}, // Parenthesized Ideograph ..Circled Ideograph Koto
{0x3250, 0x32fe}, // Partnership Sign ..Circled Katakana Wo
{0x3300, 0x4dbf}, // Square Apaato ..
{0x4e00, 0xa48c}, // Cjk Unified Ideograph-4e..Yi Syllable Yyr
{0xa490, 0xa4c6}, // Yi Radical Qot ..Yi Radical Ke
{0xa960, 0xa97c}, // Hangul Choseong Tikeut-m..Hangul Choseong Ssangyeo
{0xac00, 0xd7a3}, // Hangul Syllable Ga ..Hangul Syllable Hih
{0xf900, 0xfaff}, // Cjk Compatibility Ideogr..
{0xfe10, 0xfe19}, // Presentation F||m F|| Ve..Presentation F||m F|| Ve
{0xfe30, 0xfe52}, // Presentation F||m F|| Ve..Small Full Stop
{0xfe54, 0xfe66}, // Small Semicolon ..Small Equals Sign
{0xfe68, 0xfe6b}, // Small Reverse Solidus ..Small Commercial At
{0xff01, 0xff60}, // Fullwidth Exclamation Ma..Fullwidth Right White Pa
{0xffe0, 0xffe6}, // Fullwidth Cent Sign ..Fullwidth Won Sign
{0x16fe0, 0x16fe0}, // (nil) ..
{0x17000, 0x187ec}, // (nil) ..
{0x18800, 0x18af2}, // (nil) ..
{0x1b000, 0x1b001}, // Katakana Letter Archaic ..Hiragana Letter Archaic
{0x1f004, 0x1f004}, // Mahjong Tile Red Dragon ..Mahjong Tile Red Dragon
{0x1f0cf, 0x1f0cf}, // Playing Card Black Joker..Playing Card Black Joker
{0x1f18e, 0x1f18e}, // Negative Squared Ab ..Negative Squared Ab
{0x1f191, 0x1f19a}, // Squared Cl ..Squared Vs
{0x1f200, 0x1f202}, // Square Hiragana Hoka ..Squared Katakana Sa
{0x1f210, 0x1f23b}, // Squared Cjk Unified Ideo..
{0x1f240, 0x1f248}, // T||toise Shell Bracketed..T||toise Shell Bracketed
{0x1f250, 0x1f251}, // Circled Ideograph Advant..Circled Ideograph Accept
{0x1f300, 0x1f320}, // Cyclone ..Shooting Star
{0x1f32d, 0x1f335}, // Hot Dog ..Cactus
{0x1f337, 0x1f37c}, // Tulip ..Baby Bottle
{0x1f37e, 0x1f393}, // Bottle With Popping C||k..Graduation Cap
{0x1f3a0, 0x1f3ca}, // Carousel H||se ..Swimmer
{0x1f3cf, 0x1f3d3}, // Cricket Bat And Ball ..Table Tennis Paddle And
{0x1f3e0, 0x1f3f0}, // House Building ..European Castle
{0x1f3f4, 0x1f3f4}, // Waving Black Flag ..Waving Black Flag
{0x1f3f8, 0x1f43e}, // Badminton Racquet And Sh..Paw Prints
{0x1f440, 0x1f440}, // Eyes ..Eyes
{0x1f442, 0x1f4fc}, // Ear ..Videocassette
{0x1f4ff, 0x1f53d}, // Prayer Beads ..Down-pointing Small Red
{0x1f54b, 0x1f54e}, // Kaaba ..Men||ah With Nine Branch
{0x1f550, 0x1f567}, // Clock Face One Oclock ..Clock Face Twelve-thirty
{0x1f57a, 0x1f57a}, // (nil) ..
{0x1f595, 0x1f596}, // Reversed Hand With Middl..Raised Hand With Part Be
{0x1f5a4, 0x1f5a4}, // (nil) ..
{0x1f5fb, 0x1f64f}, // Mount Fuji ..Person With Folded Hands
{0x1f680, 0x1f6c5}, // Rocket ..Left Luggage
{0x1f6cc, 0x1f6cc}, // Sleeping Accommodation ..Sleeping Accommodation
{0x1f6d0, 0x1f6d2}, // Place Of W||ship ..
{0x1f6eb, 0x1f6ec}, // Airplane Departure ..Airplane Arriving
{0x1f6f4, 0x1f6f6}, // (nil) ..
{0x1f910, 0x1f91e}, // Zipper-mouth Face ..
{0x1f920, 0x1f927}, // (nil) ..
{0x1f930, 0x1f930}, // (nil) ..
{0x1f933, 0x1f93e}, // (nil) ..
{0x1f940, 0x1f94b}, // (nil) ..
{0x1f950, 0x1f95e}, // (nil) ..
{0x1f980, 0x1f991}, // Crab ..
{0x1f9c0, 0x1f9c0}, // Cheese Wedge ..Cheese Wedge
{0x20000, 0x2fffd}, // Cjk Unified Ideograph-20..
{0x30000, 0x3fffd}, // (nil) ..
};
/** Return the terminal display width of a code point: 0, 1 or 2. */
public static int width(int wc) { private static boolean intable(int[][] table, int c) {
if (wc < 0xff) return (wc + 1 & 0x7f) >= 0x21 ? 1 : (wc != 0) ? 0 : 0; // First quick check f|| Latin1 etc. characters.
if ((wc & 0xfffeffff) < 0xfffe) { if (c < table[0][0]) return false;
if (((table[table[wc >> 8] * 32 + ((wc & 255) >> 3)] >> (wc & 7)) & 1) != 0) return 0;
if (((wtable[wtable[wc >> 8] * 32 + ((wc & 255) >> 3)] >> (wc & 7)) & 1) != 0) return 2; // Binary search in table.
return 1; int bot = 0;
int top = table.length - 1; // (int)(size / sizeof(struct interval) - 1);
while (top >= bot) {
int mid = (bot + top) / 2;
if (table[mid][1] < c) {
bot = mid + 1;
} else if (table[mid][0] > c) {
top = mid - 1;
} else {
return true;
} }
if ((wc & 0xfffe) == 0xfffe) return 0; }
if (wc - 0x20000 < 0x20000) return 2; return false;
if (wc == 0xe0001 || wc - 0xe0020 < 0x5f || wc - 0xe0100 < 0xef) return 0; }
return 1;
/** Return the terminal display width of a code point: 0, 1 || 2. */
public static int width(int ucs) {
if (ucs == 0 ||
ucs == 0x034F ||
(0x200B <= ucs && ucs <= 0x200F) ||
ucs == 0x2028 ||
ucs == 0x2029 ||
(0x202A <= ucs && ucs <= 0x202E) ||
(0x2060 <= ucs && ucs <= 0x2063)) {
return 0;
}
// C0/C1 control characters
// Termux change: Return 0 instead of -1.
if (ucs < 32 || (0x07F <= ucs && ucs < 0x0A0)) return 0;
// combining characters with zero width
if (intable(ZERO_WIDTH, ucs)) return 0;
return intable(WIDE_EASTASIAN, ucs) ? 2 : 1;
} }
/** The width at an index position in a java char array. */ /** The width at an index position in a java char array. */

View File

@@ -1,12 +1,15 @@
package com.termux.view; package com.termux.view;
import android.view.KeyEvent;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.ScaleGestureDetector; import android.view.ScaleGestureDetector;
import com.termux.terminal.TerminalSession;
/** /**
* Input and scale listener which may be set on a {@link TerminalView} through * Input and scale listener which may be set on a {@link TerminalView} through
* {@link TerminalView#setOnKeyListener(TerminalKeyListener)}. * {@link TerminalView#setOnKeyListener(TerminalKeyListener)}.
* * <p/>
* TODO: Rename to TerminalViewClient. * TODO: Rename to TerminalViewClient.
*/ */
public interface TerminalKeyListener { public interface TerminalKeyListener {
@@ -21,4 +24,14 @@ public interface TerminalKeyListener {
void copyModeChanged(boolean copyMode); void copyModeChanged(boolean copyMode);
boolean onKeyDown(int keyCode, KeyEvent e, TerminalSession session);
boolean onKeyUp(int keyCode, KeyEvent e);
boolean readControlKey();
boolean readAltKey();
boolean onCodePoint(int codePoint, boolean ctrlDown, TerminalSession session);
} }

View File

@@ -13,7 +13,7 @@ import com.termux.terminal.WcWidth;
/** /**
* Renderer of a {@link TerminalEmulator} into a {@link Canvas}. * Renderer of a {@link TerminalEmulator} into a {@link Canvas}.
* * <p/>
* Saves font metrics, so needs to be recreated each time the typeface or font size changes. * Saves font metrics, so needs to be recreated each time the typeface or font size changes.
*/ */
final class TerminalRenderer { final class TerminalRenderer {
@@ -54,7 +54,8 @@ final class TerminalRenderer {
} }
/** Render the terminal to a canvas with at a specified row scroll, and an optional rectangular selection. */ /** Render the terminal to a canvas with at a specified row scroll, and an optional rectangular selection. */
public final void render(TerminalEmulator mEmulator, Canvas canvas, int topRow, int selectionY1, int selectionY2, int selectionX1, int selectionX2) { public final void render(TerminalEmulator mEmulator, Canvas canvas, int topRow,
int selectionY1, int selectionY2, int selectionX1, int selectionX2) {
final boolean reverseVideo = mEmulator.isReverseVideo(); final boolean reverseVideo = mEmulator.isReverseVideo();
final int endRow = topRow + mEmulator.mRows; final int endRow = topRow + mEmulator.mRows;
final int columns = mEmulator.mColumns; final int columns = mEmulator.mColumns;
@@ -63,9 +64,10 @@ final class TerminalRenderer {
final boolean cursorVisible = mEmulator.isShowingCursor(); final boolean cursorVisible = mEmulator.isShowingCursor();
final TerminalBuffer screen = mEmulator.getScreen(); final TerminalBuffer screen = mEmulator.getScreen();
final int[] palette = mEmulator.mColors.mCurrentColors; final int[] palette = mEmulator.mColors.mCurrentColors;
final int cursorShape = mEmulator.getCursorStyle();
int fillColor = palette[reverseVideo ? TextStyle.COLOR_INDEX_FOREGROUND : TextStyle.COLOR_INDEX_BACKGROUND]; if (reverseVideo)
canvas.drawColor(fillColor, PorterDuff.Mode.SRC); canvas.drawColor(palette[TextStyle.COLOR_INDEX_FOREGROUND], PorterDuff.Mode.SRC);
float heightOffset = mFontLineSpacingAndAscent; float heightOffset = mFontLineSpacingAndAscent;
for (int row = topRow; row < endRow; row++) { for (int row = topRow; row < endRow; row++) {
@@ -82,7 +84,7 @@ final class TerminalRenderer {
final char[] line = lineObject.mText; final char[] line = lineObject.mText;
final int charsUsedInLine = lineObject.getSpaceUsed(); final int charsUsedInLine = lineObject.getSpaceUsed();
int lastRunStyle = 0; long lastRunStyle = 0;
boolean lastRunInsideCursor = false; boolean lastRunInsideCursor = false;
int lastRunStartColumn = -1; int lastRunStartColumn = -1;
int lastRunStartIndex = 0; int lastRunStartIndex = 0;
@@ -97,7 +99,7 @@ final class TerminalRenderer {
final int codePoint = charIsHighsurrogate ? Character.toCodePoint(charAtIndex, line[currentCharIndex + 1]) : charAtIndex; final int codePoint = charIsHighsurrogate ? Character.toCodePoint(charAtIndex, line[currentCharIndex + 1]) : charAtIndex;
final int codePointWcWidth = WcWidth.width(codePoint); final int codePointWcWidth = WcWidth.width(codePoint);
final boolean insideCursor = (column >= selx1 && column <= selx2) || (cursorX == column || (codePointWcWidth == 2 && cursorX == column + 1)); final boolean insideCursor = (column >= selx1 && column <= selx2) || (cursorX == column || (codePointWcWidth == 2 && cursorX == column + 1));
final int style = lineObject.getStyle(column); final long style = lineObject.getStyle(column);
// Check if the measured text width for this code point is not the same as that expected by wcwidth(). // Check if the measured text width for this code point is not the same as that expected by wcwidth().
// This could happen for some fonts which are not truly monospace, or for more exotic characters such as // This could happen for some fonts which are not truly monospace, or for more exotic characters such as
@@ -113,8 +115,10 @@ final class TerminalRenderer {
} else { } else {
final int columnWidthSinceLastRun = column - lastRunStartColumn; final int columnWidthSinceLastRun = column - lastRunStartColumn;
final int charsSinceLastRun = currentCharIndex - lastRunStartIndex; final int charsSinceLastRun = currentCharIndex - lastRunStartIndex;
drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun, lastRunStartIndex, charsSinceLastRun, int cursorColor = lastRunInsideCursor ? mEmulator.mColors.mCurrentColors[TextStyle.COLOR_INDEX_CURSOR] : 0;
measuredWidthForRun, lastRunInsideCursor, lastRunStyle, reverseVideo); drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun,
lastRunStartIndex, charsSinceLastRun, measuredWidthForRun,
cursorColor, cursorShape, lastRunStyle, reverseVideo);
} }
measuredWidthForRun = 0.f; measuredWidthForRun = 0.f;
lastRunStyle = style; lastRunStyle = style;
@@ -135,40 +139,42 @@ final class TerminalRenderer {
final int columnWidthSinceLastRun = columns - lastRunStartColumn; final int columnWidthSinceLastRun = columns - lastRunStartColumn;
final int charsSinceLastRun = currentCharIndex - lastRunStartIndex; final int charsSinceLastRun = currentCharIndex - lastRunStartIndex;
int cursorColor = lastRunInsideCursor ? mEmulator.mColors.mCurrentColors[TextStyle.COLOR_INDEX_CURSOR] : 0;
drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun, lastRunStartIndex, charsSinceLastRun, drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun, lastRunStartIndex, charsSinceLastRun,
measuredWidthForRun, lastRunInsideCursor, lastRunStyle, reverseVideo); measuredWidthForRun, cursorColor, cursorShape, lastRunStyle, reverseVideo);
} }
} }
/** private void drawTextRun(Canvas canvas, char[] text, int[] palette, float y, int startColumn, int runWidthColumns,
* @param canvas int startCharIndex, int runWidthChars, float mes, int cursor, int cursorStyle,
* the canvas to render on long textStyle, boolean reverseVideo) {
* @param palette
* the color palette to look up colors from textStyle
* @param y
* height offset into the canvas where to render the line: line * {@link #mFontLineSpacing}
* @param startColumn
* the run offset in columns
* @param runWidthColumns
* the run width in columns - this is computed from wcwidth() and may not be what the font measures to
* @param text
* the java char array to render text from
* @param startCharIndex
* index into the text array where to start
* @param runWidthChars
* number of java characters from the text array to render
* @param cursor
* true if rendering a cursor or selection
* @param textStyle
* the background, foreground and effect encoded using {@link TextStyle}
* @param reverseVideo
* if the screen is rendered with the global reverse video flag set
*/
private void drawTextRun(Canvas canvas, char[] text, int[] palette, float y, int startColumn, int runWidthColumns, int startCharIndex, int runWidthChars,
float mes, boolean cursor, int textStyle, boolean reverseVideo) {
int foreColor = TextStyle.decodeForeColor(textStyle); int foreColor = TextStyle.decodeForeColor(textStyle);
int backColor = TextStyle.decodeBackColor(textStyle);
final int effect = TextStyle.decodeEffect(textStyle); final int effect = TextStyle.decodeEffect(textStyle);
int backColor = TextStyle.decodeBackColor(textStyle);
final boolean bold = (effect & (TextStyle.CHARACTER_ATTRIBUTE_BOLD | TextStyle.CHARACTER_ATTRIBUTE_BLINK)) != 0;
final boolean underline = (effect & TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE) != 0;
final boolean italic = (effect & TextStyle.CHARACTER_ATTRIBUTE_ITALIC) != 0;
final boolean strikeThrough = (effect & TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH) != 0;
final boolean dim = (effect & TextStyle.CHARACTER_ATTRIBUTE_DIM) != 0;
if ((foreColor & 0xff000000) != 0xff000000) {
// Let bold have bright colors if applicable (one of the first 8):
if (bold && foreColor >= 0 && foreColor < 8) foreColor += 8;
foreColor = palette[foreColor];
}
if ((backColor & 0xff000000) != 0xff000000) {
backColor = palette[backColor];
}
// Reverse video here if _one and only one_ of the reverse flags are set:
final boolean reverseVideoHere = reverseVideo ^ (effect & (TextStyle.CHARACTER_ATTRIBUTE_INVERSE)) != 0;
if (reverseVideoHere) {
int tmp = foreColor;
foreColor = backColor;
backColor = tmp;
}
float left = startColumn * mFontWidth; float left = startColumn * mFontWidth;
float right = left + runWidthColumns * mFontWidth; float right = left + runWidthColumns * mFontWidth;
@@ -182,50 +188,38 @@ final class TerminalRenderer {
savedMatrix = true; savedMatrix = true;
} }
// Reverse video here if _one and only one_ of the reverse flags are set: if (backColor != palette[TextStyle.COLOR_INDEX_BACKGROUND]) {
boolean reverseVideoHere = reverseVideo ^ (effect & (TextStyle.CHARACTER_ATTRIBUTE_INVERSE)) != 0;
// Switch if _one and only one_ of reverse video and cursor is set:
if (reverseVideoHere ^ cursor) {
int tmp = foreColor;
foreColor = backColor;
backColor = tmp;
}
if (backColor != TextStyle.COLOR_INDEX_BACKGROUND) {
// Only draw non-default background. // Only draw non-default background.
mTextPaint.setColor(palette[backColor]); mTextPaint.setColor(backColor);
canvas.drawRect(left, y - mFontLineSpacingAndAscent + mFontAscent, right, y, mTextPaint); canvas.drawRect(left, y - mFontLineSpacingAndAscent + mFontAscent, right, y, mTextPaint);
} }
if (cursor != 0) {
mTextPaint.setColor(cursor);
float cursorHeight = mFontLineSpacingAndAscent - mFontAscent;
if (cursorStyle == TerminalEmulator.CURSOR_STYLE_UNDERLINE) cursorHeight /= 4.;
else if (cursorStyle == TerminalEmulator.CURSOR_STYLE_BAR) right -= ((right - left) * 3) / 4.;
canvas.drawRect(left, y - cursorHeight, right, y, mTextPaint);
}
if ((effect & TextStyle.CHARACTER_ATTRIBUTE_INVISIBLE) == 0) { if ((effect & TextStyle.CHARACTER_ATTRIBUTE_INVISIBLE) == 0) {
// Treat blink as bold:
final boolean bold = (effect & (TextStyle.CHARACTER_ATTRIBUTE_BOLD | TextStyle.CHARACTER_ATTRIBUTE_BLINK)) != 0;
final boolean underline = (effect & TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE) != 0;
final boolean italic = (effect & TextStyle.CHARACTER_ATTRIBUTE_ITALIC) != 0;
final boolean strikeThrough = (effect & TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH) != 0;
final boolean dim = (effect & TextStyle.CHARACTER_ATTRIBUTE_DIM) != 0;
// Let bold have bright colors if applicable (one of the first 8):
final int actualForeColor = foreColor + (bold && foreColor < 8 ? 8 : 0);
int foreColorARGB = palette[actualForeColor];
if (dim) { if (dim) {
int red = (0xFF & (foreColorARGB >> 16)); int red = (0xFF & (foreColor >> 16));
int green = (0xFF & (foreColorARGB >> 8)); int green = (0xFF & (foreColor >> 8));
int blue = (0xFF & foreColorARGB); int blue = (0xFF & foreColor);
// Dim color handling used by libvte which in turn took it from xterm // Dim color handling used by libvte which in turn took it from xterm
// (https://bug735245.bugzilla-attachments.gnome.org/attachment.cgi?id=284267): // (https://bug735245.bugzilla-attachments.gnome.org/attachment.cgi?id=284267):
red = red * 2 / 3; red = red * 2 / 3;
green = green * 2 / 3; green = green * 2 / 3;
blue = blue * 2 / 3; blue = blue * 2 / 3;
foreColorARGB = 0xFF000000 + (red << 16) + (green << 8) + blue; foreColor = 0xFF000000 + (red << 16) + (green << 8) + blue;
} }
mTextPaint.setFakeBoldText(bold); mTextPaint.setFakeBoldText(bold);
mTextPaint.setUnderlineText(underline); mTextPaint.setUnderlineText(underline);
mTextPaint.setTextSkewX(italic ? -0.35f : 0.f); mTextPaint.setTextSkewX(italic ? -0.35f : 0.f);
mTextPaint.setStrikeThruText(strikeThrough); mTextPaint.setStrikeThruText(strikeThrough);
mTextPaint.setColor(foreColorARGB); mTextPaint.setColor(foreColor);
// The text alignment is the default Paint.Align.LEFT. // The text alignment is the default Paint.Align.LEFT.
canvas.drawText(text, startCharIndex, runWidthChars, left, y - mFontLineSpacingAndAscent, mTextPaint); canvas.drawText(text, startCharIndex, runWidthChars, left, y - mFontLineSpacingAndAscent, mTextPaint);

View File

@@ -5,13 +5,12 @@ import android.annotation.TargetApi;
import android.content.ClipData; import android.content.ClipData;
import android.content.ClipboardManager; import android.content.ClipboardManager;
import android.content.Context; import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.Typeface; import android.graphics.Typeface;
import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.BitmapDrawable;
import android.media.AudioManager;
import android.os.Build; import android.os.Build;
import android.text.Editable;
import android.text.InputType; import android.text.InputType;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.AttributeSet; import android.util.AttributeSet;
@@ -34,15 +33,9 @@ import com.termux.R;
import com.termux.terminal.EmulatorDebug; import com.termux.terminal.EmulatorDebug;
import com.termux.terminal.KeyHandler; import com.termux.terminal.KeyHandler;
import com.termux.terminal.TerminalBuffer; import com.termux.terminal.TerminalBuffer;
import com.termux.terminal.TerminalColors;
import com.termux.terminal.TerminalEmulator; import com.termux.terminal.TerminalEmulator;
import com.termux.terminal.TerminalSession; import com.termux.terminal.TerminalSession;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.Properties;
/** View displaying and interacting with a {@link TerminalSession}. */ /** View displaying and interacting with a {@link TerminalSession}. */
public final class TerminalView extends View { public final class TerminalView extends View {
@@ -61,9 +54,6 @@ public final class TerminalView extends View {
/** The top row of text to display. Ranges from -activeTranscriptRows to 0. */ /** The top row of text to display. Ranges from -activeTranscriptRows to 0. */
int mTopRow; int mTopRow;
/** Keeping track of the special keys acting as Ctrl and Fn for the soft keyboard and other hardware keys. */
boolean mVirtualControlKeyDown, mVirtualFnKeyDown;
boolean mIsSelectingText = false, mIsDraggingLeftSelection, mInitialTextSelection; boolean mIsSelectingText = false, mIsDraggingLeftSelection, mInitialTextSelection;
int mSelX1 = -1, mSelX2 = -1, mSelY1 = -1, mSelY2 = -1; int mSelX1 = -1, mSelX2 = -1, mSelY1 = -1, mSelY2 = -1;
float mSelectionDownX, mSelectionDownY; float mSelectionDownX, mSelectionDownY;
@@ -90,23 +80,29 @@ public final class TerminalView extends View {
super(context, attributes); super(context, attributes);
mGestureRecognizer = new GestureAndScaleRecognizer(context, new GestureAndScaleRecognizer.Listener() { mGestureRecognizer = new GestureAndScaleRecognizer(context, new GestureAndScaleRecognizer.Listener() {
boolean scrolledWithFinger;
@Override @Override
public boolean onUp(MotionEvent e) { public boolean onUp(MotionEvent e) {
mScrollRemainder = 0.0f; mScrollRemainder = 0.0f;
if (mEmulator != null && mEmulator.isMouseTrackingActive() && !mIsSelectingText) { if (mEmulator != null && mEmulator.isMouseTrackingActive() && !mIsSelectingText && !scrolledWithFinger) {
// Quick event processing when mouse tracking is active - do not wait for check of double tapping // Quick event processing when mouse tracking is active - do not wait for check of double tapping
// for zooming. // for zooming.
sendMouseEventCode(e, TerminalEmulator.MOUSE_LEFT_BUTTON, true); sendMouseEventCode(e, TerminalEmulator.MOUSE_LEFT_BUTTON, true);
sendMouseEventCode(e, TerminalEmulator.MOUSE_LEFT_BUTTON, false); sendMouseEventCode(e, TerminalEmulator.MOUSE_LEFT_BUTTON, false);
return true; return true;
} }
scrolledWithFinger = false;
return false; return false;
} }
@Override @Override
public boolean onSingleTapUp(MotionEvent e) { public boolean onSingleTapUp(MotionEvent e) {
if (mEmulator == null) return true; if (mEmulator == null) return true;
if (mIsSelectingText) { toggleSelectingText(null); return true; } if (mIsSelectingText) {
toggleSelectingText(null);
return true;
}
requestFocus(); requestFocus();
if (!mEmulator.isMouseTrackingActive()) { if (!mEmulator.isMouseTrackingActive()) {
if (!e.isFromSource(InputDevice.SOURCE_MOUSE)) { if (!e.isFromSource(InputDevice.SOURCE_MOUSE)) {
@@ -118,19 +114,20 @@ public final class TerminalView extends View {
} }
@Override @Override
public boolean onScroll(MotionEvent e2, float distanceX, float distanceY) { public boolean onScroll(MotionEvent e, float distanceX, float distanceY) {
if (mEmulator == null || mIsSelectingText) return true; if (mEmulator == null || mIsSelectingText) return true;
if (mEmulator.isMouseTrackingActive() && e2.isFromSource(InputDevice.SOURCE_MOUSE)) { if (mEmulator.isMouseTrackingActive() && e.isFromSource(InputDevice.SOURCE_MOUSE)) {
// If moving with mouse pointer while pressing button, report that instead of scroll. // If moving with mouse pointer while pressing button, report that instead of scroll.
// This means that we never report moving with button press-events for touch input, // This means that we never report moving with button press-events for touch input,
// since we cannot just start sending these events without a starting press event, // since we cannot just start sending these events without a starting press event,
// which we do not do for touch input, only mouse in onTouchEvent(). // which we do not do for touch input, only mouse in onTouchEvent().
sendMouseEventCode(e2, TerminalEmulator.MOUSE_LEFT_BUTTON_MOVED, true); sendMouseEventCode(e, TerminalEmulator.MOUSE_LEFT_BUTTON_MOVED, true);
} else { } else {
scrolledWithFinger = true;
distanceY += mScrollRemainder; distanceY += mScrollRemainder;
int deltaRows = (int) (distanceY / mRenderer.mFontLineSpacing); int deltaRows = (int) (distanceY / mRenderer.mFontLineSpacing);
mScrollRemainder = distanceY - deltaRows * mRenderer.mFontLineSpacing; mScrollRemainder = distanceY - deltaRows * mRenderer.mFontLineSpacing;
doScroll(e2, deltaRows); doScroll(e, deltaRows);
} }
return true; return true;
} }
@@ -202,8 +199,7 @@ public final class TerminalView extends View {
} }
/** /**
* @param onKeyListener * @param onKeyListener Listener for all kinds of key events, both hardware and IME (which makes it different from that
* Listener for all kinds of key events, both hardware and IME (which makes it different from that
* available with {@link View#setOnKeyListener(OnKeyListener)}. * available with {@link View#setOnKeyListener(OnKeyListener)}.
*/ */
public void setOnKeyListener(TerminalKeyListener onKeyListener) { public void setOnKeyListener(TerminalKeyListener onKeyListener) {
@@ -213,8 +209,7 @@ public final class TerminalView extends View {
/** /**
* Attach a {@link TerminalSession} to this view. * Attach a {@link TerminalSession} to this view.
* *
* @param session * @param session The {@link TerminalSession} this view will be displaying.
* The {@link TerminalSession} this view will be displaying.
*/ */
public boolean attachSession(TerminalSession session) { public boolean attachSession(TerminalSession session) {
if (session == mTermSession) return false; if (session == mTermSession) return false;
@@ -234,72 +229,58 @@ public final class TerminalView extends View {
@Override @Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) { public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
// Make the IME run in a limited "generate key events" mode. // Using InputType.NULL is the most correct input type and avoids issues with other hacks.
// //
// If using just "TYPE_NULL", there is a problem with the "Google Pinyin Input" being in // Previous keyboard issues:
// word mode when used with the "En" tab available when the "Show English keyboard" option // https://github.com/termux/termux-packages/issues/25
// is enabled - see https://github.com/termux/termux-packages/issues/25. // https://github.com/termux/termux-app/issues/87.
// // https://github.com/termux/termux-app/issues/126.
// Adding TYPE_TEXT_FLAG_NO_SUGGESTIONS fixes Pinyin Input, put causes Swype to be put in // https://github.com/termux/termux-app/issues/137 (japanese chars and TYPE_NULL).
// word mode... Using TYPE_TEXT_VARIATION_VISIBLE_PASSWORD fixes that. outAttrs.inputType = InputType.TYPE_NULL;
//
// So a bit messy. If this gets too messy it's perhaps best resolved by reverting back to just
// "TYPE_NULL" and let the Pinyin Input english keyboard be in word mode.
outAttrs.inputType = InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD;
// Let part of the application show behind when in landscape: outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_FULLSCREEN |
outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_FULLSCREEN; EditorInfo.IME_FLAG_NO_ENTER_ACTION |
EditorInfo.IME_ACTION_NONE;
return new BaseInputConnection(this, true) { return new BaseInputConnection(this, true) {
@Override
public boolean beginBatchEdit() {
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "IME: beginBatchEdit()");
return true;
}
@Override
public boolean clearMetaKeyStates(int states) {
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "IME: clearMetaKeyStates(" + states + ")");
return true;
}
@Override
public boolean endBatchEdit() {
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "IME: endBatchEdit()");
return false;
}
@Override @Override
public boolean finishComposingText() { public boolean finishComposingText() {
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "IME: finishComposingText()"); if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "IME: finishComposingText()");
super.finishComposingText();
sendTextToTerminal(getEditable());
getEditable().clear();
return true; return true;
} }
@Override
public int getCursorCapsMode(int reqModes) {
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "IME: getCursorCapsMode(" + reqModes + ")");
int mode = 0;
if ((reqModes & TextUtils.CAP_MODE_CHARACTERS) != 0) {
mode |= TextUtils.CAP_MODE_CHARACTERS;
}
return mode;
}
@Override
public CharSequence getTextAfterCursor(int n, int flags) {
return "";
}
@Override
public CharSequence getTextBeforeCursor(int n, int flags) {
return "";
}
@Override @Override
public boolean commitText(CharSequence text, int newCursorPosition) { public boolean commitText(CharSequence text, int newCursorPosition) {
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "IME: commitText(\"" + text + "\", " + newCursorPosition + ")"); if (LOG_KEY_EVENTS) {
Log.i(EmulatorDebug.LOG_TAG, "IME: commitText(\"" + text + "\", " + newCursorPosition + ")");
}
super.commitText(text, newCursorPosition);
if (mEmulator == null) return true; if (mEmulator == null) return true;
Editable content = getEditable();
sendTextToTerminal(content);
content.clear();
return true;
}
@Override
public boolean deleteSurroundingText(int leftLength, int rightLength) {
if (LOG_KEY_EVENTS) {
Log.i(EmulatorDebug.LOG_TAG, "IME: deleteSurroundingText(" + leftLength + ", " + rightLength + ")");
}
// The stock Samsung keyboard with 'Auto check spelling' enabled sends leftLength > 1.
KeyEvent deleteKey = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL);
for (int i = 0; i < leftLength; i++) sendKeyEvent(deleteKey);
return super.deleteSurroundingText(leftLength, rightLength);
}
void sendTextToTerminal(CharSequence text) {
final int textLengthInChars = text.length(); final int textLengthInChars = text.length();
for (int i = 0; i < textLengthInChars; i++) { for (int i = 0; i < textLengthInChars; i++) {
char firstChar = text.charAt(i); char firstChar = text.charAt(i);
@@ -314,21 +295,32 @@ public final class TerminalView extends View {
} else { } else {
codePoint = firstChar; codePoint = firstChar;
} }
inputCodePoint(codePoint, false, false);
boolean ctrlHeld = false;
if (codePoint <= 31 && codePoint != 27) {
// E.g. penti keyboard for ctrl input.
ctrlHeld = true;
switch (codePoint) {
case 31:
codePoint = '_';
break;
case 30:
codePoint = '^';
break;
case 29:
codePoint = ']';
break;
case 28:
codePoint = '\\';
break;
default:
codePoint += 96;
break;
} }
return true;
} }
@Override inputCodePoint(codePoint, ctrlHeld, false);
public boolean deleteSurroundingText(int leftLength, int rightLength) { }
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "IME: deleteSurroundingText(" + leftLength + ", " + rightLength + ")");
// Swype keyboard sometimes(?) sends this on backspace:
if (leftLength == 0 && rightLength == 0) leftLength = 1;
for (int i = 0; i < leftLength; i++)
sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));
return true;
} }
}; };
@@ -388,14 +380,19 @@ public final class TerminalView extends View {
/** /**
* Sets the text size, which in turn sets the number of rows and columns. * Sets the text size, which in turn sets the number of rows and columns.
* *
* @param textSize * @param textSize the new font size, in density-independent pixels.
* the new font size, in density-independent pixels.
*/ */
public void setTextSize(int textSize) { public void setTextSize(int textSize) {
mRenderer = new TerminalRenderer(textSize, mRenderer == null ? Typeface.MONOSPACE : mRenderer.mTypeface); mRenderer = new TerminalRenderer(textSize, mRenderer == null ? Typeface.MONOSPACE : mRenderer.mTypeface);
updateSize(); updateSize();
} }
public void setTypeface(Typeface newTypeface) {
mRenderer = new TerminalRenderer(mRenderer.mTextSize, newTypeface);
updateSize();
invalidate();
}
@Override @Override
public boolean onCheckIsTextEditor() { public boolean onCheckIsTextEditor() {
return true; return true;
@@ -498,11 +495,14 @@ public final class TerminalView extends View {
// Switch handles. // Switch handles.
mIsDraggingLeftSelection = !mIsDraggingLeftSelection; mIsDraggingLeftSelection = !mIsDraggingLeftSelection;
int tmpX1 = mSelX1, tmpY1 = mSelY1; int tmpX1 = mSelX1, tmpY1 = mSelY1;
mSelX1 = mSelX2; mSelY1 = mSelY2; mSelX1 = mSelX2;
mSelX2 = tmpX1; mSelY2 = tmpY1; mSelY1 = mSelY2;
mSelX2 = tmpX1;
mSelY2 = tmpY1;
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) mActionMode.invalidateContentRect(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
mActionMode.invalidateContentRect();
invalidate(); invalidate();
break; break;
default: default:
@@ -541,7 +541,8 @@ public final class TerminalView extends View {
@Override @Override
public boolean onKeyPreIme(int keyCode, KeyEvent event) { public boolean onKeyPreIme(int keyCode, KeyEvent event) {
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "onKeyPreIme(keyCode=" + keyCode + ", event=" + event + ")"); if (LOG_KEY_EVENTS)
Log.i(EmulatorDebug.LOG_TAG, "onKeyPreIme(keyCode=" + keyCode + ", event=" + event + ")");
if (keyCode == KeyEvent.KEYCODE_BACK) { if (keyCode == KeyEvent.KEYCODE_BACK) {
if (mIsSelectingText) { if (mIsSelectingText) {
toggleSelectingText(null); toggleSelectingText(null);
@@ -561,21 +562,25 @@ public final class TerminalView extends View {
@Override @Override
public boolean onKeyDown(int keyCode, KeyEvent event) { public boolean onKeyDown(int keyCode, KeyEvent event) {
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "onKeyDown(keyCode=" + keyCode + ", isSystem()=" + event.isSystem() + ", event=" + event + ")"); if (LOG_KEY_EVENTS)
Log.i(EmulatorDebug.LOG_TAG, "onKeyDown(keyCode=" + keyCode + ", isSystem()=" + event.isSystem() + ", event=" + event + ")");
if (mEmulator == null) return true; if (mEmulator == null) return true;
int metaState = event.getMetaState(); if (mOnKeyListener.onKeyDown(keyCode, event, mTermSession)) {
boolean controlDownFromEvent = event.isCtrlPressed();
boolean leftAltDownFromEvent = (metaState & KeyEvent.META_ALT_LEFT_ON) != 0;
boolean rightAltDownFromEvent = (metaState & KeyEvent.META_ALT_RIGHT_ON) != 0;
if (handleVirtualKeys(keyCode, event, true)) {
invalidate(); invalidate();
return true; return true;
} else if (event.isSystem() && (!mOnKeyListener.shouldBackButtonBeMappedToEscape() || keyCode != KeyEvent.KEYCODE_BACK)) { } else if (event.isSystem() && (!mOnKeyListener.shouldBackButtonBeMappedToEscape() || keyCode != KeyEvent.KEYCODE_BACK)) {
return super.onKeyDown(keyCode, event); return super.onKeyDown(keyCode, event);
} else if (event.getAction() == KeyEvent.ACTION_MULTIPLE && keyCode == KeyEvent.KEYCODE_UNKNOWN) {
mTermSession.write(event.getCharacters());
return true;
} }
final int metaState = event.getMetaState();
final boolean controlDownFromEvent = event.isCtrlPressed();
final boolean leftAltDownFromEvent = (metaState & KeyEvent.META_ALT_LEFT_ON) != 0;
final boolean rightAltDownFromEvent = (metaState & KeyEvent.META_ALT_RIGHT_ON) != 0;
int keyMod = 0; int keyMod = 0;
if (controlDownFromEvent) keyMod |= KeyHandler.KEYMOD_CTRL; if (controlDownFromEvent) keyMod |= KeyHandler.KEYMOD_CTRL;
if (event.isAltPressed()) keyMod |= KeyHandler.KEYMOD_ALT; if (event.isAltPressed()) keyMod |= KeyHandler.KEYMOD_ALT;
@@ -596,7 +601,8 @@ public final class TerminalView extends View {
int effectiveMetaState = event.getMetaState() & ~bitsToClear; int effectiveMetaState = event.getMetaState() & ~bitsToClear;
int result = event.getUnicodeChar(effectiveMetaState); int result = event.getUnicodeChar(effectiveMetaState);
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "KeyEvent#getUnicodeChar(" + effectiveMetaState + ") returned: " + result); if (LOG_KEY_EVENTS)
Log.i(EmulatorDebug.LOG_TAG, "KeyEvent#getUnicodeChar(" + effectiveMetaState + ") returned: " + result);
if (result == 0) { if (result == 0) {
return true; return true;
} }
@@ -604,7 +610,8 @@ public final class TerminalView extends View {
int oldCombiningAccent = mCombiningAccent; int oldCombiningAccent = mCombiningAccent;
if ((result & KeyCharacterMap.COMBINING_ACCENT) != 0) { if ((result & KeyCharacterMap.COMBINING_ACCENT) != 0) {
// If entered combining accent previously, write it out: // If entered combining accent previously, write it out:
if (mCombiningAccent != 0) inputCodePoint(mCombiningAccent, controlDownFromEvent, leftAltDownFromEvent); if (mCombiningAccent != 0)
inputCodePoint(mCombiningAccent, controlDownFromEvent, leftAltDownFromEvent);
mCombiningAccent = result & KeyCharacterMap.COMBINING_ACCENT_MASK; mCombiningAccent = result & KeyCharacterMap.COMBINING_ACCENT_MASK;
} else { } else {
if (mCombiningAccent != 0) { if (mCombiningAccent != 0) {
@@ -626,8 +633,12 @@ public final class TerminalView extends View {
+ leftAltDownFromEvent + ")"); + leftAltDownFromEvent + ")");
} }
int resultingKeyCode = -1; // Set if virtual key causes this to be translated to key event. final boolean controlDown = controlDownFromEvent || mOnKeyListener.readControlKey();
if (controlDownFromEvent || mVirtualControlKeyDown) { final boolean altDown = leftAltDownFromEvent || mOnKeyListener.readAltKey();
if (mOnKeyListener.onCodePoint(codePoint, controlDown, mTermSession)) return;
if (controlDown) {
if (codePoint >= 'a' && codePoint <= 'z') { if (codePoint >= 'a' && codePoint <= 'z') {
codePoint = codePoint - 'a' + 1; codePoint = codePoint - 'a' + 1;
} else if (codePoint >= 'A' && codePoint <= 'Z') { } else if (codePoint >= 'A' && codePoint <= 'Z') {
@@ -642,68 +653,16 @@ public final class TerminalView extends View {
codePoint = 29; codePoint = 29;
} else if (codePoint == '^' || codePoint == '6') { } else if (codePoint == '^' || codePoint == '6') {
codePoint = 30; // control-^ codePoint = 30; // control-^
} else if (codePoint == '_' || codePoint == '7') { } else if (codePoint == '_' || codePoint == '7' || codePoint == '/') {
// "Ctrl-/ sends 0x1f which is equivalent of Ctrl-_ since the days of VT102"
// - http://apple.stackexchange.com/questions/24261/how-do-i-send-c-that-is-control-slash-to-the-terminal
codePoint = 31; codePoint = 31;
} else if (codePoint == '8') { } else if (codePoint == '8') {
codePoint = 127; // DEL codePoint = 127; // DEL
} else if (codePoint == '9') {
resultingKeyCode = KeyEvent.KEYCODE_F11;
} else if (codePoint == '0') {
resultingKeyCode = KeyEvent.KEYCODE_F12;
}
} else if (mVirtualFnKeyDown) {
if (codePoint == 'w' || codePoint == 'W') {
resultingKeyCode = KeyEvent.KEYCODE_DPAD_UP;
} else if (codePoint == 'a' || codePoint == 'A') {
resultingKeyCode = KeyEvent.KEYCODE_DPAD_LEFT;
} else if (codePoint == 's' || codePoint == 'S') {
resultingKeyCode = KeyEvent.KEYCODE_DPAD_DOWN;
} else if (codePoint == 'd' || codePoint == 'D') {
resultingKeyCode = KeyEvent.KEYCODE_DPAD_RIGHT;
} else if (codePoint == 'p' || codePoint == 'P') {
resultingKeyCode = KeyEvent.KEYCODE_PAGE_UP;
} else if (codePoint == 'n' || codePoint == 'N') {
resultingKeyCode = KeyEvent.KEYCODE_PAGE_DOWN;
} else if (codePoint == 't' || codePoint == 'T') {
resultingKeyCode = KeyEvent.KEYCODE_TAB;
} else if (codePoint == 'l' || codePoint == 'L') {
codePoint = '|';
} else if (codePoint == 'u' || codePoint == 'U') {
codePoint = '_';
} else if (codePoint == 'e' || codePoint == 'E') {
codePoint = 27; // ^[ (Esc)
} else if (codePoint == '.') {
codePoint = 28; // ^\
} else if (codePoint > '0' && codePoint <= '9') {
// F1-F9
resultingKeyCode = (codePoint - '1') + KeyEvent.KEYCODE_F1;
} else if (codePoint == '0') {
resultingKeyCode = KeyEvent.KEYCODE_F10;
} else if (codePoint == 'i' || codePoint == 'I') {
resultingKeyCode = KeyEvent.KEYCODE_INSERT;
} else if (codePoint == 'x' || codePoint == 'X') {
resultingKeyCode = KeyEvent.KEYCODE_FORWARD_DEL;
} else if (codePoint == 'h' || codePoint == 'H') {
resultingKeyCode = KeyEvent.KEYCODE_MOVE_HOME;
} else if (codePoint == 'f' || codePoint == 'F') {
// As left alt+f, jumping forward in readline:
codePoint = 'f';
leftAltDownFromEvent = true;
} else if (codePoint == 'b' || codePoint == 'B') {
// As left alt+b, jumping forward in readline:
codePoint = 'b';
leftAltDownFromEvent = true;
} else if (codePoint == 'v' || codePoint == 'V') {
codePoint = -1;
AudioManager audio = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
audio.adjustSuggestedStreamVolume(AudioManager.ADJUST_SAME, AudioManager.USE_DEFAULT_STREAM_TYPE, AudioManager.FLAG_SHOW_UI);
} }
} }
if (codePoint > -1) { if (codePoint > -1) {
if (resultingKeyCode > -1) {
handleKeyCode(resultingKeyCode, 0);
} else {
// Work around bluetooth keyboards sending funny unicode characters instead // Work around bluetooth keyboards sending funny unicode characters instead
// of the more normal ones from ASCII that terminal programs expect - the // of the more normal ones from ASCII that terminal programs expect - the
// desire to input the original characters should be low. // desire to input the original characters should be low.
@@ -720,8 +679,7 @@ public final class TerminalView extends View {
} }
// If left alt, send escape before the code point to make e.g. Alt+B and Alt+F work in readline: // If left alt, send escape before the code point to make e.g. Alt+B and Alt+F work in readline:
mTermSession.writeCodePoint(leftAltDownFromEvent, codePoint); mTermSession.writeCodePoint(altDown, codePoint);
}
} }
} }
@@ -737,18 +695,17 @@ public final class TerminalView extends View {
/** /**
* Called when a key is released in the view. * Called when a key is released in the view.
* *
* @param keyCode * @param keyCode The keycode of the key which was released.
* The keycode of the key which was released. * @param event A {@link KeyEvent} describing the event.
* @param event
* A {@link KeyEvent} describing the event.
* @return Whether the event was handled. * @return Whether the event was handled.
*/ */
@Override @Override
public boolean onKeyUp(int keyCode, KeyEvent event) { public boolean onKeyUp(int keyCode, KeyEvent event) {
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "onKeyUp(keyCode=" + keyCode + ", event=" + event + ")"); if (LOG_KEY_EVENTS)
Log.i(EmulatorDebug.LOG_TAG, "onKeyUp(keyCode=" + keyCode + ", event=" + event + ")");
if (mEmulator == null) return true; if (mEmulator == null) return true;
if (handleVirtualKeys(keyCode, event, false)) { if (mOnKeyListener.onKeyUp(keyCode, event)) {
invalidate(); invalidate();
return true; return true;
} else if (event.isSystem()) { } else if (event.isSystem()) {
@@ -759,49 +716,6 @@ public final class TerminalView extends View {
return true; return true;
} }
/** Handle dedicated volume buttons as virtual keys if applicable. */
private boolean handleVirtualKeys(int keyCode, KeyEvent event, boolean down) {
InputDevice inputDevice = event.getDevice();
if (inputDevice != null && inputDevice.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC) {
// Do not steal dedicated buttons from a full external keyboard.
return false;
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "handleVirtualKeys(down=" + down + ") taking ctrl event");
mVirtualControlKeyDown = down;
return true;
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "handleVirtualKeys(down=" + down + ") taking Fn event");
mVirtualFnKeyDown = down;
return true;
}
return false;
}
public void checkForFontAndColors() {
try {
// Hard-coded paths since this file is used also in Termux:Float.
@SuppressLint("SdCardPath") File fontFile = new File("/data/data/com.termux/files/home/.termux/font.ttf");
@SuppressLint("SdCardPath") File colorsFile = new File("/data/data/com.termux/files/home/.termux/colors.properties");
final Properties props = new Properties();
if (colorsFile.isFile()) {
try (InputStream in = new FileInputStream(colorsFile)) {
props.load(in);
}
}
TerminalColors.COLOR_SCHEME.updateWith(props);
if (mEmulator != null) mEmulator.mColors.reset();
final Typeface newTypeface = fontFile.exists() ? Typeface.createFromFile(fontFile) : Typeface.MONOSPACE;
mRenderer = new TerminalRenderer(mRenderer.mTextSize, newTypeface);
updateSize();
invalidate();
} catch (Exception e) {
Log.e(EmulatorDebug.LOG_TAG, "Error in checkForFontAndColors()", e);
}
}
/** /**
* This is called during layout when the size of this view has changed. If you were just added to the view * This is called during layout when the size of this view has changed. If you were just added to the view
* hierarchy, you're called with the old values of 0. * hierarchy, you're called with the old values of 0.
@@ -818,8 +732,8 @@ public final class TerminalView extends View {
if (viewWidth == 0 || viewHeight == 0 || mTermSession == null) return; if (viewWidth == 0 || viewHeight == 0 || mTermSession == null) return;
// Set to 80 and 24 if you want to enable vttest. // Set to 80 and 24 if you want to enable vttest.
int newColumns = Math.max(8, (int) (viewWidth / mRenderer.mFontWidth)); int newColumns = Math.max(4, (int) (viewWidth / mRenderer.mFontWidth));
int newRows = Math.max(8, (viewHeight - mRenderer.mFontLineSpacingAndAscent) / mRenderer.mFontLineSpacing); int newRows = Math.max(4, (viewHeight - mRenderer.mFontLineSpacingAndAscent) / mRenderer.mFontLineSpacing);
if (mEmulator == null || (newColumns != mEmulator.mColumns || newRows != mEmulator.mRows)) { if (mEmulator == null || (newColumns != mEmulator.mColumns || newRows != mEmulator.mRows)) {
mTermSession.updateSize(newColumns, newRows); mTermSession.updateSize(newColumns, newRows);
@@ -895,18 +809,12 @@ public final class TerminalView extends View {
final ActionMode.Callback callback = new ActionMode.Callback() { final ActionMode.Callback callback = new ActionMode.Callback() {
@Override @Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) { public boolean onCreateActionMode(ActionMode mode, Menu menu) {
final int[] ACTION_MODE_ATTRS = { android.R.attr.actionModeCopyDrawable, android.R.attr.actionModePasteDrawable, };
TypedArray styledAttributes = getContext().obtainStyledAttributes(ACTION_MODE_ATTRS);
try {
int show = MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT; int show = MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT;
ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE); ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
menu.add(Menu.NONE, 1, Menu.NONE, R.string.copy_text).setIcon(styledAttributes.getResourceId(0, 0)).setShowAsAction(show); menu.add(Menu.NONE, 1, Menu.NONE, R.string.copy_text).setShowAsAction(show);
menu.add(Menu.NONE, 2, Menu.NONE, R.string.paste_text).setIcon(styledAttributes.getResourceId(1, 0)).setEnabled(clipboard.hasPrimaryClip()).setShowAsAction(show); menu.add(Menu.NONE, 2, Menu.NONE, R.string.paste_text).setEnabled(clipboard.hasPrimaryClip()).setShowAsAction(show);
menu.add(Menu.NONE, 3, Menu.NONE, R.string.text_selection_more); menu.add(Menu.NONE, 3, Menu.NONE, R.string.text_selection_more);
} finally {
styledAttributes.recycle();
}
return true; return true;
} }

View File

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

View File

@@ -208,11 +208,6 @@ JNIEXPORT int JNICALL Java_com_termux_terminal_JNI_waitFor(JNIEnv* TERMUX_UNUSED
} }
} }
JNIEXPORT void JNICALL Java_com_termux_terminal_JNI_hangupProcessGroup(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint procId)
{
killpg(procId, SIGHUP);
}
JNIEXPORT void JNICALL Java_com_termux_terminal_JNI_close(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint fileDescriptor) JNIEXPORT void JNICALL Java_com_termux_terminal_JNI_close(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint fileDescriptor)
{ {
close(fileDescriptor); close(fileDescriptor);

View File

@@ -0,0 +1,7 @@
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:duration="30"
android:fromXDelta="-1%"
android:repeatCount="1"
android:repeatMode="reverse"
android:toXDelta="1%"/>
</set>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 695 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 786 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 597 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 779 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 983 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,17 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFF"
android:pathData="M 12, 12
m -10.5, 0
a 10.5,10.5 0 1,0 21,0
a 10.5,10.5 0 1,0 -21,0"/>
<path
android:fillColor="#FF000000"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM17,13h-4v4h-2v-4L7,13v-2h4L11,7h2v4h4v2z"/>
</vector>

View File

@@ -0,0 +1,33 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="48dp"
android:width="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<!--
https://material.google.com/style/icons.html
-->
<!-- Screen border. -->
<path android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeWidth="3"
android:pathData="M7,4
l34,0
q3 0,3 3
l0,34
q0 3, -3 3
l-34,0
q-3 0, -3-3
l0 -34
q0 -3, 3 -3"
/>
<!-- Block cursor. -->
<path android:fillColor="#000"
android:pathData="M12,12
l5,0
l0,10
l-5,0"
/>
</vector>

View File

@@ -1,6 +1,13 @@
<com.termux.drawer.DrawerLayout 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_height="match_parent"
android:orientation="vertical">
<android.support.v4.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_above="@+id/viewpager"
android:layout_height="match_parent"> android:layout_height="match_parent">
<com.termux.view.TerminalView <com.termux.view.TerminalView
@@ -20,6 +27,7 @@
android:choiceMode="singleChoice" android:choiceMode="singleChoice"
android:divider="@android:color/transparent" android:divider="@android:color/transparent"
android:dividerHeight="0dp" android:dividerHeight="0dp"
android:descendantFocusability="blocksDescendants"
android:orientation="vertical"> android:orientation="vertical">
<ListView <ListView
@@ -55,4 +63,13 @@
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
</com.termux.drawer.DrawerLayout> </android.support.v4.widget.DrawerLayout>
<android.support.v4.view.ViewPager
android:id="@+id/viewpager"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="@android:drawable/screen_background_dark_transparent"
android:layout_alignParentBottom="true" />
</RelativeLayout>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<com.termux.app.ExtraKeysView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/extra_keys"
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentBottom="true"
android:orientation="horizontal" />

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<EditText xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/text_input"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:imeOptions="actionSend|flagNoFullscreen"
android:maxLines="1"
android:inputType="text"
android:textColor="@android:color/white"
android:paddingTop="0dp"
android:textCursorDrawable="@null"
android:paddingBottom="0dp"
tools:ignore="LabelFor" />

View File

@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="8dp"
android:paddingRight="8dp">
<ListView android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:drawSelectorOnTop="false"/>
<TextView android:id="@android:id/empty"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="@string/empty_folder"/>
</LinearLayout>

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 685 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -38,8 +38,8 @@
<string name="copy_text">Copy</string> <string name="copy_text">Copy</string>
<string name="text_selection_more">More…</string> <string name="text_selection_more">More…</string>
<string name="kill_process">Hangup</string> <string name="kill_process">Kill process (%d)</string>
<string name="confirm_kill_process">Close this process?</string> <string name="confirm_kill_process">Really kill this session?</string>
<string name="session_rename_title">Set session name</string> <string name="session_rename_title">Set session name</string>
<string name="session_rename_positive_button">Set</string> <string name="session_rename_positive_button">Set</string>
@@ -50,10 +50,8 @@
<string name="styling_install">Install</string> <string name="styling_install">Install</string>
<string name="notification_action_exit">Exit</string> <string name="notification_action_exit">Exit</string>
<string name="notification_action_wakelock">Wake</string> <string name="notification_action_wake_lock">Acquire wakelock</string>
<string name="notification_action_wifilock">Wifi</string> <string name="notification_action_wake_unlock">Release wakelock</string>
<string name="empty_folder">Empty folder.</string>
<string name="file_received_title">Save file in ~/downloads/</string> <string name="file_received_title">Save file in ~/downloads/</string>
<string name="file_received_edit_button">Edit</string> <string name="file_received_edit_button">Edit</string>

View File

@@ -0,0 +1,12 @@
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<shortcut
android:shortcutId="new_session"
android:enabled="true"
android:icon="@drawable/ic_new_session"
android:shortcutShortLabel="@string/new_session">
<intent
android:action="android.intent.action.RUN"
android:targetPackage="com.termux"
android:targetClass="com.termux.app.TermuxActivity"/>
</shortcut>
</shortcuts>

View File

@@ -18,7 +18,7 @@ public class CursorAndScreenTest extends TerminalTestCase {
assertLinesAre("ABCDE", "FGHIJ", "KLMNO", "PQRST", "UVWXY"); assertLinesAre("ABCDE", "FGHIJ", "KLMNO", "PQRST", "UVWXY");
for (int row = 0; row < 5; row++) { for (int row = 0; row < 5; row++) {
for (int col = 0; col < 5; col++) { for (int col = 0; col < 5; col++) {
int s = getStyleAt(row, col); long s = getStyleAt(row, col);
Assert.assertEquals(col, TextStyle.decodeForeColor(s)); Assert.assertEquals(col, TextStyle.decodeForeColor(s));
Assert.assertEquals(row, TextStyle.decodeBackColor(s)); Assert.assertEquals(row, TextStyle.decodeBackColor(s));
} }
@@ -28,7 +28,7 @@ public class CursorAndScreenTest extends TerminalTestCase {
assertLinesAre("KLMNO", "PQRST", "UVWXY", " ", " "); assertLinesAre("KLMNO", "PQRST", "UVWXY", " ", " ");
for (int row = 0; row < 3; row++) { for (int row = 0; row < 3; row++) {
for (int col = 0; col < 5; col++) { for (int col = 0; col < 5; col++) {
int s = getStyleAt(row, col); long s = getStyleAt(row, col);
Assert.assertEquals(col, TextStyle.decodeForeColor(s)); Assert.assertEquals(col, TextStyle.decodeForeColor(s));
Assert.assertEquals(row + 2, TextStyle.decodeBackColor(s)); Assert.assertEquals(row + 2, TextStyle.decodeBackColor(s));
} }
@@ -43,7 +43,7 @@ public class CursorAndScreenTest extends TerminalTestCase {
for (int col = 0; col < 5; col++) { for (int col = 0; col < 5; col++) {
int wantedForeground = (row == 1 || row == 2) ? 98 : col; int wantedForeground = (row == 1 || row == 2) ? 98 : col;
int wantedBackground = (row == 1 || row == 2) ? 99 : (row == 0 ? 2 : row); int wantedBackground = (row == 1 || row == 2) ? 99 : (row == 0 ? 2 : row);
int s = getStyleAt(row, col); long s = getStyleAt(row, col);
Assert.assertEquals(wantedForeground, TextStyle.decodeForeColor(s)); Assert.assertEquals(wantedForeground, TextStyle.decodeForeColor(s));
Assert.assertEquals(wantedBackground, TextStyle.decodeBackColor(s)); Assert.assertEquals(wantedBackground, TextStyle.decodeBackColor(s));
} }

View File

@@ -141,10 +141,10 @@ public class KeyHandlerTest extends TestCase {
assertKeysEquals("\033[1;6D", KeyHandler.getCode(KeyEvent.KEYCODE_DPAD_LEFT, mod, false, false)); assertKeysEquals("\033[1;6D", KeyHandler.getCode(KeyEvent.KEYCODE_DPAD_LEFT, mod, false, false));
// Home/end keys: // Home/end keys:
assertKeysEquals("\033[H", KeyHandler.getCode(KeyEvent.KEYCODE_HOME, 0, false, false)); assertKeysEquals("\033[H", KeyHandler.getCode(KeyEvent.KEYCODE_MOVE_HOME, 0, false, false));
assertKeysEquals("\033[F", KeyHandler.getCode(KeyEvent.KEYCODE_MOVE_END, 0, false, false)); assertKeysEquals("\033[F", KeyHandler.getCode(KeyEvent.KEYCODE_MOVE_END, 0, false, false));
// ... shifted: // ... shifted:
assertKeysEquals("\033[1;2H", KeyHandler.getCode(KeyEvent.KEYCODE_HOME, KeyHandler.KEYMOD_SHIFT, false, false)); assertKeysEquals("\033[1;2H", KeyHandler.getCode(KeyEvent.KEYCODE_MOVE_HOME, KeyHandler.KEYMOD_SHIFT, false, false));
assertKeysEquals("\033[1;2F", KeyHandler.getCode(KeyEvent.KEYCODE_MOVE_END, KeyHandler.KEYMOD_SHIFT, false, false)); assertKeysEquals("\033[1;2F", KeyHandler.getCode(KeyEvent.KEYCODE_MOVE_END, KeyHandler.KEYMOD_SHIFT, false, false));
// Function keys F1-F12: // Function keys F1-F12:
@@ -173,5 +173,19 @@ public class KeyHandlerTest extends TestCase {
assertKeysEquals("\033[21;2~", KeyHandler.getCode(KeyEvent.KEYCODE_F10, KeyHandler.KEYMOD_SHIFT, false, false)); assertKeysEquals("\033[21;2~", KeyHandler.getCode(KeyEvent.KEYCODE_F10, KeyHandler.KEYMOD_SHIFT, false, false));
assertKeysEquals("\033[23;2~", KeyHandler.getCode(KeyEvent.KEYCODE_F11, KeyHandler.KEYMOD_SHIFT, false, false)); assertKeysEquals("\033[23;2~", KeyHandler.getCode(KeyEvent.KEYCODE_F11, KeyHandler.KEYMOD_SHIFT, false, false));
assertKeysEquals("\033[24;2~", KeyHandler.getCode(KeyEvent.KEYCODE_F12, KeyHandler.KEYMOD_SHIFT, false, false)); assertKeysEquals("\033[24;2~", KeyHandler.getCode(KeyEvent.KEYCODE_F12, KeyHandler.KEYMOD_SHIFT, false, false));
assertKeysEquals("0", KeyHandler.getCode(KeyEvent.KEYCODE_NUMPAD_0, 0, false, false));
assertKeysEquals("1", KeyHandler.getCode(KeyEvent.KEYCODE_NUMPAD_1, 0, false, false));
assertKeysEquals("2", KeyHandler.getCode(KeyEvent.KEYCODE_NUMPAD_2, 0, false, false));
assertKeysEquals("3", KeyHandler.getCode(KeyEvent.KEYCODE_NUMPAD_3, 0, false, false));
assertKeysEquals("4", KeyHandler.getCode(KeyEvent.KEYCODE_NUMPAD_4, 0, false, false));
assertKeysEquals("5", KeyHandler.getCode(KeyEvent.KEYCODE_NUMPAD_5, 0, false, false));
assertKeysEquals("6", KeyHandler.getCode(KeyEvent.KEYCODE_NUMPAD_6, 0, false, false));
assertKeysEquals("7", KeyHandler.getCode(KeyEvent.KEYCODE_NUMPAD_7, 0, false, false));
assertKeysEquals("8", KeyHandler.getCode(KeyEvent.KEYCODE_NUMPAD_8, 0, false, false));
assertKeysEquals("9", KeyHandler.getCode(KeyEvent.KEYCODE_NUMPAD_9, 0, false, false));
assertKeysEquals(",", KeyHandler.getCode(KeyEvent.KEYCODE_NUMPAD_COMMA, 0, false, false));
assertKeysEquals(".", KeyHandler.getCode(KeyEvent.KEYCODE_NUMPAD_DOT, 0, false, false));
} }
} }

View File

@@ -93,7 +93,7 @@ public class ResizeTest extends TerminalTestCase {
enterString("\033[2J"); enterString("\033[2J");
for (int r = 0; r < rows; r++) { for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) { for (int c = 0; c < cols; c++) {
int style = getStyleAt(r, c); long style = getStyleAt(r, c);
assertEquals(119, TextStyle.decodeForeColor(style)); assertEquals(119, TextStyle.decodeForeColor(style));
assertEquals(129, TextStyle.decodeBackColor(style)); assertEquals(129, TextStyle.decodeBackColor(style));
} }
@@ -105,7 +105,7 @@ public class ResizeTest extends TerminalTestCase {
// After resize, screen should still be same color: // After resize, screen should still be same color:
for (int r = 0; r < rows - 2; r++) { for (int r = 0; r < rows - 2; r++) {
for (int c = 0; c < cols; c++) { for (int c = 0; c < cols; c++) {
int style = getStyleAt(r, c); long style = getStyleAt(r, c);
assertEquals(119, TextStyle.decodeForeColor(style)); assertEquals(119, TextStyle.decodeForeColor(style));
assertEquals(129, TextStyle.decodeBackColor(style)); assertEquals(129, TextStyle.decodeBackColor(style));
} }
@@ -116,7 +116,7 @@ public class ResizeTest extends TerminalTestCase {
resize(cols, rows); resize(cols, rows);
for (int r = 0; r < rows; r++) { for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) { for (int c = 0; c < cols; c++) {
int style = getStyleAt(r, c); long style = getStyleAt(r, c);
assertEquals(119, TextStyle.decodeForeColor(style)); assertEquals(119, TextStyle.decodeForeColor(style));
assertEquals("wrong at row=" + r, r >= 3 ? 200 : 129, TextStyle.decodeBackColor(style)); assertEquals("wrong at row=" + r, r >= 3 ? 200 : 129, TextStyle.decodeBackColor(style));
} }

View File

@@ -1,6 +1,6 @@
package com.termux.terminal; package com.termux.terminal;
public class ScreenBufferTest extends TerminalTest { public class ScreenBufferTest extends TerminalTestCase {
public void testBasics() { public void testBasics() {
TerminalBuffer screen = new TerminalBuffer(5, 3, 3); TerminalBuffer screen = new TerminalBuffer(5, 3, 3);

View File

@@ -147,7 +147,6 @@ public class TerminalTest extends TerminalTestCase {
enterString("\033[38;5;119m"); enterString("\033[38;5;119m");
assertEquals(119, mTerminal.mForeColor); assertEquals(119, mTerminal.mForeColor);
assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor); assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
enterString("\033[48;5;129m"); enterString("\033[48;5;129m");
assertEquals(119, mTerminal.mForeColor); assertEquals(119, mTerminal.mForeColor);
assertEquals(129, mTerminal.mBackColor); assertEquals(129, mTerminal.mBackColor);
@@ -161,6 +160,30 @@ public class TerminalTest extends TerminalTestCase {
enterString("\033[38;5;178;48;5;179;m"); enterString("\033[38;5;178;48;5;179;m");
assertEquals(178, mTerminal.mForeColor); assertEquals(178, mTerminal.mForeColor);
assertEquals(179, mTerminal.mBackColor); assertEquals(179, mTerminal.mBackColor);
// 24 bit colors:
enterString(("\033[0m")); // Reset fg and bg colors.
enterString("\033[38;2;255;127;2m");
int expectedForeground = 0xff000000 | (255 << 16) | (127 << 8) | 2;
assertEquals(expectedForeground, mTerminal.mForeColor);
assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
enterString("\033[48;2;1;2;254m");
int expectedBackground = 0xff000000 | (1 << 16) | (2 << 8) | 254;
assertEquals(expectedForeground, mTerminal.mForeColor);
assertEquals(expectedBackground, mTerminal.mBackColor);
// 24 bit colors, set fg and bg at once:
enterString(("\033[0m")); // Reset fg and bg colors.
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
enterString("\033[38;2;255;127;2;48;2;1;2;254m");
assertEquals(expectedForeground, mTerminal.mForeColor);
assertEquals(expectedBackground, mTerminal.mBackColor);
// 24 bit colors, invalid input:
enterString("\033[38;2;300;127;2;48;2;1;300;254m");
assertEquals(expectedForeground, mTerminal.mForeColor);
assertEquals(expectedBackground, mTerminal.mBackColor);
} }
public void testBackgroundColorErase() { public void testBackgroundColorErase() {
@@ -169,7 +192,7 @@ public class TerminalTest extends TerminalTestCase {
withTerminalSized(cols, rows); withTerminalSized(cols, rows);
for (int r = 0; r < rows; r++) { for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) { for (int c = 0; c < cols; c++) {
int style = getStyleAt(r, c); long style = getStyleAt(r, c);
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, TextStyle.decodeForeColor(style)); assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, TextStyle.decodeForeColor(style));
assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, TextStyle.decodeBackColor(style)); assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, TextStyle.decodeBackColor(style));
} }
@@ -182,7 +205,7 @@ public class TerminalTest extends TerminalTestCase {
enterString("\033[2J"); enterString("\033[2J");
for (int r = 0; r < rows; r++) { for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) { for (int c = 0; c < cols; c++) {
int style = getStyleAt(r, c); long style = getStyleAt(r, c);
assertEquals(119, TextStyle.decodeForeColor(style)); assertEquals(119, TextStyle.decodeForeColor(style));
assertEquals(129, TextStyle.decodeBackColor(style)); assertEquals(129, TextStyle.decodeBackColor(style));
} }
@@ -193,7 +216,7 @@ public class TerminalTest extends TerminalTestCase {
enterString("\033[2L"); enterString("\033[2L");
for (int r = 0; r < rows; r++) { for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) { for (int c = 0; c < cols; c++) {
int style = getStyleAt(r, c); long style = getStyleAt(r, c);
assertEquals((r == 0 || r == 1) ? 139 : 129, TextStyle.decodeBackColor(style)); assertEquals((r == 0 || r == 1) ? 139 : 129, TextStyle.decodeBackColor(style));
} }
} }

View File

@@ -19,6 +19,7 @@ public abstract class TerminalTestCase extends TestCase {
public final List<ChangedTitle> titleChanges = new ArrayList<>(); public final List<ChangedTitle> titleChanges = new ArrayList<>();
public final List<String> clipboardPuts = new ArrayList<>(); public final List<String> clipboardPuts = new ArrayList<>();
public int bellsRung = 0; public int bellsRung = 0;
public int colorsChanged = 0;
@Override @Override
public void write(byte[] data, int offset, int count) { public void write(byte[] data, int offset, int count) {
@@ -49,6 +50,11 @@ public abstract class TerminalTestCase extends TestCase {
public void onBell() { public void onBell() {
bellsRung++; bellsRung++;
} }
@Override
public void onColorsChanged() {
colorsChanged++;
}
} }
public TerminalEmulator mTerminal; public TerminalEmulator mTerminal;
@@ -234,7 +240,7 @@ public abstract class TerminalTestCase extends TestCase {
} }
/** For testing only. Encoded style according to {@link TextStyle}. */ /** For testing only. Encoded style according to {@link TextStyle}. */
public int getStyleAt(int externalRow, int column) { public long getStyleAt(int externalRow, int column) {
return mTerminal.getScreen().getStyleAt(externalRow, column); return mTerminal.getScreen().getStyleAt(externalRow, column);
} }
@@ -290,7 +296,7 @@ public abstract class TerminalTestCase extends TestCase {
} }
public void assertForegroundColorAt(int externalRow, int column, int color) { public void assertForegroundColorAt(int externalRow, int column, int color) {
int style = mTerminal.getScreen().mLines[mTerminal.getScreen().externalToInternalRow(externalRow)].getStyle(column); long style = mTerminal.getScreen().mLines[mTerminal.getScreen().externalToInternalRow(externalRow)].getStyle(column);
assertEquals(color, TextStyle.decodeForeColor(style)); assertEquals(color, TextStyle.decodeForeColor(style));
} }

View File

@@ -13,7 +13,7 @@ public class TextStyleTest extends TestCase {
for (int fx : ALL_EFFECTS) { for (int fx : ALL_EFFECTS) {
for (int fg = 0; fg < TextStyle.NUM_INDEXED_COLORS; fg++) { for (int fg = 0; fg < TextStyle.NUM_INDEXED_COLORS; fg++) {
for (int bg = 0; bg < TextStyle.NUM_INDEXED_COLORS; bg++) { for (int bg = 0; bg < TextStyle.NUM_INDEXED_COLORS; bg++) {
int encoded = TextStyle.encode(fg, bg, fx); long encoded = TextStyle.encode(fg, bg, fx);
assertEquals(fg, TextStyle.decodeForeColor(encoded)); assertEquals(fg, TextStyle.decodeForeColor(encoded));
assertEquals(bg, TextStyle.decodeBackColor(encoded)); assertEquals(bg, TextStyle.decodeBackColor(encoded));
assertEquals(fx, TextStyle.decodeEffect(encoded)); assertEquals(fx, TextStyle.decodeEffect(encoded));
@@ -22,6 +22,22 @@ public class TextStyleTest extends TestCase {
} }
} }
public void testEncoding24Bit() {
int[] values = {255, 240, 127, 1, 0};
for (int red : values) {
for (int green : values) {
for (int blue : values) {
int argb = 0xFF000000 | (red << 16) | (green << 8) | blue;
long encoded = TextStyle.encode(argb, 0, 0);
assertEquals(argb, TextStyle.decodeForeColor(encoded));
encoded = TextStyle.encode(0, argb, 0);
assertEquals(argb, TextStyle.decodeBackColor(encoded));
}
}
}
}
public void testEncodingCombinations() { public void testEncodingCombinations() {
for (int f1 : ALL_EFFECTS) { for (int f1 : ALL_EFFECTS) {
for (int f2 : ALL_EFFECTS) { for (int f2 : ALL_EFFECTS) {
@@ -32,13 +48,13 @@ public class TextStyleTest extends TestCase {
} }
public void testEncodingStrikeThrough() { public void testEncodingStrikeThrough() {
int encoded = TextStyle.encode(TextStyle.COLOR_INDEX_FOREGROUND, TextStyle.COLOR_INDEX_BACKGROUND, long encoded = TextStyle.encode(TextStyle.COLOR_INDEX_FOREGROUND, TextStyle.COLOR_INDEX_BACKGROUND,
TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH); TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH);
assertTrue((TextStyle.decodeEffect(encoded) & TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH) != 0); assertTrue((TextStyle.decodeEffect(encoded) & TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH) != 0);
} }
public void testEncodingProtected() { public void testEncodingProtected() {
int encoded = TextStyle.encode(TextStyle.COLOR_INDEX_FOREGROUND, TextStyle.COLOR_INDEX_BACKGROUND, long encoded = TextStyle.encode(TextStyle.COLOR_INDEX_FOREGROUND, TextStyle.COLOR_INDEX_BACKGROUND,
TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH); TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH);
assertTrue((TextStyle.decodeEffect(encoded) & TextStyle.CHARACTER_ATTRIBUTE_PROTECTED) == 0); assertTrue((TextStyle.decodeEffect(encoded) & TextStyle.CHARACTER_ATTRIBUTE_PROTECTED) == 0);
encoded = TextStyle.encode(TextStyle.COLOR_INDEX_FOREGROUND, TextStyle.COLOR_INDEX_BACKGROUND, encoded = TextStyle.encode(TextStyle.COLOR_INDEX_FOREGROUND, TextStyle.COLOR_INDEX_BACKGROUND,

View File

@@ -15,6 +15,12 @@ public class WcWidthTest extends TestCase {
} }
} }
public void testSomeWidthOne() {
assertWidthIs(1, 'å');
assertWidthIs(1, 'ä');
assertWidthIs(1, 'ö');
}
public void testSomeWide() { public void testSomeWide() {
assertWidthIs(2, ''); assertWidthIs(2, '');
assertWidthIs(2, ''); assertWidthIs(2, '');
@@ -39,11 +45,19 @@ public class WcWidthTest extends TestCase {
public void testCombining() { public void testCombining() {
assertWidthIs(0, 0x0302); assertWidthIs(0, 0x0302);
assertWidthIs(0, 0x0308); assertWidthIs(0, 0x0308);
}
public void testWordJoiner() {
// https://en.wikipedia.org/wiki/Word_joiner
// The word joiner (WJ) is a code point in Unicode used to separate words when using scripts
// that do not use explicit spacing. It is encoded since Unicode version 3.2
// (released in 2002) as U+2060 WORD JOINER (HTML &#8288;).
// The word joiner does not produce any space, and prohibits a line break at its position.
assertWidthIs(0, 0x2060); assertWidthIs(0, 0x2060);
} }
public void testWatch() { public void testWatch() {
assertWidthIs(1, 0x231a);
} }
public void testSofthyphen() { public void testSofthyphen() {
@@ -57,7 +71,13 @@ public class WcWidthTest extends TestCase {
} }
public void testHangul() { public void testHangul() {
assertWidthIs(2, 0x11A3); assertWidthIs(1, 0x11A3);
}
public void testEmojis() {
assertWidthIs(2, 0x1F428); // KOALA.
assertWidthIs(2, 0x231a); // WATCH.
assertWidthIs(2, 0x1F643); // UPSIDE-DOWN FACE (Unicode 8).
} }
} }

20
art/generate-pngs.sh Executable file
View File

@@ -0,0 +1,20 @@
#!/bin/sh
for DENSITY in mdpi hdpi xhdpi xxhdpi xxxhdpi; do
case $DENSITY in
mdpi) SIZE=48;;
hdpi) SIZE=72;;
xhdpi) SIZE=96;;
xxhdpi) SIZE=144;;
xxxhdpi) SIZE=192;;
esac
FOLDER=../app/src/main/res/mipmap-$DENSITY
mkdir -p $FOLDER
for FILE in ic_launcher ic_launcher_round; do
PNG=$FOLDER/$FILE.png
rsvg-convert -w $SIZE -h $SIZE $FILE.svg > $PNG
zopflipng -y $PNG $PNG
done
done

26
art/ic_launcher.svg Normal file
View File

@@ -0,0 +1,26 @@
<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="M7,4
l34,0
q3 0,3 3
l0,34
q0 3, -3 3
l-34,0
q-3 0, -3-3
l0 -34
q0 -3, 3 -3"
/>
<!-- Block cursor. -->
<path fill="#FFF"
d="M12,12
l5,0
l0,10
l-5,0"
/>
</svg>

After

Width:  |  Height:  |  Size: 512 B

21
art/ic_launcher_round.svg Normal file
View File

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

After

Width:  |  Height:  |  Size: 429 B

View File

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

View File

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

View File

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

Binary file not shown.

View File

@@ -1,6 +1,6 @@
#Tue Mar 15 00:24:33 CET 2016 #Sun Dec 04 17:26:05 CET 2016
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-2.12-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-3.2.1-bin.zip

68
gradlew vendored
View File

@@ -1,4 +1,4 @@
#!/usr/bin/env bash #!/usr/bin/env sh
############################################################################## ##############################################################################
## ##
@@ -6,12 +6,30 @@
## ##
############################################################################## ##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. # Attempt to set APP_HOME
DEFAULT_JVM_OPTS="" # Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle" 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.
DEFAULT_JVM_OPTS=""
# 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"
@@ -30,6 +48,7 @@ die ( ) {
cygwin=false cygwin=false
msys=false msys=false
darwin=false darwin=false
nonstop=false
case "`uname`" in case "`uname`" in
CYGWIN* ) CYGWIN* )
cygwin=true cygwin=true
@@ -40,26 +59,11 @@ case "`uname`" in
MINGW* ) MINGW* )
msys=true msys=true
;; ;;
NONSTOP* )
nonstop=true
;;
esac esac
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM. # Determine the Java command to use to start the JVM.
@@ -85,7 +89,7 @@ location of your Java installation."
fi fi
# Increase the maximum file descriptors if we can. # Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n` MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
@@ -150,11 +154,19 @@ if $cygwin ; then
esac esac
fi fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules # Escape application args
function splitJvmOpts() { save ( ) {
JVM_OPTS=("$@") for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
} }
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS APP_ARGS=$(save "$@")
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" # Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

84
gradlew.bat vendored Normal file
View File

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