Compare commits

..

104 Commits
v0.27 ... v0.38

Author SHA1 Message Date
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
Fredrik Fornwall
d69485b70b Match less files for file receiving (fixes #66) 2016-03-25 00:22:49 +01:00
Fredrik Fornwall
421dfcca39 Do not fail with NPE when scheme is null
Also remove some debug logging left by mistake.
2016-03-23 18:31:03 +01:00
Fredrik Fornwall
3aaa0ab267 Remove unused imports 2016-03-21 15:24:02 +01:00
Fredrik Fornwall
e7f9647beb Remove unused variable 2016-03-21 15:19:08 +01:00
Fredrik Fornwall
5558f371b4 Bump version to 0.31 2016-03-21 00:01:59 +01:00
Fredrik Fornwall
0882ed6470 Keep the EXTERNAL_STORAGE environment variable
The EXTERNAL_STORAGE environment variable is needed on at least the
Samsung Galaxy S7 for /system/bin/am to function.
2016-03-20 23:57:34 +01:00
Fredrik Fornwall
5c02448521 Install 64-bit arm packages on capable devices
This will only affect new installations. Existing users wishing to
install 64-bit packages can re-install the app completely, or just
'rm -Rf $PREFIX' and exit all sessions, which will cause Termux to
re-install all packages at next startup.
2016-03-20 22:24:05 +01:00
Fredrik Fornwall
17382fb190 Do not have /system/bin in the PATH
By appending the old system PATH environment variable to the paths
setup by Termux system binaries are found as a fallback.

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

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

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

Also remove the built JNI libraries from source control.
2016-02-09 11:24:05 +01:00
Fredrik Fornwall
1b6919bb23 Add test comment 2016-01-28 16:45:45 +01:00
64 changed files with 8387 additions and 10254 deletions

View File

@@ -12,3 +12,5 @@ root = true
end_of_line = lf end_of_line = lf
insert_final_newline = true insert_final_newline = true
charset = utf-8 charset = utf-8
indent_style = space
indent_size = 4

1
.gitignore vendored
View File

@@ -5,6 +5,7 @@
# Built application files # Built application files
build/ build/
*.apk *.apk
*.so
# 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>

7
.idea/gradle.xml generated
View File

@@ -6,13 +6,18 @@
<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="myModules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</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 @@
language: android
sudo: false sudo: false
language: android
jdk: oraclejdk8
env: env:
global: global:
@@ -11,8 +12,8 @@ android:
components: components:
- platform-tools - platform-tools
- tools - tools
- build-tools-23.0.2 - build-tools-24.0.1
- android-23 - android-24
- extra-android-m2repository - extra-android-m2repository
script: script:

View File

@@ -20,7 +20,7 @@ Released under [the GPLv3 license](https://www.gnu.org/licenses/gpl.html). Conta
Building JNI libraries Building JNI libraries
====================== ======================
For ease of use, the JNI libraries are checked into version control. Execute the `build-jnilibs.sh` script to rebuild them. Execute the `build-jnilibs.sh` script to build the required JNI libraries.
Terminal resources Terminal resources
================== ==================

View File

@@ -1,30 +1,32 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
android { android {
compileSdkVersion 23 compileSdkVersion 24
buildToolsVersion "23.0.2" buildToolsVersion "24.0.1"
dependencies { dependencies {
compile 'com.android.support:support-annotations:23.1.1' compile 'com.android.support:support-annotations:24.1.1'
} compile "com.android.support:support-v4:24.1.1"
sourceSets {
main {
jni.srcDirs = []
}
} }
defaultConfig { defaultConfig {
applicationId "com.termux" applicationId "com.termux"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 23 targetSdkVersion 24
versionCode 27 versionCode 38
versionName "0.27" versionName "0.38"
ndk {
moduleName "libtermux"
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
cFlags "-std=c11 -Wall -Wextra -Os -fno-stack-protector -nostdlib -Wl,--gc-sections"
}
} }
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'
} }
} }

View File

@@ -44,29 +44,45 @@
android:label="@string/application_name" /> android:label="@string/application_name" />
<activity <activity
android:name="com.termux.filepicker.TermuxFilePickerActivity" android:name="com.termux.filepicker.TermuxFileReceiverActivity"
android:label="@string/application_name" android:label="@string/application_name"
android:theme="@android:style/Theme.Material.Light.DarkActionBar" android:taskAffinity="com.termux.filereceiver"
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.SEND"/>
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="application/*" />
<data android:mimeType="*/*" /> <data android:mimeType="audio/*" />
<data android:mimeType="image/*" />
<data android:mimeType="message/*" />
<data android:mimeType="multipart/*" />
<data android:mimeType="text/*" />
<data android:mimeType="video/*" />
</intent-filter>
<!-- Be more restrictive for viewing files, restricting ourselves to text files. -->
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/*" />
<data android:mimeType="application/json" />
<data android:mimeType="application/*xml*" />
<data android:mimeType="application/*latex*" />
<data android:mimeType="application/javascript" />
</intent-filter> </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,112 @@
package com.termux.app;
import android.util.Log;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
/**
* A background job launched by Termux.
*/
public final class BackgroundJob {
private static final String LOG_TAG = "termux-background";
final Process mProcess;
public BackgroundJob(File cwd, File fileToExecute, String[] args) throws IOException {
String[] env = buildEnvironment(false, cwd.getAbsolutePath());
String[] progArray = new String[args.length + 1];
mProcess = Runtime.getRuntime().exec(progArray, env, cwd);
new Thread() {
@Override
public void run() {
while (true) {
try {
int exitCode = mProcess.waitFor();
if (exitCode == 0) {
Log.i(LOG_TAG, "exited normally");
return;
} else {
Log.i(LOG_TAG, "exited with exit code: " + exitCode);
}
} catch (InterruptedException e) {
// Ignore.
}
}
}
}.start();
new Thread() {
@Override
public void run() {
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, line);
}
} catch (IOException 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.e(LOG_TAG, line);
}
} catch (IOException e) {
// Ignore.
}
}
};
}
public 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};
}
}
}

View File

@@ -3,24 +3,25 @@ package com.termux.app;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.res.Configuration;
import android.text.Selection; import android.text.Selection;
import android.util.TypedValue; import android.util.TypedValue;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.ViewGroup.LayoutParams; import android.view.ViewGroup.LayoutParams;
import android.view.WindowManager;
import android.widget.EditText; import android.widget.EditText;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
final class DialogUtils { public final class DialogUtils {
public interface TextSetListener { public interface TextSetListener {
void onTextSet(String text); void onTextSet(String text);
} }
static void textInput(Activity activity, int titleText, int positiveButtonText, String initialText, final TextSetListener onPositive, public static void textInput(Activity activity, int titleText, String initialText,
int neutralButtonText, final TextSetListener onNeutral) { int positiveButtonText, final TextSetListener onPositive,
int neutralButtonText, final TextSetListener onNeutral,
int negativeButtonText, final TextSetListener onNegative,
final DialogInterface.OnDismissListener onDismiss) {
final EditText input = new EditText(activity); final EditText input = new EditText(activity);
input.setSingleLine(); input.setSingleLine();
if (initialText != null) { if (initialText != null) {
@@ -57,8 +58,7 @@ final class DialogUtils {
public void onClick(DialogInterface d, int whichButton) { public void onClick(DialogInterface d, int whichButton) {
onPositive.onTextSet(input.getText().toString()); onPositive.onTextSet(input.getText().toString());
} }
}) });
.setNegativeButton(android.R.string.cancel, null);
if (onNeutral != null) { if (onNeutral != null) {
builder.setNeutralButton(neutralButtonText, new DialogInterface.OnClickListener() { builder.setNeutralButton(neutralButtonText, new DialogInterface.OnClickListener() {
@@ -69,11 +69,21 @@ final class DialogUtils {
}); });
} }
dialogHolder[0] = builder.create(); if (onNegative == null) {
if ((activity.getResources().getConfiguration().hardKeyboardHidden & Configuration.HARDKEYBOARDHIDDEN_YES) == 0) { builder.setNegativeButton(android.R.string.cancel, null);
// Show soft keyboard unless hardware keyboard available. } else {
dialogHolder[0].getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); builder.setNegativeButton(negativeButtonText, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
onNegative.onTextSet(input.getText().toString());
} }
});
}
if (onDismiss != null) builder.setOnDismissListener(onDismiss);
dialogHolder[0] = builder.create();
dialogHolder[0].setCanceledOnTouchOutside(false);
dialogHolder[0].show(); dialogHolder[0].show();
} }

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, weight);
param.rowSpec = GridLayout.spec(row, 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);
int usableHeightNow = Math.min(mWindowRect.height(), childViewOfContent.getRootView().getHeight());
FrameLayout.LayoutParams layout = (LayoutParams) childViewOfContent.getLayoutParams();
if (layout.height != usableHeightNow) {
layout.height = usableHeightNow;
childViewOfContent.requestLayout();
} }
setImmersiveMode();
} else {
mActivity.findViewById(R.id.viewpager).setVisibility(View.GONE);
}
}
});
setImmersiveMode();
} else {
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
decorView.setOnSystemUiVisibilityChangeListener(null);
}
}
private static boolean isColorLight(int color) {
double darkness = 1 - (0.299 * Color.red(color) + 0.587 * Color.green(color) + 0.114 * Color.blue(color)) / 255;
return darkness < 0.5;
}
void setImmersiveMode() {
int flags = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
| View.SYSTEM_UI_FLAG_FULLSCREEN;
int color = ((ColorDrawable) mActivity.getWindow().getDecorView().getBackground()).getColor();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && isColorLight(color))
flags |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
mActivity.getWindow().getDecorView().setSystemUiVisibility(flags);
} }
} }

View File

@@ -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,10 +44,8 @@ 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;
@@ -53,26 +54,32 @@ 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 +105,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 +134,10 @@ 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;
private final BroadcastReceiver mBroadcastReceiever = new BroadcastReceiver() { private final BroadcastReceiver mBroadcastReceiever = new BroadcastReceiver() {
@Override @Override
@@ -134,20 +145,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,135 +209,88 @@ 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: mSettings = new TermuxPreferences(this);
getWindow().getDecorView().setBackground(null);
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) {
Resources res = getResources(); DialogUtils.textInput(TermuxActivity.this, R.string.session_new_named_title, null, R.string.session_new_named_positive_button,
DialogUtils.textInput(TermuxActivity.this, R.string.session_new_named_title, R.string.session_new_named_positive_button, null,
new DialogUtils.TextSetListener() { new DialogUtils.TextSetListener() {
@Override @Override
public void onTextSet(String text) { public void onTextSet(String text) {
@@ -303,7 +302,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
addNewSession(true, text); addNewSession(true, text);
} }
} }
); , -1, null, null);
return true; return true;
} }
}); });
@@ -317,18 +316,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
@@ -368,7 +386,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();
} }
@@ -378,7 +397,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
if (!mIsVisible) return; if (!mIsVisible) return;
showToast("Clipboard:\n\"" + text + "\"", false); showToast("Clipboard:\n\"" + text + "\"", false);
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
clipboard.setPrimaryClip(new ClipData(null, new String[] { "text/plain" }, new ClipData.Item(text))); clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(text)));
} }
@Override @Override
@@ -391,10 +410,18 @@ 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();
}
}; };
ListView listView = (ListView) findViewById(R.id.left_drawer_list); ListView listView = (ListView) findViewById(R.id.left_drawer_list);
@@ -490,15 +517,25 @@ public final class TermuxActivity extends Activity implements ServiceConnection
} }
} }
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) {
DialogUtils.textInput(this, R.string.session_rename_title, R.string.session_rename_positive_button, sessionToRename.mSessionName, DialogUtils.textInput(this, R.string.session_rename_title, sessionToRename.mSessionName, R.string.session_rename_positive_button, new DialogUtils.TextSetListener() {
new DialogUtils.TextSetListener() {
@Override @Override
public void onTextSet(String text) { public void onTextSet(String text) {
sessionToRename.mSessionName = text; sessionToRename.mSessionName = text;
} }
}, -1, null); }, -1, null, -1, null, null);
} }
@Override @Override
@@ -509,7 +546,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
} }
} }
@Nullable TerminalSession getCurrentTermSession() { @Nullable
TerminalSession getCurrentTermSession() {
return mTerminalView.getCurrentSession(); return mTerminalView.getCurrentSession();
} }
@@ -525,6 +563,10 @@ public final class TermuxActivity extends Activity implements ServiceConnection
} }
registerReceiver(mBroadcastReceiever, new IntentFilter(RELOAD_STYLE_ACTION)); registerReceiver(mBroadcastReceiever, new IntentFilter(RELOAD_STYLE_ACTION));
// The current terminal session may have changed while being away, force
// a refresh of the displayed terminal:
mTerminalView.onScreenUpdated();
} }
@Override @Override
@@ -539,11 +581,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() {
@@ -577,7 +620,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) {
@@ -614,7 +660,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);
@@ -661,7 +707,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
public void onClick(DialogInterface di, int which) { public void onClick(DialogInterface di, int which) {
String url = (String) urls[which]; String url = (String) urls[which];
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
clipboard.setPrimaryClip(new ClipData(null, new String[] { "text/plain" }, new ClipData.Item(url))); clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(url)));
Toast.makeText(TermuxActivity.this, R.string.select_url_copied_to_clipboard, Toast.LENGTH_LONG).show(); Toast.makeText(TermuxActivity.this, R.string.select_url_copied_to_clipboard, Toast.LENGTH_LONG).show();
} }
}).setTitle(R.string.select_url_dialog_title).create(); }).setTitle(R.string.select_url_dialog_title).create();
@@ -676,7 +722,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;
} }
}); });
@@ -732,9 +784,11 @@ public final class TermuxActivity extends Activity implements ServiceConnection
stylingIntent.setClassName("com.termux.styling", "com.termux.styling.TermuxStyleActivity"); stylingIntent.setClassName("com.termux.styling", "com.termux.styling.TermuxStyleActivity");
try { try {
startActivity(stylingIntent); startActivity(stylingIntent);
} catch (ActivityNotFoundException e) { } catch (ActivityNotFoundException | IllegalArgumentException e) {
// The startActivity() call is not documented to throw IllegalArgumentException.
// However, crash reporting shows that it sometimes does, so catch it here.
new AlertDialog.Builder(this).setMessage(R.string.styling_not_installed) 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")));
@@ -777,7 +831,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. */
@@ -797,4 +852,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,14 +184,25 @@ 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 termuxArch = null;
if (arch.startsWith("arm") || arch.equals("aarch64")) { // Note that we cannot use System.getProperty("os.arch") since that may give e.g. "aarch64"
// Handle different arm variants such as armv7l: // while a 64-bit runtime may not be installed (like on the Samsung Galaxy S5 Neo).
arch = "arm"; // Instead we search through the supported abi:s on the device, see:
} else if (arch.equals("x86_64")) { // http://developer.android.com/ndk/guides/abis.html
arch = "i686"; // Note that we search for abi:s in preferred order, and want to avoid installing arm on
// an x86 system where arm emulation is available.
final String[] androidArchNames = {"arm64-v8a", "x86_64", "x86", "armeabi-v7a"};
final String[] termuxArchNames = {"aarch64", "x86_64", "i686", "arm"};
final List<String> supportedArches = Arrays.asList(Build.SUPPORTED_ABIS);
for (int i = 0; i < termuxArchNames.length; i++) {
if (supportedArches.contains(androidArchNames[i])) {
termuxArch = termuxArchNames[i];
break;
} }
return new URL("https://termux.net/bootstrap/bootstrap-" + arch + ".zip"); }
return new URL("https://termux.net/bootstrap/bootstrap-" + termuxArch + ".zip");
} }
/** Delete a folder and all its content or throw. */ /** Delete a folder and all its content or throw. */

View File

@@ -0,0 +1,277 @@
package com.termux.app;
import android.content.Context;
import android.media.AudioManager;
import android.support.v4.widget.DrawerLayout;
import android.util.Log;
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.isShiftPressed()) {
// 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':
resultingKeyCode = KeyEvent.KEYCODE_MOVE_HOME;
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() {
@@ -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;
@@ -26,15 +16,25 @@ 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()}.
*/ */
@@ -55,7 +55,11 @@ public final class TermuxService extends Service implements SessionChangedCallba
/** Intent action to toggle the wifi lock, {@link #mWifiLock}, which this service may hold. */ /** 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"; 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. */
private static final String ACTION_EXECUTE = "com.termux.service_execute"; public static final String ACTION_EXECUTE = "com.termux.service_execute";
public static final String EXTRA_ARGUMENTS = "com.termux.execute.arguments";
public static final String EXTRA_CURRENT_WORKING_DIRECTORY = "com.termux.execute.cwd";
/** This service is only bound from inside the same process and never uses IPC. */ /** This service is only bound from inside the same process and never uses IPC. */
class LocalBinder extends Binder { class LocalBinder extends Binder {
@@ -66,7 +70,7 @@ public final class TermuxService extends Service implements SessionChangedCallba
/** /**
* 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()} }.
*/ */
@@ -113,8 +117,8 @@ public final class TermuxService extends Service implements SessionChangedCallba
} 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("com.termux.execute.arguments")); String[] arguments = (executableUri == null ? null : intent.getStringArrayExtra(EXTRA_ARGUMENTS));
String cwd = intent.getStringExtra("com.termux.execute.cwd"); String cwd = intent.getStringExtra(EXTRA_CURRENT_WORKING_DIRECTORY);
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".
@@ -237,17 +241,22 @@ public final class TermuxService extends Service implements SessionChangedCallba
final String prefixEnv = "PREFIX=" + PREFIX_PATH; final String prefixEnv = "PREFIX=" + PREFIX_PATH;
final String androidRootEnv = "ANDROID_ROOT=" + System.getenv("ANDROID_ROOT"); final String androidRootEnv = "ANDROID_ROOT=" + System.getenv("ANDROID_ROOT");
final String androidDataEnv = "ANDROID_DATA=" + System.getenv("ANDROID_DATA"); 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; String[] env;
if (failSafe) { if (failSafe) {
env = new String[] { termEnv, homeEnv, prefixEnv, androidRootEnv, androidDataEnv }; // Keep the default path so that system binaries can be used in the failsafe session.
final String pathEnv = "PATH=" + System.getenv("PATH");
env = new String[]{termEnv, homeEnv, prefixEnv, androidRootEnv, androidDataEnv, pathEnv, externalStorageEnv};
} else { } else {
final String ps1Env = "PS1=$ "; final String ps1Env = "PS1=$ ";
final String ldEnv = "LD_LIBRARY_PATH=" + PREFIX_PATH + "/lib"; final String ldEnv = "LD_LIBRARY_PATH=" + PREFIX_PATH + "/lib";
final String langEnv = "LANG=en_US.UTF-8"; final String langEnv = "LANG=en_US.UTF-8";
final String pathEnv = "PATH=" + PREFIX_PATH + "/bin:" + PREFIX_PATH + "/bin/applets:" + System.getenv("PATH"); final String pathEnv = "PATH=" + PREFIX_PATH + "/bin:" + PREFIX_PATH + "/bin/applets";
final String pwdEnv = "PWD=" + cwd; final String pwdEnv = "PWD=" + cwd;
env = new String[] { termEnv, homeEnv, prefixEnv, ps1Env, ldEnv, langEnv, pathEnv, pwdEnv, androidRootEnv, androidDataEnv }; env = new String[]{termEnv, homeEnv, prefixEnv, ps1Env, ldEnv, langEnv, pathEnv, pwdEnv, androidRootEnv, androidDataEnv, externalStorageEnv};
} }
String shellName; String shellName;
@@ -268,7 +277,7 @@ public final class TermuxService extends Service implements SessionChangedCallba
if (executablePath == null) { if (executablePath == null) {
// Try bash, zsh and ash in that order: // Try bash, zsh and ash in that order:
for (String shellBinary : new String[] { "bash", "zsh", "ash" }) { for (String shellBinary : new String[]{"bash", "zsh", "ash"}) {
File shellFile = new File(PREFIX_PATH + "/bin/" + shellBinary); File shellFile = new File(PREFIX_PATH + "/bin/" + shellBinary);
if (shellFile.canExecute()) { if (shellFile.canExecute()) {
executablePath = shellFile.getAbsolutePath(); executablePath = shellFile.getAbsolutePath();
@@ -291,7 +300,7 @@ public final class TermuxService extends Service implements SessionChangedCallba
String[] args; String[] args;
if (arguments == null) { if (arguments == null) {
args = new String[] { shellName }; args = new String[]{shellName};
} else { } else {
args = new String[arguments.length + 1]; args = new String[arguments.length + 1];
args[0] = shellName; args[0] = shellName;
@@ -325,7 +334,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
@@ -343,4 +353,9 @@ 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);
}
} }

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.drawable.ic_launcher);
return result;
}
@Override
public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException {
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
includeFile(result, documentId, null);
return result;
}
@Override
public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException {
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
final File parent = getFileForDocId(parentDocumentId);
for (File file : parent.listFiles()) {
if (!file.getName().startsWith(".")) {
includeFile(result, null, file);
}
}
return result;
}
@Override
public ParcelFileDescriptor openDocument(final String documentId, String mode, CancellationSignal signal) throws FileNotFoundException {
final File file = getFileForDocId(documentId);
final int accessMode = ParcelFileDescriptor.parseMode(mode);
return ParcelFileDescriptor.open(file, accessMode);
}
@Override
public AssetFileDescriptor openDocumentThumbnail(String documentId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException {
final File file = getFileForDocId(documentId);
final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
return new AssetFileDescriptor(pfd, 0, file.length());
}
@Override
public boolean onCreate() {
return true;
}
@Override
public void deleteDocument(String documentId) throws FileNotFoundException {
File file = getFileForDocId(documentId);
if (!file.delete()) {
throw new FileNotFoundException("Failed to delete document with id " + documentId);
}
}
@Override
public String getDocumentType(String documentId) throws FileNotFoundException {
File file = getFileForDocId(documentId);
return getMimeType(file);
}
@Override
public Cursor querySearchDocuments(String rootId, String query, String[] projection) throws FileNotFoundException {
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
final File parent = getFileForDocId(rootId);
// This example implementation searches file names for the query and doesn't rank search
// results, so we can stop as soon as we find a sufficient number of matches. Other
// implementations might rank results and use other data about files, rather than the file
// name, to produce a match.
final LinkedList<File> pending = new LinkedList<>();
pending.add(parent);
final int MAX_SEARCH_RESULTS = 50;
while (!pending.isEmpty() && result.getCount() < MAX_SEARCH_RESULTS) {
final File file = pending.removeFirst();
// Avoid folders outside the $HOME folders linked in to symlinks (to avoid e.g. search
// through the whole SD card).
boolean isInsideHome;
try {
isInsideHome = file.getCanonicalPath().startsWith(TermuxService.HOME_PATH);
} catch (IOException e) {
isInsideHome = true;
}
final boolean isHidden = file.getName().startsWith(".");
if (isInsideHome && !isHidden) {
if (file.isDirectory()) {
Collections.addAll(pending, file.listFiles());
} else {
if (file.getName().toLowerCase().contains(query)) {
includeFile(result, null, file);
}
}
}
}
return result;
}
/**
* Get the document id given a file. This document id must be consistent across time as other
* applications may save the ID and use it to reference documents later.
* <p/>
* The reverse of @{link #getFileForDocId}.
*/
private static String getDocIdForFile(File file) {
return file.getAbsolutePath();
}
/**
* Get the file given a document id (the reverse of {@link #getDocIdForFile(File)}).
*/
private static File getFileForDocId(String docId) throws FileNotFoundException {
final File f = new File(docId);
if (!f.exists()) throw new FileNotFoundException(f.getAbsolutePath() + " not found");
return f;
}
private static String getMimeType(File file) {
if (file.isDirectory()) {
return Document.MIME_TYPE_DIR;
} else {
final String name = file.getName();
final int lastDot = name.lastIndexOf('.');
if (lastDot >= 0) {
final String extension = name.substring(lastDot + 1);
final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
if (mime != null) return mime;
}
return "application/octet-stream";
}
}
/**
* Add a representation of a file to a cursor.
*
* @param result the cursor to modify
* @param docId the document ID representing the desired file (may be null if given file)
* @param file the File object representing the desired file (may be null if given docID)
*/
private void includeFile(MatrixCursor result, String docId, File file)
throws FileNotFoundException {
if (docId == null) {
docId = getDocIdForFile(file);
} else {
file = getFileForDocId(docId);
}
int flags = 0;
if (file.isDirectory()) {
if (file.isDirectory() && file.canWrite()) flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
} else if (file.canWrite()) {
flags |= Document.FLAG_SUPPORTS_WRITE | Document.FLAG_SUPPORTS_DELETE;
}
final String displayName = file.getName();
final String mimeType = getMimeType(file);
if (mimeType.startsWith("image/")) flags |= Document.FLAG_SUPPORTS_THUMBNAIL;
final MatrixCursor.RowBuilder row = result.newRow();
row.add(Document.COLUMN_DOCUMENT_ID, docId);
row.add(Document.COLUMN_DISPLAY_NAME, displayName);
row.add(Document.COLUMN_SIZE, file.length());
row.add(Document.COLUMN_MIME_TYPE, mimeType);
row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified());
row.add(Document.COLUMN_FLAGS, flags);
row.add(Document.COLUMN_ICON, R.drawable.ic_launcher);
}
}

View File

@@ -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,51 +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 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 false;
}
@Override
public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
return null;
}
@Override
public String getType(@NonNull Uri uri) {
return null;
}
@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

@@ -0,0 +1,217 @@
package com.termux.filepicker;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.provider.OpenableColumns;
import android.util.Log;
import android.util.Patterns;
import com.termux.R;
import com.termux.app.DialogUtils;
import com.termux.app.TermuxService;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
public class TermuxFileReceiverActivity extends Activity {
static final String TERMUX_RECEIVEDIR = TermuxService.FILES_PATH + "/home/downloads";
static final String EDITOR_PROGRAM = TermuxService.HOME_PATH + "/bin/termux-file-editor";
static final String URL_OPENER_PROGRAM = TermuxService.HOME_PATH + "/bin/termux-url-opener";
/**
* If the activity should be finished when the name input dialog is dismissed. This is disabled
* before showing an error dialog, since the act of showing the error dialog will cause the
* name input dialog to be implicitly dismissed, and we do not want to finish the activity directly
* when showing the error dialog.
*/
boolean mFinishOnDismissNameDialog = true;
@Override
protected void onResume() {
super.onResume();
final Intent intent = getIntent();
final String action = intent.getAction();
final String type = intent.getType();
final String scheme = intent.getScheme();
if (Intent.ACTION_SEND.equals(action) && type != null) {
final String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
final Uri sharedUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
if (sharedText != null) {
if (Patterns.WEB_URL.matcher(sharedText).matches()) {
handleUrlAndFinish(sharedText);
} else {
String subject = intent.getStringExtra(Intent.EXTRA_SUBJECT);
if (subject == null) subject = intent.getStringExtra(Intent.EXTRA_TITLE);
if (subject != null) subject += ".txt";
promptNameAndSave(new ByteArrayInputStream(sharedText.getBytes(StandardCharsets.UTF_8)), subject);
}
} else if (sharedUri != null) {
handleContentUri(sharedUri, intent.getStringExtra(Intent.EXTRA_TITLE));
} else {
showErrorDialogAndQuit("Send action without content - nothing to save.");
}
} else if ("content".equals(scheme)) {
handleContentUri(intent.getData(), intent.getStringExtra(Intent.EXTRA_TITLE));
} else if ("file".equals(scheme)) {
// When e.g. clicking on a downloaded apk:
String path = intent.getData().getPath();
File file = new File(path);
try {
FileInputStream in = new FileInputStream(file);
promptNameAndSave(in, file.getName());
} catch (FileNotFoundException e) {
showErrorDialogAndQuit("Cannot open file: " + e.getMessage() + ".");
}
} else {
showErrorDialogAndQuit("Unable to receive any file or URL.");
}
}
void showErrorDialogAndQuit(String message) {
mFinishOnDismissNameDialog = false;
new AlertDialog.Builder(this).setMessage(message).setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog) {
finish();
}
}).setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
finish();
}
}).show();
}
void handleContentUri(final Uri uri, String subjectFromIntent) {
try {
String attachmentFileName = null;
String[] projection = new String[]{OpenableColumns.DISPLAY_NAME};
try (Cursor c = getContentResolver().query(uri, projection, null, null, null)) {
if (c != null && c.moveToFirst()) {
final int fileNameColumnId = c.getColumnIndex(OpenableColumns.DISPLAY_NAME);
if (fileNameColumnId >= 0) attachmentFileName = c.getString(fileNameColumnId);
}
}
if (attachmentFileName == null) attachmentFileName = subjectFromIntent;
InputStream in = getContentResolver().openInputStream(uri);
promptNameAndSave(in, attachmentFileName);
} catch (Exception e) {
showErrorDialogAndQuit("Unable to handle shared content:\n\n" + e.getMessage());
Log.e("termux", "handleContentUri(uri=" + uri + ") failed", e);
}
}
void promptNameAndSave(final InputStream in, final String attachmentFileName) {
DialogUtils.textInput(this, R.string.file_received_title, attachmentFileName, R.string.file_received_edit_button, new DialogUtils.TextSetListener() {
@Override
public void onTextSet(String text) {
File outFile = saveStreamWithName(in, text);
if (outFile == null) return;
final File editorProgramFile = new File(EDITOR_PROGRAM);
if (!editorProgramFile.isFile()) {
showErrorDialogAndQuit("The following file does not exist:\n$HOME/bin/termux-file-editor\n\n"
+ "Create this file as a script or a symlink - it will be called with the received file as only argument.");
return;
}
// Do this for the user if necessary:
//noinspection ResultOfMethodCallIgnored
editorProgramFile.setExecutable(true);
final Uri scriptUri = new Uri.Builder().scheme("file").path(EDITOR_PROGRAM).build();
Intent executeIntent = new Intent(TermuxService.ACTION_EXECUTE, scriptUri);
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
executeIntent.putExtra(TermuxService.EXTRA_ARGUMENTS, new String[]{outFile.getAbsolutePath()});
startService(executeIntent);
finish();
}
},
R.string.file_received_open_folder_button, new DialogUtils.TextSetListener() {
@Override
public void onTextSet(String text) {
if (saveStreamWithName(in, text) == null) return;
Intent executeIntent = new Intent(TermuxService.ACTION_EXECUTE);
executeIntent.putExtra(TermuxService.EXTRA_CURRENT_WORKING_DIRECTORY, TERMUX_RECEIVEDIR);
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
startService(executeIntent);
finish();
}
},
android.R.string.cancel, new DialogUtils.TextSetListener() {
@Override
public void onTextSet(final String text) {
finish();
}
}, new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog) {
if (mFinishOnDismissNameDialog) finish();
}
});
}
public File saveStreamWithName(InputStream in, String attachmentFileName) {
File receiveDir = new File(TERMUX_RECEIVEDIR);
if (!receiveDir.isDirectory() && !receiveDir.mkdirs()) {
showErrorDialogAndQuit("Cannot create directory: " + receiveDir.getAbsolutePath());
return null;
}
try {
final File outFile = new File(receiveDir, attachmentFileName);
try (FileOutputStream f = new FileOutputStream(outFile)) {
byte[] buffer = new byte[4096];
int readBytes;
while ((readBytes = in.read(buffer)) > 0) {
f.write(buffer, 0, readBytes);
}
}
return outFile;
} catch (IOException e) {
showErrorDialogAndQuit("Error saving file:\n\n" + e);
Log.e("termux", "Error saving file", e);
return null;
}
}
void handleUrlAndFinish(final String url) {
final File urlOpenerProgramFile = new File(URL_OPENER_PROGRAM);
if (!urlOpenerProgramFile.isFile()) {
showErrorDialogAndQuit("The following file does not exist:\n$HOME/bin/termux-url-opener\n\n"
+ "Create this file as a script or a symlink - it will be called with the shared URL as only argument.");
return;
}
// Do this for the user if necessary:
//noinspection ResultOfMethodCallIgnored
urlOpenerProgramFile.setExecutable(true);
final Uri urlOpenerProgramUri = new Uri.Builder().scheme("file").path(URL_OPENER_PROGRAM).build();
Intent executeIntent = new Intent(TermuxService.ACTION_EXECUTE, urlOpenerProgramUri);
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
executeIntent.putExtra(TermuxService.EXTRA_ARGUMENTS, new String[]{url});
startService(executeIntent);
finish();
}
}

View File

@@ -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,23 +12,18 @@ 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.
*/ */
public static native int createSubprocess(String cmd, String cwd, String[] args, String[] envVars, int[] processId); public static native int createSubprocess(String cmd, String cwd, String[] args, String[] envVars, int[] processId, int rows, int columns);
/** Set the window size for a given pty, which allows connected programs to learn how large their screen is. */ /** Set the window size for a given pty, which allows connected programs to learn how large their screen is. */
public static native void setPtyWindowSize(int fd, int rows, int cols); public static native void setPtyWindowSize(int fd, int rows, int cols);
@@ -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;
@@ -22,6 +26,7 @@ import static android.view.KeyEvent.KEYCODE_F7;
import static android.view.KeyEvent.KEYCODE_F8; import static android.view.KeyEvent.KEYCODE_F8;
import static android.view.KeyEvent.KEYCODE_F9; 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_HOME;
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_NUMPAD_0; import static android.view.KeyEvent.KEYCODE_NUMPAD_0;
@@ -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,6 +61,7 @@ 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
@@ -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_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_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,7 @@ 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_HOME:
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 +209,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:
@@ -224,12 +225,13 @@ public final class KeyHandler {
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";

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) {
@@ -63,7 +60,7 @@ public final class TerminalBuffer {
int x2Index = (x2 < mColumns) ? lineObject.findStartOfColumn(x2) : lineObject.getSpaceUsed(); int x2Index = (x2 < mColumns) ? lineObject.findStartOfColumn(x2) : lineObject.getSpaceUsed();
if (x2Index == x1Index) { if (x2Index == x1Index) {
// Selected the start of a wide character. // Selected the start of a wide character.
x2Index = lineObject.findStartOfColumn(x2+1); x2Index = lineObject.findStartOfColumn(x2 + 1);
} }
char[] line = lineObject.mText; char[] line = lineObject.mText;
int lastPrintingCharIndex = -1; int lastPrintingCharIndex = -1;
@@ -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) {
@@ -127,20 +124,21 @@ public final class TerminalBuffer {
mLines[externalToInternalRow(row)].mLineWrap = true; mLines[externalToInternalRow(row)].mLineWrap = true;
} }
private boolean getLineWrap(int row) { public boolean getLineWrap(int row) {
return mLines[externalToInternalRow(row)].mLineWrap; return mLines[externalToInternalRow(row)].mLineWrap;
} }
public void clearLineWrap(int row) {
mLines[externalToInternalRow(row)].mLineWrap = false;
}
/** /**
* Resize the screen which this transcript backs. Currently, this only works if the number of columns does not * 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, int 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):
@@ -234,7 +232,8 @@ 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;
@@ -298,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;
@@ -320,12 +317,9 @@ 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, int style) {
if (topMargin > bottomMargin - 1 || topMargin < 0 || bottomMargin > mScreenRows) if (topMargin > bottomMargin - 1 || topMargin < 0 || bottomMargin > mScreenRows)
@@ -356,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;

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, 0xffffffff};
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;
@@ -294,8 +294,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)) {
@@ -307,7 +306,7 @@ public final class TerminalEmulator {
// Clip to screen, and clip to the limits of 8-bit data. // Clip to screen, and clip to the limits of 8-bit data.
boolean out_of_bounds = column < 1 || row < 1 || column > mColumns || row > mRows || column > 255 - 32 || row > 255 - 32; boolean out_of_bounds = column < 1 || row < 1 || column > mColumns || row > mRows || column > 255 - 32 || row > 255 - 32;
if (!out_of_bounds) { if (!out_of_bounds) {
byte[] data = { '\033', '[', 'M', (byte) (32 + mouseButton), (byte) (32 + column), (byte) (32 + row) }; byte[] data = {'\033', '[', 'M', (byte) (32 + mouseButton), (byte) (32 + column), (byte) (32 + row)};
mSession.write(data, 0, data.length); mSession.write(data, 0, data.length);
} }
} }
@@ -341,7 +340,7 @@ public final class TerminalEmulator {
} }
private void resizeScreen() { private void resizeScreen() {
final int[] cursor = { mCursorCol, mCursorRow }; final int[] cursor = {mCursorCol, mCursorRow};
int newTotalRows = (mScreen == mAltBuffer) ? mRows : mMainBuffer.mTotalRows; int newTotalRows = (mScreen == mAltBuffer) ? mRows : mMainBuffer.mTotalRows;
mScreen.resize(mColumns, mRows, newTotalRows, cursor, getStyle(), isAlternateBufferActive()); mScreen.resize(mColumns, mRows, newTotalRows, cursor, getStyle(), isAlternateBufferActive());
mCursorCol = cursor[0]; mCursorCol = cursor[0];
@@ -390,10 +389,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++)
@@ -474,15 +471,27 @@ public final class TerminalEmulator {
else else
mSession.onBell(); mSession.onBell();
break; break;
case 8: // BS case 8: // Backspace (BS, ^H).
setCursorCol(Math.max(mLeftMargin, mCursorCol - 1)); if (mLeftMargin == mCursorCol) {
break; // Jump to previous line if it was auto-wrapped.
case 9: // Horizontal tab - move to next tab stop, but not past edge of screen int previousRow = mCursorRow - 1;
int nextTabStop = nextTabStop(1); if (previousRow >= 0 && mScreen.getLineWrap(previousRow)) {
while (mCursorCol < nextTabStop) { mScreen.clearLineWrap(previousRow);
// Emit newlines to get background color right. setCursorRowCol(previousRow, mRightMargin - 1);
processCodePoint(' ');
} }
} else {
setCursorCol(mCursorCol - 1);
}
break;
case 9: // Horizontal tab (HT, \t) - move to next tab stop, but not past edge of screen
// XXX: Should perhaps use color if writing to new cells. Try with
// printf "\033[41m\tXX\033[0m\n"
// The OSX Terminal.app colors the spaces from the tab red, but xterm does not.
// Note that Terminal.app only colors on new cells, in e.g.
// printf "\033[41m\t\r\033[42m\tXX\033[0m\n"
// the first cells are created with a red background, but when tabbing over
// them again with a green background they are not overwritten.
mCursorCol = nextTabStop(1);
break; break;
case 10: // Line feed (LF, \n). case 10: // Line feed (LF, \n).
case 11: // Vertical tab (VT, \v). case 11: // Vertical tab (VT, \v).
@@ -885,7 +894,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();
} }
@@ -1074,7 +1084,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;
} }
@@ -1530,7 +1541,7 @@ public final class TerminalEmulator {
switch (getArg0(0)) { switch (getArg0(0)) {
case 5: // Device status report (DSR): case 5: // Device status report (DSR):
// Answer is ESC [ 0 n (Terminal OK). // Answer is ESC [ 0 n (Terminal OK).
byte[] dsr = { (byte) 27, (byte) '[', (byte) '0', (byte) 'n' }; byte[] dsr = {(byte) 27, (byte) '[', (byte) '0', (byte) 'n'};
mSession.write(dsr, 0, dsr.length); mSession.write(dsr, 0, dsr.length);
break; break;
case 6: // Cursor position report (CPR): case 6: // Cursor position report (CPR):
@@ -1709,7 +1720,8 @@ public final class TerminalEmulator {
mBackColor = color; mBackColor = color;
} }
} 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);
} }
} }
} }
@@ -1724,7 +1736,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));
} }
} }
} }
@@ -1792,7 +1805,7 @@ public final class TerminalEmulator {
// and specification can be given in one control sequence, xterm can make more than one reply. // and specification can be given in one control sequence, xterm can make more than one reply.
int colorIndex = -1; int colorIndex = -1;
int parsingPairStart = -1; int parsingPairStart = -1;
for (int i = 0;; i++) { for (int i = 0; ; i++) {
boolean endOfInput = i == textParameter.length(); boolean endOfInput = i == textParameter.length();
char b = endOfInput ? ';' : textParameter.charAt(i); char b = endOfInput ? ';' : textParameter.charAt(i);
if (b == ';') { if (b == ';') {
@@ -1804,6 +1817,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;
} }
@@ -1824,7 +1838,7 @@ public final class TerminalEmulator {
case 12: // Set cursor color. case 12: // Set cursor color.
int specialIndex = TextStyle.COLOR_INDEX_FOREGROUND + (value - 10); int specialIndex = TextStyle.COLOR_INDEX_FOREGROUND + (value - 10);
int lastSemiIndex = 0; int lastSemiIndex = 0;
for (int charIndex = 0;; charIndex++) { for (int charIndex = 0; ; charIndex++) {
boolean endOfInput = charIndex == textParameter.length(); boolean endOfInput = charIndex == textParameter.length();
if (endOfInput || textParameter.charAt(charIndex) == ';') { if (endOfInput || textParameter.charAt(charIndex) == ';') {
try { try {
@@ -1839,9 +1853,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.
@@ -1865,14 +1881,16 @@ 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++) {
boolean endOfInput = charIndex == textParameter.length(); boolean endOfInput = charIndex == textParameter.length();
if (endOfInput || textParameter.charAt(charIndex) == ';') { if (endOfInput || textParameter.charAt(charIndex) == ';') {
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;
@@ -1887,6 +1905,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;
@@ -2058,8 +2077,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) {
@@ -2190,13 +2208,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);
} }
@@ -2261,6 +2281,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 {
@@ -99,7 +99,7 @@ public final class TerminalRow {
} }
private boolean wideDisplayCharacterStartingAt(int column) { private boolean wideDisplayCharacterStartingAt(int column) {
for (int currentCharIndex = 0, currentColumn = 0; currentCharIndex < mSpaceUsed;) { for (int currentCharIndex = 0, currentColumn = 0; currentCharIndex < mSpaceUsed; ) {
char c = mText[currentCharIndex++]; char c = mText[currentCharIndex++];
int codePoint = Character.isHighSurrogate(c) ? Character.toCodePoint(c, mText[currentCharIndex++]) : c; int codePoint = Character.isHighSurrogate(c) ? Character.toCodePoint(c, mText[currentCharIndex++]) : c;
int wcwidth = WcWidth.width(codePoint); int wcwidth = WcWidth.width(codePoint);

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) {
@@ -82,14 +88,17 @@ public final class TerminalSession extends TerminalOutput {
/** Callback which gets notified when a session finishes or changes title. */ /** Callback which gets notified when a session finishes or changes title. */
final SessionChangedCallback mChangeCallback; final SessionChangedCallback mChangeCallback;
/** The pid of the shell process or -1 if not running. */ /** The pid of the shell process. 0 if not started and -1 if finished running. */
int mShellPid; int mShellPid;
int mShellExitStatus = -1;
/** The exit status of the shell process. Only valid if ${@link #mShellPid} is -1. */
int mShellExitStatus;
/** /**
* The file descriptor referencing the master half of a pseudo-terminal pair, resulting from calling * The file descriptor referencing the master half of a pseudo-terminal pair, resulting from calling
* {@link JNI#createSubprocess(String, String, String[], String[], int[])}. * {@link JNI#createSubprocess(String, String, String[], String[], int[], int, int)}.
*/ */
final int mTerminalFileDescriptor; private int mTerminalFileDescriptor;
/** Set by the application for user identification of session, not by terminal. */ /** Set by the application for user identification of session, not by terminal. */
public String mSessionName; public String mSessionName;
@@ -128,20 +137,26 @@ public final class TerminalSession extends TerminalOutput {
} }
}; };
private final String mShellPath;
private final String mCwd;
private final String[] mArgs;
private final String[] mEnv;
public TerminalSession(String shellPath, String cwd, String[] args, String[] env, SessionChangedCallback changeCallback) { public TerminalSession(String shellPath, String cwd, String[] args, String[] env, SessionChangedCallback changeCallback) {
mChangeCallback = changeCallback; mChangeCallback = changeCallback;
int[] processId = new int[1]; this.mShellPath = shellPath;
mTerminalFileDescriptor = JNI.createSubprocess(shellPath, cwd, args, env, processId); this.mCwd = cwd;
mShellPid = processId[0]; this.mArgs = args;
this.mEnv = env;
} }
/** Inform the attached pty of the new size and reflow or initialize the emulator. */ /** Inform the attached pty of the new size and reflow or initialize the emulator. */
public void updateSize(int columns, int rows) { public void updateSize(int columns, int rows) {
JNI.setPtyWindowSize(mTerminalFileDescriptor, rows, columns);
if (mEmulator == null) { if (mEmulator == null) {
initializeEmulator(columns, rows); initializeEmulator(columns, rows);
} else { } else {
JNI.setPtyWindowSize(mTerminalFileDescriptor, rows, columns);
mEmulator.resize(columns, rows); mEmulator.resize(columns, rows);
} }
} }
@@ -154,13 +169,16 @@ 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= */5000);
int[] processId = new int[1];
mTerminalFileDescriptor = JNI.createSubprocess(mShellPath, mCwd, mArgs, mEnv, processId, rows, columns);
mShellPid = processId[0];
final FileDescriptor terminalFileDescriptorWrapped = wrapFileDescriptor(mTerminalFileDescriptor); final FileDescriptor terminalFileDescriptorWrapped = wrapFileDescriptor(mTerminalFileDescriptor);
new Thread("TermSessionInputReader[pid=" + mShellPid + "]") { new Thread("TermSessionInputReader[pid=" + mShellPid + "]") {
@@ -176,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();
@@ -199,12 +213,21 @@ 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. */
@Override @Override
public void write(byte[] data, int offset, int count) { public void write(byte[] data, int offset, int count) {
mTerminalToProcessIOQueue.write(data, offset, count); if (mShellPid > 0) mTerminalToProcessIOQueue.write(data, offset, count);
} }
/** Write the Unicode code point to the terminal encoded in UTF-8. */ /** Write the Unicode code point to the terminal encoded in UTF-8. */
@@ -259,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);
} }
} }
@@ -311,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

@@ -3,7 +3,7 @@ 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 32 bit integer, 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 foreground and background colors take 9 bits each, leaving (32-9-9)=14 bits for effect flags. Using 9 for now
* (the different CHARACTER_ATTRIBUTE_* bits). * (the different CHARACTER_ATTRIBUTE_* bits).
*/ */
@@ -18,7 +18,7 @@ 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.
*/ */

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 {
@@ -64,8 +64,8 @@ final class TerminalRenderer {
final TerminalBuffer screen = mEmulator.getScreen(); final TerminalBuffer screen = mEmulator.getScreen();
final int[] palette = mEmulator.mColors.mCurrentColors; final int[] palette = mEmulator.mColors.mCurrentColors;
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++) {
@@ -90,7 +90,7 @@ final class TerminalRenderer {
int currentCharIndex = 0; int currentCharIndex = 0;
float measuredWidthForRun = 0.f; float measuredWidthForRun = 0.f;
for (int column = 0; column < columns;) { for (int column = 0; column < columns; ) {
final char charAtIndex = line[currentCharIndex]; final char charAtIndex = line[currentCharIndex];
final boolean charIsHighsurrogate = Character.isHighSurrogate(charAtIndex); final boolean charIsHighsurrogate = Character.isHighSurrogate(charAtIndex);
final int charsForCodePoint = charIsHighsurrogate ? 2 : 1; final int charsForCodePoint = charIsHighsurrogate ? 2 : 1;
@@ -141,28 +141,17 @@ final class TerminalRenderer {
} }
/** /**
* @param canvas * @param canvas the canvas to render on
* the canvas to render on * @param palette the color palette to look up colors from textStyle
* @param palette * @param y height offset into the canvas where to render the line: line * {@link #mFontLineSpacing}
* the color palette to look up colors from textStyle * @param startColumn the run offset in columns
* @param y * @param runWidthColumns the run width in columns - this is computed from wcwidth() and may not be what the font measures to
* height offset into the canvas where to render the line: line * {@link #mFontLineSpacing} * @param text the java char array to render text from
* @param startColumn * @param startCharIndex index into the text array where to start
* the run offset in columns * @param runWidthChars number of java characters from the text array to render
* @param runWidthColumns * @param cursor true if rendering a cursor or selection
* the run width in columns - this is computed from wcwidth() and may not be what the font measures to * @param textStyle the background, foreground and effect encoded using {@link TextStyle}
* @param text * @param reverseVideo if the screen is rendered with the global reverse video flag set
* 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, 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) { float mes, boolean cursor, int textStyle, boolean reverseVideo) {

View File

@@ -5,12 +5,10 @@ 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.InputType; import android.text.InputType;
import android.text.TextUtils; import android.text.TextUtils;
@@ -34,20 +32,14 @@ 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 {
/** Log view key and IME events. */ /** Log view key and IME events. */
private static final boolean LOG_KEY_EVENTS = true; private static final boolean LOG_KEY_EVENTS = false;
/** The currently displayed terminal session, whose emulator is {@link #mEmulator}. */ /** The currently displayed terminal session, whose emulator is {@link #mEmulator}. */
TerminalSession mTermSession; TerminalSession mTermSession;
@@ -61,9 +53,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 +79,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 +113,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 +198,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 +208,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,71 +228,34 @@ 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 for breakage from that.
// Adding TYPE_TEXT_FLAG_NO_SUGGESTIONS fixes Pinyin Input, put causes Swype to be put in outAttrs.inputType = InputType.TYPE_NULL;
// word mode... Using TYPE_TEXT_VARIATION_VISIBLE_PASSWORD fixes that.
//
// So a bit messy. If this gets too messy it's perhaps best resolved by reverting back to just
// "TYPE_NULL" and let the Pinyin Input english keyboard be in word mode.
outAttrs.inputType = InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD;
// Let part of the application show behind when in landscape: // Let part of the application show behind when in landscape:
outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_FULLSCREEN; outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_FULLSCREEN;
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()");
commitText(getEditable(), 0);
// Clear the editable.
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 + ")");
if (mEmulator == null) return true; if (mEmulator == null) return true;
final int textLengthInChars = text.length(); final int textLengthInChars = text.length();
for (int i = 0; i < textLengthInChars; i++) { for (int i = 0; i < textLengthInChars; i++) {
@@ -314,23 +271,61 @@ 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;
} }
}
inputCodePoint(codePoint, ctrlHeld, false);
}
return true; return true;
} }
@Override @Override
public boolean deleteSurroundingText(int leftLength, int rightLength) { public boolean deleteSurroundingText(int leftLength, int rightLength) {
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "IME: deleteSurroundingText(" + leftLength + ", " + rightLength + ")"); if (LOG_KEY_EVENTS)
Log.i(EmulatorDebug.LOG_TAG, "IME: deleteSurroundingText(" + leftLength + ", " + rightLength + ")");
// Swype keyboard sometimes(?) sends this on backspace: // If leftLength=2 it may be due to a UTF-16 surrogate pair. So we cannot send
if (leftLength == 0 && rightLength == 0) leftLength = 1; // multiple key events for that. Let's just hope that keyboards don't use
// leftLength > 1 for other purposes (such as holding down backspace for repeat).
for (int i = 0; i < leftLength; i++)
sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL)); sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));
return true; return true;
} }
@Override
public boolean setComposingText(CharSequence text, int newCursorPosition) {
if (LOG_KEY_EVENTS)
Log.i(EmulatorDebug.LOG_TAG, "IME: setComposingText(\"" + text + "\", " + newCursorPosition + ")");
if (text.length() == 0) {
// Avoid log spam "SpannableStringBuilder: SPAN_EXCLUSIVE_EXCLUSIVE spans cannot
// have a zero length" when backspacing with the Google keyboard.
getEditable().clear();
} else {
super.setComposingText(text, newCursorPosition);
}
return true;
}
}; };
} }
@@ -388,14 +383,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;
@@ -469,8 +469,8 @@ public final class TerminalView extends View {
mInitialTextSelection = false; mInitialTextSelection = false;
break; break;
case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_DOWN:
int distanceFromSel1 = Math.abs(cx-mSelX1) + Math.abs(cy-mSelY1); int distanceFromSel1 = Math.abs(cx - mSelX1) + Math.abs(cy - mSelY1);
int distanceFromSel2 = Math.abs(cx-mSelX2) + Math.abs(cy-mSelY2); int distanceFromSel2 = Math.abs(cx - mSelX2) + Math.abs(cy - mSelY2);
mIsDraggingLeftSelection = distanceFromSel1 <= distanceFromSel2; mIsDraggingLeftSelection = distanceFromSel1 <= distanceFromSel2;
mSelectionDownX = ev.getX(); mSelectionDownX = ev.getX();
mSelectionDownY = ev.getY(); mSelectionDownY = ev.getY();
@@ -498,11 +498,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 +544,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 +565,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 +604,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 +613,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 +636,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 +656,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 +682,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 +698,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,48 +719,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 {
File fontFile = new File("/data/data/com.termux/files/home/.termux/font.ttf");
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.
@@ -817,8 +735,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);
@@ -842,12 +760,12 @@ public final class TerminalView extends View {
final int gripHandleMargin = gripHandleWidth / 4; // See the png. final int gripHandleMargin = gripHandleWidth / 4; // See the png.
int right = Math.round((mSelX1) * mRenderer.mFontWidth) + gripHandleMargin; int right = Math.round((mSelX1) * mRenderer.mFontWidth) + gripHandleMargin;
int top = (mSelY1+1 - mTopRow)*mRenderer.mFontLineSpacing + mRenderer.mFontLineSpacingAndAscent; int top = (mSelY1 + 1 - mTopRow) * mRenderer.mFontLineSpacing + mRenderer.mFontLineSpacingAndAscent;
mLeftSelectionHandle.setBounds(right - gripHandleWidth, top, right, top + mLeftSelectionHandle.getIntrinsicHeight()); mLeftSelectionHandle.setBounds(right - gripHandleWidth, top, right, top + mLeftSelectionHandle.getIntrinsicHeight());
mLeftSelectionHandle.draw(canvas); mLeftSelectionHandle.draw(canvas);
int left = Math.round((mSelX2+1)*mRenderer.mFontWidth) - gripHandleMargin; int left = Math.round((mSelX2 + 1) * mRenderer.mFontWidth) - gripHandleMargin;
top = (mSelY2+1 - mTopRow) *mRenderer.mFontLineSpacing + mRenderer.mFontLineSpacingAndAscent; top = (mSelY2 + 1 - mTopRow) * mRenderer.mFontLineSpacing + mRenderer.mFontLineSpacingAndAscent;
mRightSelectionHandle.setBounds(left, top, left + gripHandleWidth, top + mRightSelectionHandle.getIntrinsicHeight()); mRightSelectionHandle.setBounds(left, top, left + gripHandleWidth, top + mRightSelectionHandle.getIntrinsicHeight());
mRightSelectionHandle.draw(canvas); mRightSelectionHandle.draw(canvas);
} }
@@ -878,10 +796,10 @@ public final class TerminalView extends View {
TerminalBuffer screen = mEmulator.getScreen(); TerminalBuffer screen = mEmulator.getScreen();
if (!" ".equals(screen.getSelectedText(mSelX1, mSelY1, mSelX1, mSelY1))) { if (!" ".equals(screen.getSelectedText(mSelX1, mSelY1, mSelX1, mSelY1))) {
// Selecting something other than whitespace. Expand to word. // Selecting something other than whitespace. Expand to word.
while (mSelX1 > 0 && !"".equals(screen.getSelectedText(mSelX1-1, mSelY1, mSelX1-1, mSelY1))) { while (mSelX1 > 0 && !"".equals(screen.getSelectedText(mSelX1 - 1, mSelY1, mSelX1 - 1, mSelY1))) {
mSelX1--; mSelX1--;
} }
while (mSelX2 < mEmulator.mColumns-1 && !"".equals(screen.getSelectedText(mSelX2+1, mSelY1, mSelX2+1, mSelY1))) { while (mSelX2 < mEmulator.mColumns - 1 && !"".equals(screen.getSelectedText(mSelX2 + 1, mSelY1, mSelX2 + 1, mSelY1))) {
mSelX2++; mSelX2++;
} }
} }
@@ -894,13 +812,11 @@ 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);
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);
return true; return true;
} }

View File

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

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

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

@@ -1,7 +1,14 @@
<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_height="match_parent" > android:layout_alignParentTop="true"
android:layout_above="@+id/viewpager"
android:layout_height="match_parent">
<com.termux.view.TerminalView <com.termux.view.TerminalView
android:id="@+id/terminal_view" android:id="@+id/terminal_view"
@@ -20,7 +27,8 @@
android:choiceMode="singleChoice" android:choiceMode="singleChoice"
android:divider="@android:color/transparent" android:divider="@android:color/transparent"
android:dividerHeight="0dp" android:dividerHeight="0dp"
android:orientation="vertical" > android:descendantFocusability="blocksDescendants"
android:orientation="vertical">
<ListView <ListView
android:id="@+id/left_drawer_list" android:id="@+id/left_drawer_list"
@@ -35,7 +43,7 @@
style="?android:attr/buttonBarStyle" style="?android:attr/buttonBarStyle"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" > android:orientation="horizontal">
<Button <Button
android:id="@+id/toggle_keyboard_button" android:id="@+id/toggle_keyboard_button"
@@ -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,15 @@
<?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:singleLine="true"
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>

View File

@@ -12,7 +12,7 @@
<string name="help">Help</string> <string name="help">Help</string>
<string name="welcome_dialog_title">Welcome to Termux</string> <string name="welcome_dialog_title">Welcome to Termux</string>
<string name="welcome_dialog_body">Long press anywhere on the terminal for a context menu where Help is available.\n\nExecute \'apt update\' to update the packages list before installing packages.</string> <string name="welcome_dialog_body">Long press and select <i>More…</i> to show a menu where <i>Help</i> is available.\n\nExecute <b>apt update</b> to update the packages list before installing packages.</string>
<string name="welcome_dialog_dont_show_again_button">Do not show again</string> <string name="welcome_dialog_dont_show_again_button">Do not show again</string>
<string name="bootstrap_installer_body">Installing…</string> <string name="bootstrap_installer_body">Installing…</string>
@@ -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>
@@ -53,6 +53,8 @@
<string name="notification_action_wakelock">Wake</string> <string name="notification_action_wakelock">Wake</string>
<string name="notification_action_wifilock">Wifi</string> <string name="notification_action_wifilock">Wifi</string>
<string name="empty_folder">Empty folder.</string> <string name="file_received_title">Save file in ~/downloads/</string>
<string name="file_received_edit_button">Edit</string>
<string name="file_received_open_folder_button">Open folder</string>
</resources> </resources>

View File

@@ -163,7 +163,12 @@ public class CursorAndScreenTest extends TerminalTestCase {
} }
} }
public void testHorizontalTabColorsBackground() { /**
* See comments on horizontal tab handling in TerminalEmulator.java.
*
* We do not want to color already written cells when tabbing over them.
*/
public void DISABLED_testHorizontalTabColorsBackground() {
withTerminalSized(10, 3).enterString("\033[48;5;15m").enterString("\t"); withTerminalSized(10, 3).enterString("\033[48;5;15m").enterString("\t");
assertCursorAt(0, 8); assertCursorAt(0, 8);
for (int i = 0; i < 10; i++) { for (int i = 0; i < 10; i++) {
@@ -213,4 +218,13 @@ public class CursorAndScreenTest extends TerminalTestCase {
" -"); " -");
} }
public void testBackspaceAcrossWrappedLines() {
// Backspace should not go to previous line if not auto-wrapped:
withTerminalSized(3, 3).enterString("hi\r\n\b\byou").assertLinesAre("hi ", "you", " ");
// Backspace should go to previous line if auto-wrapped:
withTerminalSized(3, 3).enterString("hi y").assertLinesAre("hi ", "y ", " ").enterString("\b\b#").assertLinesAre("hi#", "y ", " ");
// Initial backspace should do nothing:
withTerminalSized(3, 3).enterString("\b\b\b\bhi").assertLinesAre("hi ", " ", " ");
}
} }

View File

@@ -92,6 +92,7 @@ public class TerminalTest extends TerminalTestCase {
assertEnteringStringGivesResponse("\033[6n", "\033[2;1R"); assertEnteringStringGivesResponse("\033[6n", "\033[2;1R");
} }
/** Test the cursor shape changes using DECSCUSR. */
public void testSetCursorStyle() throws Exception { public void testSetCursorStyle() throws Exception {
withTerminalSized(5, 5); withTerminalSized(5, 5);
assertEquals(TerminalEmulator.CURSOR_STYLE_BLOCK, mTerminal.getCursorStyle()); assertEquals(TerminalEmulator.CURSOR_STYLE_BLOCK, mTerminal.getCursorStyle());
@@ -257,4 +258,9 @@ public class TerminalTest extends TerminalTestCase {
withTerminalSized(3, 3).enterString("abc\r ").assertLinesAre(" bc", " ", " ").assertCursorAt(0, 1); withTerminalSized(3, 3).enterString("abc\r ").assertLinesAre(" bc", " ", " ").assertCursorAt(0, 1);
} }
public void testTab() {
withTerminalSized(11, 2).enterString("01234567890\r\tXX").assertLinesAre("01234567XX0", " ");
withTerminalSized(11, 2).enterString("01234567890\033[44m\r\tXX").assertLinesAre("01234567XX0", " ");
}
} }

View File

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

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).
} }
} }

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

View File

@@ -1,18 +1,17 @@
# 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
#Fri May 13 01:11:09 CEST 2016
org.gradle.jvmargs=-Xmx2048M
android.useDeprecatedNdk=true

Binary file not shown.

View File

@@ -1,6 +1,6 @@
#Wed Dec 23 01:44:47 CET 2015 #Sat Jul 23 17:08:29 CEST 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.10-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-bin.zip

46
gradlew vendored
View File

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