Compare commits

..

75 Commits
v0.34 ... v0.42

Author SHA1 Message Date
Fredrik Fornwall
8056013082 Bump version to 0.42 2016-09-16 23:18:51 +02:00
Fredrik Fornwall
8e90545c4b Remove comment from build.gradle 2016-09-16 23:18:34 +02:00
Fredrik Fornwall
426ddbacbd Remove useless casts 2016-09-16 23:17:47 +02:00
Fredrik Fornwall
7e1f8a551f Change VolumeUp+H to generate ~
Using VolumeUp+H to generate a tilde (~) is more useful than
sending the home key.

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

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

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

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

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

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

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

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

229
.idea/codeStyleSettings.xml generated Normal file
View File

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

1
.idea/gradle.xml generated
View File

@@ -3,6 +3,7 @@
<component name="GradleSettings"> <component name="GradleSettings">
<option name="linkedExternalProjectsSettings"> <option name="linkedExternalProjectsSettings">
<GradleProjectSettings> <GradleProjectSettings>
<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="modules"> <option name="modules">

View File

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

View File

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

View File

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

@@ -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.3.0' compile 'com.android.support:support-annotations:24.2.0'
} compile "com.android.support:support-v4:24.2.0"
sourceSets {
main {
jni.srcDirs = []
}
} }
defaultConfig { defaultConfig {
applicationId "com.termux" applicationId "com.termux"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 23 targetSdkVersion 24
versionCode 34 versionCode 42
versionName "0.34" versionName "0.42"
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

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

@@ -13,52 +13,52 @@ import android.widget.TextView;
public final class DialogUtils { public final class DialogUtils {
public interface TextSetListener { public interface TextSetListener {
void onTextSet(String text); void onTextSet(String text);
} }
public static void textInput(Activity activity, int titleText, String initialText, public static void textInput(Activity activity, int titleText, String initialText,
int positiveButtonText, final TextSetListener onPositive, int positiveButtonText, final TextSetListener onPositive,
int neutralButtonText, final TextSetListener onNeutral, int neutralButtonText, final TextSetListener onNeutral,
int negativeButtonText, final TextSetListener onNegative, int negativeButtonText, final TextSetListener onNegative,
final DialogInterface.OnDismissListener onDismiss) { 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) {
input.setText(initialText); input.setText(initialText);
Selection.setSelection(input.getText(), initialText.length()); Selection.setSelection(input.getText(), initialText.length());
} }
final AlertDialog[] dialogHolder = new AlertDialog[1]; final AlertDialog[] dialogHolder = new AlertDialog[1];
input.setImeActionLabel(activity.getResources().getString(positiveButtonText), KeyEvent.KEYCODE_ENTER); input.setImeActionLabel(activity.getResources().getString(positiveButtonText), KeyEvent.KEYCODE_ENTER);
input.setOnEditorActionListener(new TextView.OnEditorActionListener() { input.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override @Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
onPositive.onTextSet(input.getText().toString()); onPositive.onTextSet(input.getText().toString());
dialogHolder[0].dismiss(); dialogHolder[0].dismiss();
return true; return true;
} }
}); });
float dipInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, activity.getResources().getDisplayMetrics()); float dipInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, activity.getResources().getDisplayMetrics());
// https://www.google.com/design/spec/components/dialogs.html#dialogs-specs // https://www.google.com/design/spec/components/dialogs.html#dialogs-specs
int paddingTopAndSides = Math.round(16 * dipInPixels); int paddingTopAndSides = Math.round(16 * dipInPixels);
int paddingBottom = Math.round(24 * dipInPixels); int paddingBottom = Math.round(24 * dipInPixels);
LinearLayout layout = new LinearLayout(activity); LinearLayout layout = new LinearLayout(activity);
layout.setOrientation(LinearLayout.VERTICAL); layout.setOrientation(LinearLayout.VERTICAL);
layout.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); layout.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
layout.setPadding(paddingTopAndSides, paddingTopAndSides, paddingTopAndSides, paddingBottom); layout.setPadding(paddingTopAndSides, paddingTopAndSides, paddingTopAndSides, paddingBottom);
layout.addView(input); layout.addView(input);
AlertDialog.Builder builder = new AlertDialog.Builder(activity) AlertDialog.Builder builder = new AlertDialog.Builder(activity)
.setTitle(titleText).setView(layout) .setTitle(titleText).setView(layout)
.setPositiveButton(positiveButtonText, new DialogInterface.OnClickListener() { .setPositiveButton(positiveButtonText, new DialogInterface.OnClickListener() {
@Override @Override
public void onClick(DialogInterface d, int whichButton) { public void onClick(DialogInterface d, int whichButton) {
onPositive.onTextSet(input.getText().toString()); onPositive.onTextSet(input.getText().toString());
} }
}); });
if (onNeutral != null) { if (onNeutral != null) {
builder.setNeutralButton(neutralButtonText, new DialogInterface.OnClickListener() { builder.setNeutralButton(neutralButtonText, new DialogInterface.OnClickListener() {
@@ -82,9 +82,9 @@ public final class DialogUtils {
if (onDismiss != null) builder.setOnDismissListener(onDismiss); if (onDismiss != null) builder.setOnDismissListener(onDismiss);
dialogHolder[0] = builder.create(); dialogHolder[0] = builder.create();
dialogHolder[0].setCanceledOnTouchOutside(false); 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, GridLayout.FILL, weight);
param.rowSpec = GridLayout.spec(row, GridLayout.FILL, 1.f);
button.setLayoutParams(param);
addView(button);
}
}
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -15,61 +15,61 @@ 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) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
final RelativeLayout progressLayout = new RelativeLayout(this); final RelativeLayout progressLayout = new RelativeLayout(this);
RelativeLayout.LayoutParams lParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); RelativeLayout.LayoutParams lParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
lParams.addRule(RelativeLayout.CENTER_IN_PARENT); lParams.addRule(RelativeLayout.CENTER_IN_PARENT);
ProgressBar progressBar = new ProgressBar(this); ProgressBar progressBar = new ProgressBar(this);
progressBar.setIndeterminate(true); progressBar.setIndeterminate(true);
progressBar.setLayoutParams(lParams); progressBar.setLayoutParams(lParams);
progressLayout.addView(progressBar); progressLayout.addView(progressBar);
mWebView = new WebView(this); mWebView = new WebView(this);
WebSettings settings = mWebView.getSettings(); WebSettings settings = mWebView.getSettings();
settings.setCacheMode(WebSettings.LOAD_NO_CACHE); settings.setCacheMode(WebSettings.LOAD_NO_CACHE);
settings.setAppCacheEnabled(false); settings.setAppCacheEnabled(false);
setContentView(progressLayout); setContentView(progressLayout);
mWebView.clearCache(true); mWebView.clearCache(true);
mWebView.setWebViewClient(new WebViewClient() { mWebView.setWebViewClient(new WebViewClient() {
@Override @Override
public boolean shouldOverrideUrlLoading(WebView view, String url) { public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url.startsWith("https://termux.com")) { if (url.startsWith("https://termux.com")) {
// Inline help. // Inline help.
setContentView(progressLayout); setContentView(progressLayout);
return false; return false;
} }
try { try {
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
} catch (ActivityNotFoundException e) { } catch (ActivityNotFoundException e) {
// Android TV does not have a system browser. // Android TV does not have a system browser.
setContentView(progressLayout); setContentView(progressLayout);
return false; return false;
} }
return true; return true;
} }
@Override @Override
public void onPageFinished(WebView view, String url) { public void onPageFinished(WebView view, String url) {
setContentView(mWebView); setContentView(mWebView);
} }
}); });
mWebView.loadUrl("https://termux.com/help.html"); mWebView.loadUrl("https://termux.com/help.html");
} }
@Override @Override
public void onBackPressed() { public void onBackPressed() {
if (mWebView.canGoBack()) { if (mWebView.canGoBack()) {
mWebView.goBack(); mWebView.goBack();
} else { } else {
super.onBackPressed(); super.onBackPressed();
} }
} }
} }

View File

@@ -9,6 +9,7 @@ import android.content.DialogInterface.OnClickListener;
import android.content.DialogInterface.OnDismissListener; import android.content.DialogInterface.OnDismissListener;
import android.os.Build; import android.os.Build;
import android.os.Environment; import android.os.Environment;
import android.os.UserManager;
import android.system.Os; import android.system.Os;
import android.util.Log; import android.util.Log;
import android.util.Pair; import android.util.Pair;
@@ -31,234 +32,240 @@ 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 {
/** Performs setup if necessary. */ /** Performs setup if necessary. */
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)
.setOnDismissListener(new OnDismissListener() { .setOnDismissListener(new OnDismissListener() {
@Override @Override
public void onDismiss(DialogInterface dialog) { public void onDismiss(DialogInterface dialog) {
System.exit(0); System.exit(0);
} }
}).setPositiveButton(android.R.string.ok, null).show(); }).setPositiveButton(android.R.string.ok, null).show();
return; return;
} }
final File PREFIX_FILE = new File(TermuxService.PREFIX_PATH); final File PREFIX_FILE = new File(TermuxService.PREFIX_PATH);
if (PREFIX_FILE.isDirectory()) { if (PREFIX_FILE.isDirectory()) {
whenDone.run(); whenDone.run();
return; return;
} }
final ProgressDialog progress = ProgressDialog.show(activity, null, activity.getString(R.string.bootstrap_installer_body), true, false); final ProgressDialog progress = ProgressDialog.show(activity, null, activity.getString(R.string.bootstrap_installer_body), true, false);
new Thread() { new Thread() {
@Override @Override
public void run() { public void run() {
try { try {
final String STAGING_PREFIX_PATH = TermuxService.FILES_PATH + "/usr-staging"; final String STAGING_PREFIX_PATH = TermuxService.FILES_PATH + "/usr-staging";
final File STAGING_PREFIX_FILE = new File(STAGING_PREFIX_PATH); final File STAGING_PREFIX_FILE = new File(STAGING_PREFIX_PATH);
if (STAGING_PREFIX_FILE.exists()) { if (STAGING_PREFIX_FILE.exists()) {
deleteFolder(STAGING_PREFIX_FILE); deleteFolder(STAGING_PREFIX_FILE);
} }
final byte[] buffer = new byte[8096]; final byte[] buffer = new byte[8096];
final List<Pair<String, String>> symlinks = new ArrayList<>(50); final List<Pair<String, String>> symlinks = new ArrayList<>(50);
final URL zipUrl = determineZipUrl(); final URL zipUrl = determineZipUrl();
try (ZipInputStream zipInput = new ZipInputStream(zipUrl.openStream())) { try (ZipInputStream zipInput = new ZipInputStream(zipUrl.openStream())) {
ZipEntry zipEntry; ZipEntry zipEntry;
while ((zipEntry = zipInput.getNextEntry()) != null) { while ((zipEntry = zipInput.getNextEntry()) != null) {
if (zipEntry.getName().equals("SYMLINKS.txt")) { if (zipEntry.getName().equals("SYMLINKS.txt")) {
BufferedReader symlinksReader = new BufferedReader(new InputStreamReader(zipInput)); BufferedReader symlinksReader = new BufferedReader(new InputStreamReader(zipInput));
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)
String oldPath = parts[0]; throw new RuntimeException("Malformed symlink line: " + line);
String newPath = STAGING_PREFIX_PATH + "/" + parts[1]; String oldPath = parts[0];
symlinks.add(Pair.create(oldPath, newPath)); String newPath = STAGING_PREFIX_PATH + "/" + parts[1];
} symlinks.add(Pair.create(oldPath, newPath));
} else { }
String zipEntryName = zipEntry.getName(); } else {
File targetFile = new File(STAGING_PREFIX_PATH, zipEntryName); String zipEntryName = zipEntry.getName();
if (zipEntry.isDirectory()) { File targetFile = new File(STAGING_PREFIX_PATH, zipEntryName);
if (!targetFile.mkdirs()) throw new RuntimeException("Failed to create directory: " + targetFile.getAbsolutePath()); if (zipEntry.isDirectory()) {
} else { if (!targetFile.mkdirs())
try (FileOutputStream outStream = new FileOutputStream(targetFile)) { throw new RuntimeException("Failed to create directory: " + targetFile.getAbsolutePath());
int readBytes; } else {
while ((readBytes = zipInput.read(buffer)) != -1) try (FileOutputStream outStream = new FileOutputStream(targetFile)) {
outStream.write(buffer, 0, readBytes); int readBytes;
} while ((readBytes = zipInput.read(buffer)) != -1)
if (zipEntryName.startsWith("bin/") || zipEntryName.startsWith("libexec") || zipEntryName.startsWith("lib/apt/methods")) { outStream.write(buffer, 0, readBytes);
//noinspection OctalInteger }
Os.chmod(targetFile.getAbsolutePath(), 0700); if (zipEntryName.startsWith("bin/") || zipEntryName.startsWith("libexec") || zipEntryName.startsWith("lib/apt/methods")) {
} //noinspection OctalInteger
} Os.chmod(targetFile.getAbsolutePath(), 0700);
} }
} }
} }
}
}
if (symlinks.isEmpty()) throw new RuntimeException("No SYMLINKS.txt encountered"); if (symlinks.isEmpty())
for (Pair<String, String> symlink : symlinks) { throw new RuntimeException("No SYMLINKS.txt encountered");
Os.symlink(symlink.first, symlink.second); for (Pair<String, String> symlink : symlinks) {
} Os.symlink(symlink.first, symlink.second);
}
if (!STAGING_PREFIX_FILE.renameTo(PREFIX_FILE)) { if (!STAGING_PREFIX_FILE.renameTo(PREFIX_FILE)) {
throw new RuntimeException("Unable to rename staging folder"); throw new RuntimeException("Unable to rename staging folder");
} }
activity.runOnUiThread(new Runnable() { activity.runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
whenDone.run(); whenDone.run();
} }
}); });
} catch (final Exception e) { } catch (final Exception e) {
Log.e(EmulatorDebug.LOG_TAG, "Bootstrap error", e); Log.e(EmulatorDebug.LOG_TAG, "Bootstrap error", e);
activity.runOnUiThread(new Runnable() { activity.runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
try { try {
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_body) new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_body)
.setNegativeButton(R.string.bootstrap_error_abort, new OnClickListener() { .setNegativeButton(R.string.bootstrap_error_abort, new OnClickListener() {
@Override @Override
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
dialog.dismiss(); dialog.dismiss();
activity.finish(); activity.finish();
} }
}).setPositiveButton(R.string.bootstrap_error_try_again, new OnClickListener() { }).setPositiveButton(R.string.bootstrap_error_try_again, new OnClickListener() {
@Override @Override
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
dialog.dismiss(); dialog.dismiss();
TermuxInstaller.setupIfNeeded(activity, whenDone); TermuxInstaller.setupIfNeeded(activity, whenDone);
} }
}).show(); }).show();
} catch (WindowManager.BadTokenException e) { } catch (WindowManager.BadTokenException e) {
// Activity already dismissed - ignore. // Activity already dismissed - ignore.
} }
} }
}); });
} finally { } finally {
activity.runOnUiThread(new Runnable() { activity.runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
try { try {
progress.dismiss(); progress.dismiss();
} catch (RuntimeException e) { } catch (RuntimeException e) {
// Activity already dismissed - ignore. // Activity already dismissed - ignore.
} }
} }
}); });
} }
} }
}.start(); }.start();
} }
/** 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 termuxArch = null; String archName = determineTermuxArchName();
return new URL("https://termux.net/bootstrap/bootstrap-" + archName + ".zip");
}
private static String determineTermuxArchName() {
// Note that we cannot use System.getProperty("os.arch") since that may give e.g. "aarch64" // Note that we cannot use System.getProperty("os.arch") since that may give e.g. "aarch64"
// while a 64-bit runtime may not be installed (like on the Samsung Galaxy S5 Neo). // while a 64-bit runtime may not be installed (like on the Samsung Galaxy S5 Neo).
// Instead we search through the supported abi:s on the device, see: // Instead we search through the supported abi:s on the device, see:
// http://developer.android.com/ndk/guides/abis.html // http://developer.android.com/ndk/guides/abis.html
// Note that we search for abi:s in preferred order, and want to avoid installing arm on // Note that we search for abi:s in preferred order (the ordering of the
// an x86 system where arm emulation is available. // Build.SUPPORTED_ABIS list) to avoid e.g. installing arm on an x86 system where arm
final String[] androidArchNames = {"arm64-v8a", "x86", "armeabi-v7a"}; // emulation is available.
final String[] termuxArchNames = {"aarch64", "i686", "arm"}; for (String androidArch : Build.SUPPORTED_ABIS) {
switch (androidArch) {
final List<String> supportedArches = Arrays.asList(Build.SUPPORTED_ABIS); case "arm64-v8a": return "aarch64";
for (int i = 0; i < termuxArchNames.length; i++) { case "armeabi-v7a": return "arm";
if (supportedArches.contains(androidArchNames[i])) { case "x86_64": return "x86_64";
termuxArch = termuxArchNames[i]; case "x86": return "i686";
break;
} }
} }
throw new RuntimeException("Unable to determine arch from Build.SUPPORTED_ABIS = " +
return new URL("https://termux.net/bootstrap/bootstrap-" + termuxArch + ".zip"); Arrays.toString(Build.SUPPORTED_ABIS));
} }
/** Delete a folder and all its content or throw. */ /** Delete a folder and all its content or throw. */
static void deleteFolder(File fileOrDirectory) { static void deleteFolder(File fileOrDirectory) {
File[] children = fileOrDirectory.listFiles(); File[] children = fileOrDirectory.listFiles();
if (children != null) { if (children != null) {
for (File child : children) { for (File child : children) {
deleteFolder(child); deleteFolder(child);
} }
} }
if (!fileOrDirectory.delete()) { if (!fileOrDirectory.delete()) {
throw new RuntimeException("Unable to delete " + (fileOrDirectory.isDirectory() ? "directory " : "file ") + fileOrDirectory.getAbsolutePath()); throw new RuntimeException("Unable to delete " + (fileOrDirectory.isDirectory() ? "directory " : "file ") + fileOrDirectory.getAbsolutePath());
} }
} }
public static void setupStorageSymlinks(final Context context) { public static void setupStorageSymlinks(final Context context) {
final String LOG_TAG = "termux-storage"; final String LOG_TAG = "termux-storage";
new Thread() { new Thread() {
public void run() { public void run() {
try { try {
File storageDir = new File(TermuxService.HOME_PATH, "storage"); File storageDir = new File(TermuxService.HOME_PATH, "storage");
if (storageDir.exists() && !storageDir.delete()) { if (storageDir.exists() && !storageDir.delete()) {
Log.e(LOG_TAG, "Could not delete old $HOME/storage"); Log.e(LOG_TAG, "Could not delete old $HOME/storage");
return; return;
} }
if (!storageDir.mkdirs()) { if (!storageDir.mkdirs()) {
Log.e(LOG_TAG, "Unable to mkdirs() for $HOME/storage"); Log.e(LOG_TAG, "Unable to mkdirs() for $HOME/storage");
return; return;
} }
File sharedDir = Environment.getExternalStorageDirectory(); File sharedDir = Environment.getExternalStorageDirectory();
Os.symlink(sharedDir.getAbsolutePath(), new File(storageDir, "shared").getAbsolutePath()); Os.symlink(sharedDir.getAbsolutePath(), new File(storageDir, "shared").getAbsolutePath());
File downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); File downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
Os.symlink(downloadsDir.getAbsolutePath(), new File(storageDir, "downloads").getAbsolutePath()); Os.symlink(downloadsDir.getAbsolutePath(), new File(storageDir, "downloads").getAbsolutePath());
File dcimDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM); File dcimDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM);
Os.symlink(dcimDir.getAbsolutePath(), new File(storageDir, "dcim").getAbsolutePath()); Os.symlink(dcimDir.getAbsolutePath(), new File(storageDir, "dcim").getAbsolutePath());
File picturesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES); File picturesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
Os.symlink(picturesDir.getAbsolutePath(), new File(storageDir, "pictures").getAbsolutePath()); Os.symlink(picturesDir.getAbsolutePath(), new File(storageDir, "pictures").getAbsolutePath());
File musicDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC); File musicDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC);
Os.symlink(musicDir.getAbsolutePath(), new File(storageDir, "music").getAbsolutePath()); Os.symlink(musicDir.getAbsolutePath(), new File(storageDir, "music").getAbsolutePath());
File moviesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES); File moviesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES);
Os.symlink(moviesDir.getAbsolutePath(), new File(storageDir, "movies").getAbsolutePath()); Os.symlink(moviesDir.getAbsolutePath(), new File(storageDir, "movies").getAbsolutePath());
final File[] dirs = context.getExternalFilesDirs(null); final File[] dirs = context.getExternalFilesDirs(null);
if (dirs != null && dirs.length >= 2) { if (dirs != null && dirs.length >= 2) {
final File externalDir = dirs[1]; final File externalDir = dirs[1];
Os.symlink(externalDir.getAbsolutePath(), new File(storageDir, "external").getAbsolutePath()); Os.symlink(externalDir.getAbsolutePath(), new File(storageDir, "external").getAbsolutePath());
} }
} catch (Exception e) { } catch (Exception e) {
Log.e(LOG_TAG, "Error setting up link", e); Log.e(LOG_TAG, "Error setting up link", e);
} }
} }
}.start(); }.start();
} }
} }

View File

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

View File

@@ -14,129 +14,194 @@ 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;
static final int BELL_IGNORE = 3; static final int BELL_IGNORE = 3;
private final int MIN_FONTSIZE; private final int MIN_FONTSIZE;
private static final int MAX_FONTSIZE = 256; private static final int MAX_FONTSIZE = 256;
private static final String FULLSCREEN_KEY = "fullscreen"; private static final String FULLSCREEN_KEY = "fullscreen";
private static final String FONTSIZE_KEY = "fontsize"; private static final String SHOW_EXTRA_KEYS_KEY = "show_extra_keys";
private static final String CURRENT_SESSION_KEY = "current_session"; private static final String FONTSIZE_KEY = "fontsize";
private static final String SHOW_WELCOME_DIALOG_KEY = "intro_dialog"; private static final String CURRENT_SESSION_KEY = "current_session";
private static final String SHOW_WELCOME_DIALOG_KEY = "intro_dialog";
private boolean mFullScreen; private boolean mFullScreen;
private int mFontSize; private int mFontSize;
@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);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
float dipInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, context.getResources().getDisplayMetrics()); float dipInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, context.getResources().getDisplayMetrics());
// This is a bit arbitrary and sub-optimal. We want to give a sensible default for minimum font size // This is a bit arbitrary and sub-optimal. We want to give a sensible default for minimum font size
// to prevent invisible text due to zoom be mistake: // to prevent invisible text due to zoom be mistake:
MIN_FONTSIZE = (int) (4f * dipInPixels); MIN_FONTSIZE = (int) (4f * dipInPixels);
mFullScreen = prefs.getBoolean(FULLSCREEN_KEY, false); 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);
// Make it divisible by 2 since that is the minimal adjustment step: // Make it divisible by 2 since that is the minimal adjustment step:
if (defaultFontSize % 2 == 1) defaultFontSize--; if (defaultFontSize % 2 == 1) defaultFontSize--;
try { try {
mFontSize = Integer.parseInt(prefs.getString(FONTSIZE_KEY, Integer.toString(defaultFontSize))); mFontSize = Integer.parseInt(prefs.getString(FONTSIZE_KEY, Integer.toString(defaultFontSize)));
} catch (NumberFormatException | ClassCastException e) { } catch (NumberFormatException | ClassCastException e) {
mFontSize = defaultFontSize; mFontSize = defaultFontSize;
} }
mFontSize = Math.max(MIN_FONTSIZE, Math.min(mFontSize, MAX_FONTSIZE)); mFontSize = Math.max(MIN_FONTSIZE, Math.min(mFontSize, MAX_FONTSIZE));
} }
boolean isFullScreen() { boolean isFullScreen() {
return mFullScreen; return mFullScreen;
} }
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(); }
}
int getFontSize() { boolean isShowExtraKeys() {
return mFontSize; return mShowExtraKeys;
} }
void changeFontSize(Context context, boolean increase) { boolean toggleShowExtraKeys(Context context) {
mFontSize += (increase ? 1 : -1) * 2; mShowExtraKeys = !mShowExtraKeys;
mFontSize = Math.max(MIN_FONTSIZE, Math.min(mFontSize, MAX_FONTSIZE)); PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(SHOW_EXTRA_KEYS_KEY, mShowExtraKeys).apply();
return mShowExtraKeys;
}
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); int getFontSize() {
prefs.edit().putString(FONTSIZE_KEY, Integer.toString(mFontSize)).apply(); return mFontSize;
} }
static void storeCurrentSession(Context context, TerminalSession session) { void changeFontSize(Context context, boolean increase) {
PreferenceManager.getDefaultSharedPreferences(context).edit().putString(TermuxPreferences.CURRENT_SESSION_KEY, session.mHandle).commit(); mFontSize += (increase ? 1 : -1) * 2;
} mFontSize = Math.max(MIN_FONTSIZE, Math.min(mFontSize, MAX_FONTSIZE));
static TerminalSession getCurrentSession(TermuxActivity context) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
String sessionHandle = PreferenceManager.getDefaultSharedPreferences(context).getString(TermuxPreferences.CURRENT_SESSION_KEY, ""); prefs.edit().putString(FONTSIZE_KEY, Integer.toString(mFontSize)).apply();
for (int i = 0, len = context.mTermService.getSessions().size(); i < len; i++) { }
TerminalSession session = context.mTermService.getSessions().get(i);
if (session.mHandle.equals(sessionHandle)) return session;
}
return null;
}
public static boolean isShowWelcomeDialog(Context context) { static void storeCurrentSession(Context context, TerminalSession session) {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SHOW_WELCOME_DIALOG_KEY, true); PreferenceManager.getDefaultSharedPreferences(context).edit().putString(TermuxPreferences.CURRENT_SESSION_KEY, session.mHandle).commit();
} }
public static void disableWelcomeDialog(Context context) { static TerminalSession getCurrentSession(TermuxActivity context) {
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(SHOW_WELCOME_DIALOG_KEY, false).apply(); String sessionHandle = PreferenceManager.getDefaultSharedPreferences(context).getString(TermuxPreferences.CURRENT_SESSION_KEY, "");
} for (int i = 0, len = context.mTermService.getSessions().size(); i < len; i++) {
TerminalSession session = context.mTermService.getSessions().get(i);
if (session.mHandle.equals(sessionHandle)) return session;
}
return null;
}
public void reloadFromProperties(Context context) { public static boolean isShowWelcomeDialog(Context context) {
try { return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SHOW_WELCOME_DIALOG_KEY, true);
File propsFile = new File(TermuxService.HOME_PATH + "/.config/termux/termux.properties"); }
Properties props = new Properties();
if (propsFile.isFile() && propsFile.canRead()) {
try (FileInputStream in = new FileInputStream(propsFile)) {
props.load(in);
}
}
switch (props.getProperty("bell-character", "vibrate")) { public static void disableWelcomeDialog(Context context) {
case "beep": PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(SHOW_WELCOME_DIALOG_KEY, false).apply();
mBellBehaviour = BELL_BEEP; }
break;
case "ignore":
mBellBehaviour = BELL_IGNORE;
break;
default: // "vibrate".
mBellBehaviour = BELL_VIBRATE;
break;
}
mBackIsEscape = "escape".equals(props.getProperty("back-key", "back")); public void reloadFromProperties(Context context) {
} catch (Exception e) { try {
Toast.makeText(context, "Error loading properties: " + e.getMessage(), Toast.LENGTH_LONG).show(); File propsFile = new File(TermuxService.HOME_PATH + "/.termux/termux.properties");
Log.e("termux", "Error loading props", e); if (!propsFile.exists())
} propsFile = new File(TermuxService.HOME_PATH + "/.config/termux/termux.properties");
}
Properties props = new Properties();
if (propsFile.isFile() && propsFile.canRead()) {
try (FileInputStream in = new FileInputStream(propsFile)) {
props.load(in);
}
}
switch (props.getProperty("bell-character", "vibrate")) {
case "beep":
mBellBehaviour = BELL_BEEP;
break;
case "ignore":
mBellBehaviour = BELL_IGNORE;
break;
default: // "vibrate".
mBellBehaviour = BELL_VIBRATE;
break;
}
mBackIsEscape = "escape".equals(props.getProperty("back-key", "back"));
shortcuts.clear();
parseAction("shortcut.create-session", SHORTCUT_ACTION_CREATE_SESSION, props);
parseAction("shortcut.next-session", SHORTCUT_ACTION_NEXT_SESSION, props);
parseAction("shortcut.previous-session", SHORTCUT_ACTION_PREVIOUS_SESSION, props);
parseAction("shortcut.rename-session", SHORTCUT_ACTION_RENAME_SESSION, props);
} catch (Exception e) {
Toast.makeText(context, "Error loading properties: " + e.getMessage(), Toast.LENGTH_LONG).show();
Log.e("termux", "Error loading props", e);
}
}
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,330 +16,346 @@ 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()}.
*/ */
public final class TermuxService extends Service implements SessionChangedCallback { public final class TermuxService extends Service implements SessionChangedCallback {
/** Note that this is a symlink on the Android M preview. */ /** Note that this is a symlink on the Android M preview. */
@SuppressLint("SdCardPath") @SuppressLint("SdCardPath")
public static final String FILES_PATH = "/data/data/com.termux/files"; public static final String FILES_PATH = "/data/data/com.termux/files";
public static final String PREFIX_PATH = FILES_PATH + "/usr"; public static final String PREFIX_PATH = FILES_PATH + "/usr";
public static final String HOME_PATH = FILES_PATH + "/home"; public static final String HOME_PATH = FILES_PATH + "/home";
private static final int NOTIFICATION_ID = 1337; private static final int NOTIFICATION_ID = 1337;
/** Intent action to stop the service. */ /** Intent action to stop the service. */
private static final String ACTION_STOP_SERVICE = "com.termux.service_stop"; private static final String ACTION_STOP_SERVICE = "com.termux.service_stop";
/** Intent action to toggle the wake lock, {@link #mWakeLock}, which this service may hold. */ /** Intent action to toggle the wake lock, {@link #mWakeLock}, which this service may hold. */
private static final String ACTION_LOCK_WAKE = "com.termux.service_toggle_wake_lock"; private static final String ACTION_LOCK_WAKE = "com.termux.service_toggle_wake_lock";
/** 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. */
public static final String ACTION_EXECUTE = "com.termux.service_execute"; public static final String ACTION_EXECUTE = "com.termux.service_execute";
public static final String EXTRA_ARGUMENTS = "com.termux.execute.arguments"; public static final String EXTRA_ARGUMENTS = "com.termux.execute.arguments";
public static final String EXTRA_CURRENT_WORKING_DIRECTORY = "com.termux.execute.cwd"; public static final String EXTRA_CURRENT_WORKING_DIRECTORY = "com.termux.execute.cwd";
/** 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 {
public final TermuxService service = TermuxService.this; public final TermuxService service = TermuxService.this;
} }
private final IBinder mBinder = new LocalBinder(); private final IBinder mBinder = new LocalBinder();
/** /**
* The terminal sessions which this service manages. * The terminal sessions which this service manages.
* * <p/>
* Note that this list is observed by {@link TermuxActivity#mListViewAdapter}, so any changes must be made on the UI * Note that this list is observed by {@link TermuxActivity#mListViewAdapter}, so any changes must be made on the UI
* thread and followed by a call to {@link ArrayAdapter#notifyDataSetChanged()} }. * thread and followed by a call to {@link ArrayAdapter#notifyDataSetChanged()} }.
*/ */
final List<TerminalSession> mTerminalSessions = new ArrayList<>(); final List<TerminalSession> mTerminalSessions = new ArrayList<>();
/** Note that the service may often outlive the activity, so need to clear this reference. */ /** Note that the service may often outlive the activity, so need to clear this reference. */
SessionChangedCallback mSessionChangeCallback; SessionChangedCallback mSessionChangeCallback;
private PowerManager.WakeLock mWakeLock; private PowerManager.WakeLock mWakeLock;
private WifiManager.WifiLock mWifiLock; private WifiManager.WifiLock mWifiLock;
/** If the user has executed the {@link #ACTION_STOP_SERVICE} intent. */ /** If the user has executed the {@link #ACTION_STOP_SERVICE} intent. */
boolean mWantsToStop = false; boolean mWantsToStop = false;
@SuppressLint("Wakelock") @SuppressLint("Wakelock")
@Override @Override
public int onStartCommand(Intent intent, int flags, int startId) { public int onStartCommand(Intent intent, int flags, int startId) {
String action = intent.getAction(); String action = intent.getAction();
if (ACTION_STOP_SERVICE.equals(action)) { if (ACTION_STOP_SERVICE.equals(action)) {
mWantsToStop = true; mWantsToStop = true;
for (int i = 0; i < mTerminalSessions.size(); i++) for (int i = 0; i < mTerminalSessions.size(); i++)
mTerminalSessions.get(i).finishIfRunning(); mTerminalSessions.get(i).finishIfRunning();
stopSelf(); stopSelf();
} else if (ACTION_LOCK_WAKE.equals(action)) { } else if (ACTION_LOCK_WAKE.equals(action)) {
if (mWakeLock == null) { if (mWakeLock == null) {
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, EmulatorDebug.LOG_TAG); mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, EmulatorDebug.LOG_TAG);
mWakeLock.acquire(); mWakeLock.acquire();
} else { } else {
mWakeLock.release(); mWakeLock.release();
mWakeLock = null; mWakeLock = null;
} }
updateNotification(); updateNotification();
} else if (ACTION_LOCK_WIFI.equals(action)) { } else if (ACTION_LOCK_WIFI.equals(action)) {
if (mWifiLock == null) { if (mWifiLock == null) {
WifiManager wm = (WifiManager) getSystemService(Context.WIFI_SERVICE); WifiManager wm = (WifiManager) getSystemService(Context.WIFI_SERVICE);
mWifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, EmulatorDebug.LOG_TAG); mWifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, EmulatorDebug.LOG_TAG);
mWifiLock.acquire(); mWifiLock.acquire();
} else { } else {
mWifiLock.release(); mWifiLock.release();
mWifiLock = null; mWifiLock = null;
} }
updateNotification(); updateNotification();
} else if (ACTION_EXECUTE.equals(action)) { } else if (ACTION_EXECUTE.equals(action)) {
Uri executableUri = intent.getData(); Uri executableUri = intent.getData();
String executablePath = (executableUri == null ? null : executableUri.getPath()); String executablePath = (executableUri == null ? null : executableUri.getPath());
String[] arguments = (executableUri == null ? null : intent.getStringArrayExtra(EXTRA_ARGUMENTS)); String[] arguments = (executableUri == null ? null : intent.getStringArrayExtra(EXTRA_ARGUMENTS));
String cwd = intent.getStringExtra(EXTRA_CURRENT_WORKING_DIRECTORY); String cwd = intent.getStringExtra(EXTRA_CURRENT_WORKING_DIRECTORY);
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".
if (executablePath != null) { if (executablePath != null) {
int lastSlash = executablePath.lastIndexOf('/'); int lastSlash = executablePath.lastIndexOf('/');
String name = (lastSlash == -1) ? executablePath : executablePath.substring(lastSlash + 1); String name = (lastSlash == -1) ? executablePath : executablePath.substring(lastSlash + 1);
name = name.replace('-', ' '); name = name.replace('-', ' ');
newSession.mSessionName = name; newSession.mSessionName = name;
} }
// Make the newly created session the current one to be displayed: // Make the newly created session the current one to be displayed:
TermuxPreferences.storeCurrentSession(this, newSession); TermuxPreferences.storeCurrentSession(this, newSession);
// Launch the main Termux app, which will now show to current session: // Launch the main Termux app, which will now show to current session:
startActivity(new Intent(this, TermuxActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); startActivity(new Intent(this, TermuxActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
} else if (action != null) { } else if (action != null) {
Log.e(EmulatorDebug.LOG_TAG, "Unknown TermuxService action: '" + action + "'"); Log.e(EmulatorDebug.LOG_TAG, "Unknown TermuxService action: '" + action + "'");
} }
// If this service really do get killed, there is no point restarting it automatically - let the user do on next // If this service really do get killed, there is no point restarting it automatically - let the user do on next
// start of {@link Term): // start of {@link Term):
return Service.START_NOT_STICKY; return Service.START_NOT_STICKY;
} }
@Override @Override
public IBinder onBind(Intent intent) { public IBinder onBind(Intent intent) {
return mBinder; return mBinder;
} }
@Override @Override
public void onCreate() { public void onCreate() {
startForeground(NOTIFICATION_ID, buildNotification()); startForeground(NOTIFICATION_ID, buildNotification());
} }
/** Update the shown foreground service notification after making any changes that affect it. */ /** Update the shown foreground service notification after making any changes that affect it. */
private void updateNotification() { private void updateNotification() {
if (mWakeLock == null && mWifiLock == null && getSessions().isEmpty()) { if (mWakeLock == null && mWifiLock == null && getSessions().isEmpty()) {
// Exit if we are updating after the user disabled all locks with no sessions. // Exit if we are updating after the user disabled all locks with no sessions.
stopSelf(); stopSelf();
} else { } else {
((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).notify(NOTIFICATION_ID, buildNotification()); ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).notify(NOTIFICATION_ID, buildNotification());
} }
} }
private Notification buildNotification() { private Notification buildNotification() {
Intent notifyIntent = new Intent(this, TermuxActivity.class); Intent notifyIntent = new Intent(this, TermuxActivity.class);
// PendingIntent#getActivity(): "Note that the activity will be started outside of the context of an existing // PendingIntent#getActivity(): "Note that the activity will be started outside of the context of an existing
// activity, so you must use the Intent.FLAG_ACTIVITY_NEW_TASK launch flag in the Intent": // activity, so you must use the Intent.FLAG_ACTIVITY_NEW_TASK launch flag in the Intent":
notifyIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); notifyIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notifyIntent, 0); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notifyIntent, 0);
int sessionCount = mTerminalSessions.size(); int sessionCount = mTerminalSessions.size();
String contentText = sessionCount + " terminal session" + (sessionCount == 1 ? "" : "s"); String contentText = sessionCount + " terminal session" + (sessionCount == 1 ? "" : "s");
boolean wakeLockHeld = mWakeLock != null; boolean wakeLockHeld = mWakeLock != null;
boolean wifiLockHeld = mWifiLock != null; boolean wifiLockHeld = mWifiLock != null;
if (wakeLockHeld && wifiLockHeld) { if (wakeLockHeld && wifiLockHeld) {
contentText += " (wake&wifi lock held)"; contentText += " (wake&wifi lock held)";
} else if (wakeLockHeld) { } else if (wakeLockHeld) {
contentText += " (wake lock held)"; contentText += " (wake lock held)";
} else if (wifiLockHeld) { } else if (wifiLockHeld) {
contentText += " (wifi lock held)"; contentText += " (wifi lock held)";
} }
Notification.Builder builder = new Notification.Builder(this); Notification.Builder builder = new Notification.Builder(this);
builder.setContentTitle(getText(R.string.application_name)); builder.setContentTitle(getText(R.string.application_name));
builder.setContentText(contentText); builder.setContentText(contentText);
builder.setSmallIcon(R.drawable.ic_service_notification); builder.setSmallIcon(R.drawable.ic_service_notification);
builder.setContentIntent(pendingIntent); builder.setContentIntent(pendingIntent);
builder.setOngoing(true); builder.setOngoing(true);
// If holding a wake or wifi lock consider the notification of high priority since it's using power, // If holding a wake or wifi lock consider the notification of high priority since it's using power,
// otherwise use a minimal priority since this is just a background service notification: // otherwise use a minimal priority since this is just a background service notification:
builder.setPriority((wakeLockHeld || wifiLockHeld) ? Notification.PRIORITY_HIGH : Notification.PRIORITY_MIN); builder.setPriority((wakeLockHeld || wifiLockHeld) ? Notification.PRIORITY_HIGH : Notification.PRIORITY_MIN);
// No need to show a timestamp: // No need to show a timestamp:
builder.setShowWhen(false); builder.setShowWhen(false);
// Background color for small notification icon: // Background color for small notification icon:
builder.setColor(0xFF000000); builder.setColor(0xFF000000);
Resources res = getResources(); Resources res = getResources();
Intent exitIntent = new Intent(this, TermuxService.class).setAction(ACTION_STOP_SERVICE); Intent exitIntent = new Intent(this, TermuxService.class).setAction(ACTION_STOP_SERVICE);
builder.addAction(android.R.drawable.ic_delete, res.getString(R.string.notification_action_exit), PendingIntent.getService(this, 0, exitIntent, 0)); builder.addAction(android.R.drawable.ic_delete, res.getString(R.string.notification_action_exit), PendingIntent.getService(this, 0, exitIntent, 0));
Intent toggleWakeLockIntent = new Intent(this, TermuxService.class).setAction(ACTION_LOCK_WAKE); Intent toggleWakeLockIntent = new Intent(this, TermuxService.class).setAction(ACTION_LOCK_WAKE);
builder.addAction(android.R.drawable.ic_lock_lock, res.getString(R.string.notification_action_wakelock), builder.addAction(android.R.drawable.ic_lock_lock, res.getString(R.string.notification_action_wakelock),
PendingIntent.getService(this, 0, toggleWakeLockIntent, 0)); PendingIntent.getService(this, 0, toggleWakeLockIntent, 0));
Intent toggleWifiLockIntent = new Intent(this, TermuxService.class).setAction(ACTION_LOCK_WIFI); Intent toggleWifiLockIntent = new Intent(this, TermuxService.class).setAction(ACTION_LOCK_WIFI);
builder.addAction(android.R.drawable.ic_lock_lock, res.getString(R.string.notification_action_wifilock), builder.addAction(android.R.drawable.ic_lock_lock, res.getString(R.string.notification_action_wifilock),
PendingIntent.getService(this, 0, toggleWifiLockIntent, 0)); PendingIntent.getService(this, 0, toggleWifiLockIntent, 0));
return builder.build(); return builder.build();
} }
@Override @Override
public void onDestroy() { public void onDestroy() {
if (mWakeLock != null) mWakeLock.release(); if (mWakeLock != null) mWakeLock.release();
if (mWifiLock != null) mWifiLock.release(); if (mWifiLock != null) mWifiLock.release();
stopForeground(true); stopForeground(true);
for (int i = 0; i < mTerminalSessions.size(); i++) for (int i = 0; i < mTerminalSessions.size(); i++)
mTerminalSessions.get(i).finishIfRunning(); mTerminalSessions.get(i).finishIfRunning();
mTerminalSessions.clear(); mTerminalSessions.clear();
} }
public List<TerminalSession> getSessions() { public List<TerminalSession> getSessions() {
return mTerminalSessions; return mTerminalSessions;
} }
TerminalSession createTermSession(String executablePath, String[] arguments, String cwd, boolean failSafe) { TerminalSession createTermSession(String executablePath, String[] arguments, String cwd, boolean failSafe) {
new File(HOME_PATH).mkdirs(); new File(HOME_PATH).mkdirs();
if (cwd == null) cwd = HOME_PATH; if (cwd == null) cwd = HOME_PATH;
final String termEnv = "TERM=xterm-256color"; final String termEnv = "TERM=xterm-256color";
final String homeEnv = "HOME=" + HOME_PATH; final String homeEnv = "HOME=" + HOME_PATH;
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 // EXTERNAL_STORAGE is needed for /system/bin/am to work on at least
// Samsung S7 - see https://plus.google.com/110070148244138185604/posts/gp8Lk3aCGp3. // Samsung S7 - see https://plus.google.com/110070148244138185604/posts/gp8Lk3aCGp3.
final String externalStorageEnv = "EXTERNAL_STORAGE=" + System.getenv("EXTERNAL_STORAGE"); final String externalStorageEnv = "EXTERNAL_STORAGE=" + System.getenv("EXTERNAL_STORAGE");
String[] env; String[] env;
if (failSafe) { if (failSafe) {
// Keep the default path so that system binaries can be used in the failsafe session. // Keep the default path so that system binaries can be used in the failsafe session.
final String pathEnv = "PATH=" + System.getenv("PATH"); final String pathEnv = "PATH=" + System.getenv("PATH");
env = new String[] { termEnv, homeEnv, prefixEnv, androidRootEnv, androidDataEnv, pathEnv, externalStorageEnv }; 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"; 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, externalStorageEnv }; env = new String[]{termEnv, homeEnv, prefixEnv, ps1Env, ldEnv, langEnv, pathEnv, pwdEnv, androidRootEnv, androidDataEnv, externalStorageEnv};
} }
String shellName; String shellName;
if (executablePath == null) { if (executablePath == null) {
File shell = new File(HOME_PATH, ".termux/shell"); File shell = new File(HOME_PATH, ".termux/shell");
if (shell.exists()) { if (shell.exists()) {
try { try {
File canonicalFile = shell.getCanonicalFile(); File canonicalFile = shell.getCanonicalFile();
if (canonicalFile.isFile() && canonicalFile.canExecute()) { if (canonicalFile.isFile() && canonicalFile.canExecute()) {
executablePath = canonicalFile.getName().equals("busybox") ? (PREFIX_PATH + "/bin/ash") : canonicalFile.getAbsolutePath(); executablePath = canonicalFile.getName().equals("busybox") ? (PREFIX_PATH + "/bin/ash") : canonicalFile.getAbsolutePath();
} else { } else {
Log.w(EmulatorDebug.LOG_TAG, "$HOME/.termux/shell points to non-executable shell: " + canonicalFile.getAbsolutePath()); Log.w(EmulatorDebug.LOG_TAG, "$HOME/.termux/shell points to non-executable shell: " + canonicalFile.getAbsolutePath());
} }
} catch (IOException e) { } catch (IOException e) {
Log.e(EmulatorDebug.LOG_TAG, "Error checking $HOME/.termux/shell", e); Log.e(EmulatorDebug.LOG_TAG, "Error checking $HOME/.termux/shell", e);
} }
} }
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();
break; break;
} }
} }
} }
if (executablePath == null) { if (executablePath == null) {
// Fall back to system shell as last resort: // Fall back to system shell as last resort:
executablePath = "/system/bin/sh"; executablePath = "/system/bin/sh";
} }
String[] parts = executablePath.split("/"); String[] parts = executablePath.split("/");
shellName = "-" + parts[parts.length - 1]; shellName = "-" + parts[parts.length - 1];
} else { } else {
int lastSlashIndex = executablePath.lastIndexOf('/'); int lastSlashIndex = executablePath.lastIndexOf('/');
shellName = lastSlashIndex == -1 ? executablePath : executablePath.substring(lastSlashIndex + 1); shellName = lastSlashIndex == -1 ? executablePath : executablePath.substring(lastSlashIndex + 1);
} }
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;
System.arraycopy(arguments, 0, args, 1, arguments.length); System.arraycopy(arguments, 0, args, 1, arguments.length);
} }
TerminalSession session = new TerminalSession(executablePath, cwd, args, env, this); TerminalSession session = new TerminalSession(executablePath, cwd, args, env, this);
mTerminalSessions.add(session); mTerminalSessions.add(session);
updateNotification(); updateNotification();
return session; return session;
} }
public int removeTermSession(TerminalSession sessionToRemove) { public int removeTermSession(TerminalSession sessionToRemove) {
int indexOfRemoved = mTerminalSessions.indexOf(sessionToRemove); int indexOfRemoved = mTerminalSessions.indexOf(sessionToRemove);
mTerminalSessions.remove(indexOfRemoved); mTerminalSessions.remove(indexOfRemoved);
if (mTerminalSessions.isEmpty() && mWakeLock == null) { if (mTerminalSessions.isEmpty() && mWakeLock == null) {
// Finish if there are no sessions left and the wake lock is not held, otherwise keep the service alive if // Finish if there are no sessions left and the wake lock is not held, otherwise keep the service alive if
// holding wake lock since there may be daemon processes (e.g. sshd) running. // holding wake lock since there may be daemon processes (e.g. sshd) running.
stopSelf(); stopSelf();
} else { } else {
updateNotification(); updateNotification();
} }
return indexOfRemoved; return indexOfRemoved;
} }
@Override @Override
public void onTitleChanged(TerminalSession changedSession) { public void onTitleChanged(TerminalSession changedSession) {
if (mSessionChangeCallback != null) mSessionChangeCallback.onTitleChanged(changedSession); if (mSessionChangeCallback != null) mSessionChangeCallback.onTitleChanged(changedSession);
} }
@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
public void onTextChanged(TerminalSession changedSession) { public void onTextChanged(TerminalSession changedSession) {
if (mSessionChangeCallback != null) mSessionChangeCallback.onTextChanged(changedSession); if (mSessionChangeCallback != null) mSessionChangeCallback.onTextChanged(changedSession);
} }
@Override @Override
public void onClipboardText(TerminalSession session, String text) { public void onClipboardText(TerminalSession session, String text) {
if (mSessionChangeCallback != null) mSessionChangeCallback.onClipboardText(session, text); if (mSessionChangeCallback != null) mSessionChangeCallback.onClipboardText(session, text);
} }
@Override @Override
public void onBell(TerminalSession session) { public void onBell(TerminalSession session) {
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

@@ -23,9 +23,9 @@ import java.util.LinkedList;
/** /**
* A document provider for the Storage Access Framework which exposes the files in the * A document provider for the Storage Access Framework which exposes the files in the
* $HOME/ folder to other apps. * $HOME/ folder to other apps.
* <p> * <p/>
* Note that this replaces providing an activity matching the ACTION_GET_CONTENT intent: * Note that this replaces providing an activity matching the ACTION_GET_CONTENT intent:
* <p> * <p/>
* "A document provider and ACTION_GET_CONTENT should be considered mutually exclusive. If you * "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, * 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." * offering two different ways of accessing your stored data. This would be confusing for users."
@@ -172,7 +172,7 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
/** /**
* Get the document id given a file. This document id must be consistent across time as other * 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. * applications may save the ID and use it to reference documents later.
* <p> * <p/>
* The reverse of @{link #getFileForDocId}. * The reverse of @{link #getFileForDocId}.
*/ */
private static String getDocIdForFile(File file) { private static String getDocIdForFile(File file) {

View File

@@ -35,7 +35,7 @@ public class TermuxFileReceiverActivity extends Activity {
* name input dialog to be implicitly dismissed, and we do not want to finish the activity directly * name input dialog to be implicitly dismissed, and we do not want to finish the activity directly
* when showing the error dialog. * when showing the error dialog.
*/ */
private boolean mFinishOnDismissNameDialog = true; boolean mFinishOnDismissNameDialog = true;
@Override @Override
protected void onResume() { protected void onResume() {

View File

@@ -3,106 +3,106 @@ package com.termux.terminal;
/** A circular byte buffer allowing one producer and one consumer thread. */ /** A circular byte buffer allowing one producer and one consumer thread. */
final class ByteQueue { final class ByteQueue {
private final byte[] mBuffer; private final byte[] mBuffer;
private int mHead; private int mHead;
private int mStoredBytes; private int mStoredBytes;
private boolean mOpen = true; private boolean mOpen = true;
public ByteQueue(int size) { public ByteQueue(int size) {
mBuffer = new byte[size]; mBuffer = new byte[size];
} }
public synchronized void close() { public synchronized void close() {
mOpen = false; mOpen = false;
notify(); notify();
} }
public synchronized int read(byte[] buffer, boolean block) { public synchronized int read(byte[] buffer, boolean block) {
while (mStoredBytes == 0 && mOpen) { while (mStoredBytes == 0 && mOpen) {
if (block) { if (block) {
try { try {
wait(); wait();
} catch (InterruptedException e) { } catch (InterruptedException e) {
// Ignore. // Ignore.
} }
} else { } else {
return 0; return 0;
} }
} }
if (!mOpen) return -1; if (!mOpen) return -1;
int totalRead = 0; int totalRead = 0;
int bufferLength = mBuffer.length; int bufferLength = mBuffer.length;
boolean wasFull = bufferLength == mStoredBytes; boolean wasFull = bufferLength == mStoredBytes;
int length = buffer.length; int length = buffer.length;
int offset = 0; int offset = 0;
while (length > 0 && mStoredBytes > 0) { while (length > 0 && mStoredBytes > 0) {
int oneRun = Math.min(bufferLength - mHead, mStoredBytes); int oneRun = Math.min(bufferLength - mHead, mStoredBytes);
int bytesToCopy = Math.min(length, oneRun); int bytesToCopy = Math.min(length, oneRun);
System.arraycopy(mBuffer, mHead, buffer, offset, bytesToCopy); System.arraycopy(mBuffer, mHead, buffer, offset, bytesToCopy);
mHead += bytesToCopy; mHead += bytesToCopy;
if (mHead >= bufferLength) mHead = 0; if (mHead >= bufferLength) mHead = 0;
mStoredBytes -= bytesToCopy; mStoredBytes -= bytesToCopy;
length -= bytesToCopy; length -= bytesToCopy;
offset += bytesToCopy; offset += bytesToCopy;
totalRead += bytesToCopy; totalRead += bytesToCopy;
} }
if (wasFull) notify(); if (wasFull) notify();
return totalRead; return totalRead;
} }
/** /**
* 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) {
if (lengthToWrite + offset > buffer.length) { if (lengthToWrite + offset > buffer.length) {
throw new IllegalArgumentException("length + offset > buffer.length"); throw new IllegalArgumentException("length + offset > buffer.length");
} else if (lengthToWrite <= 0) { } else if (lengthToWrite <= 0) {
throw new IllegalArgumentException("length <= 0"); throw new IllegalArgumentException("length <= 0");
} }
final int bufferLength = mBuffer.length; final int bufferLength = mBuffer.length;
synchronized (this) { synchronized (this) {
while (lengthToWrite > 0) { while (lengthToWrite > 0) {
while (bufferLength == mStoredBytes && mOpen) { while (bufferLength == mStoredBytes && mOpen) {
try { try {
wait(); wait();
} catch (InterruptedException e) { } catch (InterruptedException e) {
// Ignore. // Ignore.
} }
} }
if (!mOpen) return false; if (!mOpen) return false;
final boolean wasEmpty = mStoredBytes == 0; final boolean wasEmpty = mStoredBytes == 0;
int bytesToWriteBeforeWaiting = Math.min(lengthToWrite, bufferLength - mStoredBytes); int bytesToWriteBeforeWaiting = Math.min(lengthToWrite, bufferLength - mStoredBytes);
lengthToWrite -= bytesToWriteBeforeWaiting; lengthToWrite -= bytesToWriteBeforeWaiting;
while (bytesToWriteBeforeWaiting > 0) { while (bytesToWriteBeforeWaiting > 0) {
int tail = mHead + mStoredBytes; int tail = mHead + mStoredBytes;
int oneRun; int oneRun;
if (tail >= bufferLength) { if (tail >= bufferLength) {
// Buffer: [.............] // Buffer: [.............]
// ________________H_______T // ________________H_______T
// => // =>
// Buffer: [.............] // Buffer: [.............]
// ___________T____H // ___________T____H
// onRun= _____----_ // onRun= _____----_
tail = tail - bufferLength; tail = tail - bufferLength;
oneRun = mHead - tail; oneRun = mHead - tail;
} else { } else {
oneRun = bufferLength - tail; oneRun = bufferLength - tail;
} }
int bytesToCopy = Math.min(oneRun, bytesToWriteBeforeWaiting); int bytesToCopy = Math.min(oneRun, bytesToWriteBeforeWaiting);
System.arraycopy(buffer, offset, mBuffer, tail, bytesToCopy); System.arraycopy(buffer, offset, mBuffer, tail, bytesToCopy);
offset += bytesToCopy; offset += bytesToCopy;
bytesToWriteBeforeWaiting -= bytesToCopy; bytesToWriteBeforeWaiting -= bytesToCopy;
mStoredBytes += bytesToCopy; mStoredBytes += bytesToCopy;
} }
if (wasEmpty) notify(); if (wasEmpty) notify();
} }
} }
return true; return true;
} }
} }

View File

@@ -4,7 +4,7 @@ import android.util.Log;
public final class EmulatorDebug { public final class EmulatorDebug {
/** The tag to use with {@link Log}. */ /** The tag to use with {@link Log}. */
public static final String LOG_TAG = "termux"; public static final String LOG_TAG = "termux";
} }

View File

@@ -5,51 +5,37 @@ package com.termux.terminal;
*/ */
final class JNI { final class JNI {
static { static {
System.loadLibrary("termux"); System.loadLibrary("termux");
} }
/** /**
* 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 * @return the file descriptor resulting from opening /dev/ptmx master device. The sub process will have opened the
* @param envVars * slave device counterpart (/dev/pts/$N) and have it as stdint, stdout and stderr.
* An array of strings of the form "VAR=value" to be added to the environment of the process */
* @param processId public static native int createSubprocess(String cmd, String cwd, String[] args, String[] envVars, int[] processId, int rows, int columns);
* 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
* 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, 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);
/** /**
* Causes the calling thread to wait for the process associated with the receiver to finish executing. * Causes the calling thread to wait for the process associated with the receiver to finish executing.
* *
* @return if >= 0, the exit status of the process. If < 0, the signal causing the process to stop negated. * @return if >= 0, the exit status of the process. If < 0, the signal causing the process to stop negated.
*/ */
public static native int waitFor(int processId); public static native int waitFor(int processId);
/** /** Close a file descriptor through the close(2) system call. */
* Send SIGHUP to a process group. public static native void close(int fileDescriptor);
*
* 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. */
public static native void close(int fileDescriptor);
} }

View File

@@ -1,5 +1,9 @@
package com.termux.terminal; package com.termux.terminal;
import java.util.HashMap;
import java.util.Map;
import static android.view.KeyEvent.KEYCODE_BACK;
import static android.view.KeyEvent.KEYCODE_BREAK; import static android.view.KeyEvent.KEYCODE_BREAK;
import static android.view.KeyEvent.KEYCODE_DEL; import static android.view.KeyEvent.KEYCODE_DEL;
import static android.view.KeyEvent.KEYCODE_DPAD_CENTER; import static android.view.KeyEvent.KEYCODE_DPAD_CENTER;
@@ -24,6 +28,7 @@ import static android.view.KeyEvent.KEYCODE_F9;
import static android.view.KeyEvent.KEYCODE_FORWARD_DEL; import static android.view.KeyEvent.KEYCODE_FORWARD_DEL;
import static android.view.KeyEvent.KEYCODE_INSERT; import static android.view.KeyEvent.KEYCODE_INSERT;
import static android.view.KeyEvent.KEYCODE_MOVE_END; import static android.view.KeyEvent.KEYCODE_MOVE_END;
import static android.view.KeyEvent.KEYCODE_MOVE_HOME;
import static android.view.KeyEvent.KEYCODE_NUMPAD_0; import static android.view.KeyEvent.KEYCODE_NUMPAD_0;
import static android.view.KeyEvent.KEYCODE_NUMPAD_1; import static android.view.KeyEvent.KEYCODE_NUMPAD_1;
import static android.view.KeyEvent.KEYCODE_NUMPAD_2; import static android.view.KeyEvent.KEYCODE_NUMPAD_2;
@@ -45,266 +50,264 @@ 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 {
public static final int KEYMOD_ALT = 0x80000000; public static final int KEYMOD_ALT = 0x80000000;
public static final int KEYMOD_CTRL = 0x40000000; public static final int KEYMOD_CTRL = 0x40000000;
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 {
// terminfo: http://pubs.opengroup.org/onlinepubs/7990989799/xcurses/terminfo.html
// termcap: http://man7.org/linux/man-pages/man5/termcap.5.html
TERMCAP_TO_KEYCODE.put("%i", KEYMOD_SHIFT | KEYCODE_DPAD_RIGHT);
TERMCAP_TO_KEYCODE.put("#2", KEYMOD_SHIFT | KEYCODE_HOME); // Shifted home
TERMCAP_TO_KEYCODE.put("#4", KEYMOD_SHIFT | KEYCODE_DPAD_LEFT);
TERMCAP_TO_KEYCODE.put("*7", KEYMOD_SHIFT | KEYCODE_MOVE_END); // Shifted end key
TERMCAP_TO_KEYCODE.put("k1", KEYCODE_F1); static {
TERMCAP_TO_KEYCODE.put("k2", KEYCODE_F2); // terminfo: http://pubs.opengroup.org/onlinepubs/7990989799/xcurses/terminfo.html
TERMCAP_TO_KEYCODE.put("k3", KEYCODE_F3); // termcap: http://man7.org/linux/man-pages/man5/termcap.5.html
TERMCAP_TO_KEYCODE.put("k4", KEYCODE_F4); TERMCAP_TO_KEYCODE.put("%i", KEYMOD_SHIFT | KEYCODE_DPAD_RIGHT);
TERMCAP_TO_KEYCODE.put("k5", KEYCODE_F5); TERMCAP_TO_KEYCODE.put("#2", KEYMOD_SHIFT | KEYCODE_MOVE_HOME); // Shifted home
TERMCAP_TO_KEYCODE.put("k6", KEYCODE_F6); TERMCAP_TO_KEYCODE.put("#4", KEYMOD_SHIFT | KEYCODE_DPAD_LEFT);
TERMCAP_TO_KEYCODE.put("k7", KEYCODE_F7); TERMCAP_TO_KEYCODE.put("*7", KEYMOD_SHIFT | KEYCODE_MOVE_END); // Shifted end key
TERMCAP_TO_KEYCODE.put("k8", KEYCODE_F8);
TERMCAP_TO_KEYCODE.put("k9", KEYCODE_F9);
TERMCAP_TO_KEYCODE.put("k;", KEYCODE_F10);
TERMCAP_TO_KEYCODE.put("F1", KEYCODE_F11);
TERMCAP_TO_KEYCODE.put("F2", KEYCODE_F12);
TERMCAP_TO_KEYCODE.put("F3", KEYMOD_SHIFT | KEYCODE_F1);
TERMCAP_TO_KEYCODE.put("F4", KEYMOD_SHIFT | KEYCODE_F2);
TERMCAP_TO_KEYCODE.put("F5", KEYMOD_SHIFT | KEYCODE_F3);
TERMCAP_TO_KEYCODE.put("F6", KEYMOD_SHIFT | KEYCODE_F4);
TERMCAP_TO_KEYCODE.put("F7", KEYMOD_SHIFT | KEYCODE_F5);
TERMCAP_TO_KEYCODE.put("F8", KEYMOD_SHIFT | KEYCODE_F6);
TERMCAP_TO_KEYCODE.put("F9", KEYMOD_SHIFT | KEYCODE_F7);
TERMCAP_TO_KEYCODE.put("FA", KEYMOD_SHIFT | KEYCODE_F8);
TERMCAP_TO_KEYCODE.put("FB", KEYMOD_SHIFT | KEYCODE_F9);
TERMCAP_TO_KEYCODE.put("FC", KEYMOD_SHIFT | KEYCODE_F10);
TERMCAP_TO_KEYCODE.put("FD", KEYMOD_SHIFT | KEYCODE_F11);
TERMCAP_TO_KEYCODE.put("FE", KEYMOD_SHIFT | KEYCODE_F12);
TERMCAP_TO_KEYCODE.put("kb", KEYCODE_DEL); // backspace key TERMCAP_TO_KEYCODE.put("k1", KEYCODE_F1);
TERMCAP_TO_KEYCODE.put("k2", KEYCODE_F2);
TERMCAP_TO_KEYCODE.put("k3", KEYCODE_F3);
TERMCAP_TO_KEYCODE.put("k4", KEYCODE_F4);
TERMCAP_TO_KEYCODE.put("k5", KEYCODE_F5);
TERMCAP_TO_KEYCODE.put("k6", KEYCODE_F6);
TERMCAP_TO_KEYCODE.put("k7", KEYCODE_F7);
TERMCAP_TO_KEYCODE.put("k8", KEYCODE_F8);
TERMCAP_TO_KEYCODE.put("k9", KEYCODE_F9);
TERMCAP_TO_KEYCODE.put("k;", KEYCODE_F10);
TERMCAP_TO_KEYCODE.put("F1", KEYCODE_F11);
TERMCAP_TO_KEYCODE.put("F2", KEYCODE_F12);
TERMCAP_TO_KEYCODE.put("F3", KEYMOD_SHIFT | KEYCODE_F1);
TERMCAP_TO_KEYCODE.put("F4", KEYMOD_SHIFT | KEYCODE_F2);
TERMCAP_TO_KEYCODE.put("F5", KEYMOD_SHIFT | KEYCODE_F3);
TERMCAP_TO_KEYCODE.put("F6", KEYMOD_SHIFT | KEYCODE_F4);
TERMCAP_TO_KEYCODE.put("F7", KEYMOD_SHIFT | KEYCODE_F5);
TERMCAP_TO_KEYCODE.put("F8", KEYMOD_SHIFT | KEYCODE_F6);
TERMCAP_TO_KEYCODE.put("F9", KEYMOD_SHIFT | KEYCODE_F7);
TERMCAP_TO_KEYCODE.put("FA", KEYMOD_SHIFT | KEYCODE_F8);
TERMCAP_TO_KEYCODE.put("FB", KEYMOD_SHIFT | KEYCODE_F9);
TERMCAP_TO_KEYCODE.put("FC", KEYMOD_SHIFT | KEYCODE_F10);
TERMCAP_TO_KEYCODE.put("FD", KEYMOD_SHIFT | KEYCODE_F11);
TERMCAP_TO_KEYCODE.put("FE", KEYMOD_SHIFT | KEYCODE_F12);
TERMCAP_TO_KEYCODE.put("kd", KEYCODE_DPAD_DOWN); // terminfo=kcud1, down-arrow key TERMCAP_TO_KEYCODE.put("kb", KEYCODE_DEL); // backspace key
TERMCAP_TO_KEYCODE.put("kh", KeyEvent.KEYCODE_HOME);
TERMCAP_TO_KEYCODE.put("kl", KEYCODE_DPAD_LEFT);
TERMCAP_TO_KEYCODE.put("kr", KEYCODE_DPAD_RIGHT);
// K1=Upper left of keypad: TERMCAP_TO_KEYCODE.put("kd", KEYCODE_DPAD_DOWN); // terminfo=kcud1, down-arrow key
// t_K1 <kHome> keypad home key TERMCAP_TO_KEYCODE.put("kh", KEYCODE_MOVE_HOME);
// t_K3 <kPageUp> keypad page-up key TERMCAP_TO_KEYCODE.put("kl", KEYCODE_DPAD_LEFT);
// t_K4 <kEnd> keypad end key TERMCAP_TO_KEYCODE.put("kr", KEYCODE_DPAD_RIGHT);
// t_K5 <kPageDown> keypad page-down key
TERMCAP_TO_KEYCODE.put("K1", KeyEvent.KEYCODE_HOME);
TERMCAP_TO_KEYCODE.put("K3", KeyEvent.KEYCODE_PAGE_UP);
TERMCAP_TO_KEYCODE.put("K4", KeyEvent.KEYCODE_MOVE_END);
TERMCAP_TO_KEYCODE.put("K5", KeyEvent.KEYCODE_PAGE_DOWN);
TERMCAP_TO_KEYCODE.put("ku", KEYCODE_DPAD_UP); // K1=Upper left of keypad:
// t_K1 <kHome> keypad home key
// t_K3 <kPageUp> keypad page-up key
// t_K4 <kEnd> keypad end key
// t_K5 <kPageDown> keypad page-down key
TERMCAP_TO_KEYCODE.put("K1", KEYCODE_MOVE_HOME);
TERMCAP_TO_KEYCODE.put("K3", KEYCODE_PAGE_UP);
TERMCAP_TO_KEYCODE.put("K4", KEYCODE_MOVE_END);
TERMCAP_TO_KEYCODE.put("K5", KEYCODE_PAGE_DOWN);
TERMCAP_TO_KEYCODE.put("kB", KEYMOD_SHIFT | KEYCODE_TAB); // termcap=kB, terminfo=kcbt: Back-tab TERMCAP_TO_KEYCODE.put("ku", KEYCODE_DPAD_UP);
TERMCAP_TO_KEYCODE.put("kD", KEYCODE_FORWARD_DEL); // terminfo=kdch1, delete-character key
TERMCAP_TO_KEYCODE.put("kDN", KEYMOD_SHIFT | KEYCODE_DPAD_DOWN); // non-standard shifted arrow down
TERMCAP_TO_KEYCODE.put("kF", KEYMOD_SHIFT | KEYCODE_DPAD_DOWN); // terminfo=kind, scroll-forward key
TERMCAP_TO_KEYCODE.put("kI", KEYCODE_INSERT);
TERMCAP_TO_KEYCODE.put("kN", KEYCODE_PAGE_UP);
TERMCAP_TO_KEYCODE.put("kP", KEYCODE_PAGE_DOWN);
TERMCAP_TO_KEYCODE.put("kR", KEYMOD_SHIFT | KEYCODE_DPAD_UP); // terminfo=kri, scroll-backward key
TERMCAP_TO_KEYCODE.put("kUP", KEYMOD_SHIFT | KEYCODE_DPAD_UP); // non-standard shifted up
TERMCAP_TO_KEYCODE.put("@7", KEYCODE_MOVE_END); TERMCAP_TO_KEYCODE.put("kB", KEYMOD_SHIFT | KEYCODE_TAB); // termcap=kB, terminfo=kcbt: Back-tab
TERMCAP_TO_KEYCODE.put("@8", KEYCODE_NUMPAD_ENTER); TERMCAP_TO_KEYCODE.put("kD", KEYCODE_FORWARD_DEL); // terminfo=kdch1, delete-character key
} TERMCAP_TO_KEYCODE.put("kDN", KEYMOD_SHIFT | KEYCODE_DPAD_DOWN); // non-standard shifted arrow down
TERMCAP_TO_KEYCODE.put("kF", KEYMOD_SHIFT | KEYCODE_DPAD_DOWN); // terminfo=kind, scroll-forward key
TERMCAP_TO_KEYCODE.put("kI", KEYCODE_INSERT);
TERMCAP_TO_KEYCODE.put("kN", KEYCODE_PAGE_UP);
TERMCAP_TO_KEYCODE.put("kP", KEYCODE_PAGE_DOWN);
TERMCAP_TO_KEYCODE.put("kR", KEYMOD_SHIFT | KEYCODE_DPAD_UP); // terminfo=kri, scroll-backward key
TERMCAP_TO_KEYCODE.put("kUP", KEYMOD_SHIFT | KEYCODE_DPAD_UP); // non-standard shifted up
static String getCodeFromTermcap(String termcap, boolean cursorKeysApplication, boolean keypadApplication) { TERMCAP_TO_KEYCODE.put("@7", KEYCODE_MOVE_END);
Integer keyCodeAndMod = TERMCAP_TO_KEYCODE.get(termcap); TERMCAP_TO_KEYCODE.put("@8", KEYCODE_NUMPAD_ENTER);
if (keyCodeAndMod == null) return null; }
int keyCode = keyCodeAndMod;
int keyMod = 0;
if ((keyCode & KEYMOD_SHIFT) != 0) {
keyMod |= KEYMOD_SHIFT;
keyCode &= ~KEYMOD_SHIFT;
}
if ((keyCode & KEYMOD_CTRL) != 0) {
keyMod |= KEYMOD_CTRL;
keyCode &= ~KEYMOD_CTRL;
}
if ((keyCode & KEYMOD_ALT) != 0) {
keyMod |= KEYMOD_ALT;
keyCode &= ~KEYMOD_ALT;
}
return getCode(keyCode, keyMod, cursorKeysApplication, keypadApplication);
}
public static String getCode(int keyCode, int keyMode, boolean cursorApp, boolean keypadApplication) { static String getCodeFromTermcap(String termcap, boolean cursorKeysApplication, boolean keypadApplication) {
switch (keyCode) { Integer keyCodeAndMod = TERMCAP_TO_KEYCODE.get(termcap);
case KEYCODE_DPAD_CENTER: if (keyCodeAndMod == null) return null;
return "\015"; int keyCode = keyCodeAndMod;
int keyMod = 0;
if ((keyCode & KEYMOD_SHIFT) != 0) {
keyMod |= KEYMOD_SHIFT;
keyCode &= ~KEYMOD_SHIFT;
}
if ((keyCode & KEYMOD_CTRL) != 0) {
keyMod |= KEYMOD_CTRL;
keyCode &= ~KEYMOD_CTRL;
}
if ((keyCode & KEYMOD_ALT) != 0) {
keyMod |= KEYMOD_ALT;
keyCode &= ~KEYMOD_ALT;
}
return getCode(keyCode, keyMod, cursorKeysApplication, keypadApplication);
}
case KEYCODE_DPAD_UP: public static String getCode(int keyCode, int keyMode, boolean cursorApp, boolean keypadApplication) {
return (keyMode == 0) ? (cursorApp ? "\033OA" : "\033[A") : transformForModifiers("\033[1", keyMode, 'A'); switch (keyCode) {
case KEYCODE_DPAD_DOWN: case KEYCODE_DPAD_CENTER:
return (keyMode == 0) ? (cursorApp ? "\033OB" : "\033[B") : transformForModifiers("\033[1", keyMode, 'B'); return "\015";
case KEYCODE_DPAD_RIGHT:
return (keyMode == 0) ? (cursorApp ? "\033OC" : "\033[C") : transformForModifiers("\033[1", keyMode, 'C');
case KEYCODE_DPAD_LEFT:
return (keyMode == 0) ? (cursorApp ? "\033OD" : "\033[D") : transformForModifiers("\033[1", keyMode, 'D');
case KeyEvent.KEYCODE_HOME: case KEYCODE_DPAD_UP:
return (keyMode == 0) ? (cursorApp ? "\033OH" : "\033[H") : transformForModifiers("\033[1", keyMode, 'H'); return (keyMode == 0) ? (cursorApp ? "\033OA" : "\033[A") : transformForModifiers("\033[1", keyMode, 'A');
case KEYCODE_MOVE_END: case KEYCODE_DPAD_DOWN:
return (keyMode == 0) ? (cursorApp ? "\033OF" : "\033[F") : transformForModifiers("\033[1", keyMode, 'F'); return (keyMode == 0) ? (cursorApp ? "\033OB" : "\033[B") : transformForModifiers("\033[1", keyMode, 'B');
case KEYCODE_DPAD_RIGHT:
return (keyMode == 0) ? (cursorApp ? "\033OC" : "\033[C") : transformForModifiers("\033[1", keyMode, 'C');
case KEYCODE_DPAD_LEFT:
return (keyMode == 0) ? (cursorApp ? "\033OD" : "\033[D") : transformForModifiers("\033[1", keyMode, 'D');
// An xterm can send function keys F1 to F4 in two modes: vt100 compatible or case KEYCODE_MOVE_HOME:
// not. Because Vim may not know what the xterm is sending, both types of keys // Note that KEYCODE_HOME is handled by the system and never delivered to applications.
// are recognized. The same happens for the <Home> and <End> keys. // On a Logitech k810 keyboard KEYCODE_MOVE_HOME is sent by FN+LeftArrow.
// normal vt100 ~ return (keyMode == 0) ? (cursorApp ? "\033OH" : "\033[H") : transformForModifiers("\033[1", keyMode, 'H');
// <F1> t_k1 <Esc>[11~ <xF1> <Esc>OP *<xF1>-xterm* case KEYCODE_MOVE_END:
// <F2> t_k2 <Esc>[12~ <xF2> <Esc>OQ *<xF2>-xterm* return (keyMode == 0) ? (cursorApp ? "\033OF" : "\033[F") : transformForModifiers("\033[1", keyMode, 'F');
// <F3> t_k3 <Esc>[13~ <xF3> <Esc>OR *<xF3>-xterm*
// <F4> t_k4 <Esc>[14~ <xF4> <Esc>OS *<xF4>-xterm*
// <Home> t_kh <Esc>[7~ <xHome> <Esc>OH *<xHome>-xterm*
// <End> t_@7 <Esc>[4~ <xEnd> <Esc>OF *<xEnd>-xterm*
case KEYCODE_F1:
return (keyMode == 0) ? "\033OP" : transformForModifiers("\033[1", keyMode, 'P');
case KEYCODE_F2:
return (keyMode == 0) ? "\033OQ" : transformForModifiers("\033[1", keyMode, 'Q');
case KEYCODE_F3:
return (keyMode == 0) ? "\033OR" : transformForModifiers("\033[1", keyMode, 'R');
case KEYCODE_F4:
return (keyMode == 0) ? "\033OS" : transformForModifiers("\033[1", keyMode, 'S');
case KEYCODE_F5:
return transformForModifiers("\033[15", keyMode, '~');
case KEYCODE_F6:
return transformForModifiers("\033[17", keyMode, '~');
case KEYCODE_F7:
return transformForModifiers("\033[18", keyMode, '~');
case KEYCODE_F8:
return transformForModifiers("\033[19", keyMode, '~');
case KEYCODE_F9:
return transformForModifiers("\033[20", keyMode, '~');
case KEYCODE_F10:
return transformForModifiers("\033[21", keyMode, '~');
case KEYCODE_F11:
return transformForModifiers("\033[23", keyMode, '~');
case KEYCODE_F12:
return transformForModifiers("\033[24", keyMode, '~');
case KEYCODE_SYSRQ: // An xterm can send function keys F1 to F4 in two modes: vt100 compatible or
return "\033[32~"; // Sys Request / Print // not. Because Vim may not know what the xterm is sending, both types of keys
// Is this Scroll lock? case Cancel: return "\033[33~"; // are recognized. The same happens for the <Home> and <End> keys.
case KEYCODE_BREAK: // normal vt100 ~
return "\033[34~"; // Pause/Break // <F1> t_k1 <Esc>[11~ <xF1> <Esc>OP *<xF1>-xterm*
// <F2> t_k2 <Esc>[12~ <xF2> <Esc>OQ *<xF2>-xterm*
// <F3> t_k3 <Esc>[13~ <xF3> <Esc>OR *<xF3>-xterm*
// <F4> t_k4 <Esc>[14~ <xF4> <Esc>OS *<xF4>-xterm*
// <Home> t_kh <Esc>[7~ <xHome> <Esc>OH *<xHome>-xterm*
// <End> t_@7 <Esc>[4~ <xEnd> <Esc>OF *<xEnd>-xterm*
case KEYCODE_F1:
return (keyMode == 0) ? "\033OP" : transformForModifiers("\033[1", keyMode, 'P');
case KEYCODE_F2:
return (keyMode == 0) ? "\033OQ" : transformForModifiers("\033[1", keyMode, 'Q');
case KEYCODE_F3:
return (keyMode == 0) ? "\033OR" : transformForModifiers("\033[1", keyMode, 'R');
case KEYCODE_F4:
return (keyMode == 0) ? "\033OS" : transformForModifiers("\033[1", keyMode, 'S');
case KEYCODE_F5:
return transformForModifiers("\033[15", keyMode, '~');
case KEYCODE_F6:
return transformForModifiers("\033[17", keyMode, '~');
case KEYCODE_F7:
return transformForModifiers("\033[18", keyMode, '~');
case KEYCODE_F8:
return transformForModifiers("\033[19", keyMode, '~');
case KEYCODE_F9:
return transformForModifiers("\033[20", keyMode, '~');
case KEYCODE_F10:
return transformForModifiers("\033[21", keyMode, '~');
case KEYCODE_F11:
return transformForModifiers("\033[23", keyMode, '~');
case KEYCODE_F12:
return transformForModifiers("\033[24", keyMode, '~');
case KEYCODE_ESCAPE: case KEYCODE_SYSRQ:
case KeyEvent.KEYCODE_BACK: return "\033[32~"; // Sys Request / Print
return "\033"; // Is this Scroll lock? case Cancel: return "\033[33~";
case KEYCODE_BREAK:
return "\033[34~"; // Pause/Break
case KEYCODE_INSERT: case KEYCODE_ESCAPE:
return transformForModifiers("\033[2", keyMode, '~'); case KEYCODE_BACK:
case KEYCODE_FORWARD_DEL: return "\033";
return transformForModifiers("\033[3", keyMode, '~');
case KEYCODE_NUMPAD_DOT: case KEYCODE_INSERT:
return keypadApplication ? "\033On" : "\033[3~"; return transformForModifiers("\033[2", keyMode, '~');
case KEYCODE_FORWARD_DEL:
return transformForModifiers("\033[3", keyMode, '~');
case KEYCODE_PAGE_UP: case KEYCODE_PAGE_UP:
return "\033[5~"; return "\033[5~";
case KEYCODE_PAGE_DOWN: case KEYCODE_PAGE_DOWN:
return "\033[6~"; return "\033[6~";
case KEYCODE_DEL: case KEYCODE_DEL:
// Yes, this needs to U+007F and not U+0008! String prefix = ((keyMode & KEYMOD_ALT) == 0) ? "" : "\033";
return "\u007F"; // Just do what xterm and gnome-terminal does:
case KEYCODE_NUM_LOCK: return prefix + (((keyMode & KEYMOD_CTRL) == 0) ? "\u007F" : "\u0008");
return "\033OP"; case KEYCODE_NUM_LOCK:
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";
case KEYCODE_TAB: case KEYCODE_TAB:
// This is back-tab when shifted: // This is back-tab when shifted:
return (keyMode & KEYMOD_SHIFT) == 0 ? "\011" : "\033[Z"; return (keyMode & KEYMOD_SHIFT) == 0 ? "\011" : "\033[Z";
case KEYCODE_ENTER: case KEYCODE_ENTER:
return ((keyMode & KEYMOD_ALT) == 0) ? "\r" : "\033\r"; return ((keyMode & KEYMOD_ALT) == 0) ? "\r" : "\033\r";
case KEYCODE_NUMPAD_ENTER: case KEYCODE_NUMPAD_ENTER:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'M') : "\n"; return keypadApplication ? transformForModifiers("\033O", keyMode, 'M') : "\n";
case KEYCODE_NUMPAD_MULTIPLY: case KEYCODE_NUMPAD_MULTIPLY:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'j') : "*"; return keypadApplication ? transformForModifiers("\033O", keyMode, 'j') : "*";
case KEYCODE_NUMPAD_ADD: case KEYCODE_NUMPAD_ADD:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'k') : "+"; return keypadApplication ? transformForModifiers("\033O", keyMode, 'k') : "+";
case KEYCODE_NUMPAD_COMMA: case KEYCODE_NUMPAD_COMMA:
return ","; return ",";
case KEYCODE_NUMPAD_SUBTRACT: case KEYCODE_NUMPAD_DOT:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'm') : "-"; return keypadApplication ? "\033On" : ".";
case KEYCODE_NUMPAD_DIVIDE: case KEYCODE_NUMPAD_SUBTRACT:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'o') : "/"; return keypadApplication ? transformForModifiers("\033O", keyMode, 'm') : "-";
case KEYCODE_NUMPAD_0: case KEYCODE_NUMPAD_DIVIDE:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'p') : "1"; return keypadApplication ? transformForModifiers("\033O", keyMode, 'o') : "/";
case KEYCODE_NUMPAD_1: case KEYCODE_NUMPAD_0:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'q') : "1"; return keypadApplication ? transformForModifiers("\033O", keyMode, 'p') : "0";
case KEYCODE_NUMPAD_2: case KEYCODE_NUMPAD_1:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'r') : "2"; return keypadApplication ? transformForModifiers("\033O", keyMode, 'q') : "1";
case KEYCODE_NUMPAD_3: case KEYCODE_NUMPAD_2:
return keypadApplication ? transformForModifiers("\033O", keyMode, 's') : "3"; return keypadApplication ? transformForModifiers("\033O", keyMode, 'r') : "2";
case KEYCODE_NUMPAD_4: case KEYCODE_NUMPAD_3:
return keypadApplication ? transformForModifiers("\033O", keyMode, 't') : "4"; return keypadApplication ? transformForModifiers("\033O", keyMode, 's') : "3";
case KEYCODE_NUMPAD_5: case KEYCODE_NUMPAD_4:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'u') : "5"; return keypadApplication ? transformForModifiers("\033O", keyMode, 't') : "4";
case KEYCODE_NUMPAD_6: case KEYCODE_NUMPAD_5:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'v') : "6"; return keypadApplication ? transformForModifiers("\033O", keyMode, 'u') : "5";
case KEYCODE_NUMPAD_7: case KEYCODE_NUMPAD_6:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'w') : "7"; return keypadApplication ? transformForModifiers("\033O", keyMode, 'v') : "6";
case KEYCODE_NUMPAD_8: case KEYCODE_NUMPAD_7:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'x') : "8"; return keypadApplication ? transformForModifiers("\033O", keyMode, 'w') : "7";
case KEYCODE_NUMPAD_9: case KEYCODE_NUMPAD_8:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'y') : "9"; return keypadApplication ? transformForModifiers("\033O", keyMode, 'x') : "8";
case KEYCODE_NUMPAD_EQUALS: case KEYCODE_NUMPAD_9:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'X') : "="; return keypadApplication ? transformForModifiers("\033O", keyMode, 'y') : "9";
} case KEYCODE_NUMPAD_EQUALS:
return keypadApplication ? transformForModifiers("\033O", keyMode, 'X') : "=";
}
return null; return null;
} }
private static String transformForModifiers(String start, int keymod, char lastChar) { private static String transformForModifiers(String start, int keymod, char lastChar) {
int modifier; int modifier;
switch (keymod) { switch (keymod) {
case KEYMOD_SHIFT: case KEYMOD_SHIFT:
modifier = 2; modifier = 2;
break; break;
case KEYMOD_ALT: case KEYMOD_ALT:
modifier = 3; modifier = 3;
break; break;
case (KEYMOD_SHIFT | KEYMOD_ALT): case (KEYMOD_SHIFT | KEYMOD_ALT):
modifier = 4; modifier = 4;
break; break;
case KEYMOD_CTRL: case KEYMOD_CTRL:
modifier = 5; modifier = 5;
break; break;
case KEYMOD_SHIFT | KEYMOD_CTRL: case KEYMOD_SHIFT | KEYMOD_CTRL:
modifier = 6; modifier = 6;
break; break;
case KEYMOD_ALT | KEYMOD_CTRL: case KEYMOD_ALT | KEYMOD_CTRL:
modifier = 7; modifier = 7;
break; break;
case KEYMOD_SHIFT | KEYMOD_ALT | KEYMOD_CTRL: case KEYMOD_SHIFT | KEYMOD_ALT | KEYMOD_CTRL:
modifier = 8; modifier = 8;
break; break;
default: default:
return start + lastChar; return start + lastChar;
} }
return start + (";" + modifier) + lastChar; return start + (";" + modifier) + lastChar;
} }
} }

View File

@@ -3,441 +3,425 @@ 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 {
TerminalRow[] mLines; TerminalRow[] mLines;
/** The length of {@link #mLines}. */ /** The length of {@link #mLines}. */
int mTotalRows; int mTotalRows;
/** The number of rows and columns visible on the screen. */ /** The number of rows and columns visible on the screen. */
int mScreenRows, mColumns; int mScreenRows, mColumns;
/** The number of rows kept in history. */ /** The number of rows kept in history. */
private int mActiveTranscriptRows = 0; private int mActiveTranscriptRows = 0;
/** The index in the circular buffer where the visible screen starts. */ /** The index in the circular buffer where the visible screen starts. */
private int mScreenFirstRow = 0; private int mScreenFirstRow = 0;
/** /**
* 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. * the top of the screen.
* @param screenRows */
* the height of just the screen, not including the transcript that holds lines that have scrolled off public TerminalBuffer(int columns, int totalRows, int screenRows) {
* the top of the screen. mColumns = columns;
*/ mTotalRows = totalRows;
public TerminalBuffer(int columns, int totalRows, int screenRows) { mScreenRows = screenRows;
mColumns = columns; mLines = new TerminalRow[totalRows];
mTotalRows = totalRows;
mScreenRows = screenRows;
mLines = new TerminalRow[totalRows];
blockSet(0, 0, columns, screenRows, ' ', TextStyle.NORMAL); blockSet(0, 0, columns, screenRows, ' ', TextStyle.NORMAL);
} }
public String getTranscriptText() { public String getTranscriptText() {
return getSelectedText(0, -getActiveTranscriptRows(), mColumns, mScreenRows).trim(); return getSelectedText(0, -getActiveTranscriptRows(), mColumns, mScreenRows).trim();
} }
public String getSelectedText(int selX1, int selY1, int selX2, int selY2) { public String getSelectedText(int selX1, int selY1, int selX2, int selY2) {
final StringBuilder builder = new StringBuilder(); final StringBuilder builder = new StringBuilder();
final int columns = mColumns; final int columns = mColumns;
if (selY1 < -getActiveTranscriptRows()) selY1 = -getActiveTranscriptRows(); if (selY1 < -getActiveTranscriptRows()) selY1 = -getActiveTranscriptRows();
if (selY2 >= mScreenRows) selY2 = mScreenRows - 1; if (selY2 >= mScreenRows) selY2 = mScreenRows - 1;
for (int row = selY1; row <= selY2; row++) { for (int row = selY1; row <= selY2; row++) {
int x1 = (row == selY1) ? selX1 : 0; int x1 = (row == selY1) ? selX1 : 0;
int x2; int x2;
if (row == selY2) { if (row == selY2) {
x2 = selX2 + 1; x2 = selX2 + 1;
if (x2 > columns) x2 = columns; if (x2 > columns) x2 = columns;
} else { } else {
x2 = columns; x2 = columns;
} }
TerminalRow lineObject = mLines[externalToInternalRow(row)]; TerminalRow lineObject = mLines[externalToInternalRow(row)];
int x1Index = lineObject.findStartOfColumn(x1); int x1Index = lineObject.findStartOfColumn(x1);
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;
int i; int i;
boolean rowLineWrap = getLineWrap(row); boolean rowLineWrap = getLineWrap(row);
if (rowLineWrap && x2 == columns) { if (rowLineWrap && x2 == columns) {
// If the line was wrapped, we shouldn't lose trailing space: // If the line was wrapped, we shouldn't lose trailing space:
lastPrintingCharIndex = x2Index - 1; lastPrintingCharIndex = x2Index - 1;
} else { } else {
for (i = x1Index; i < x2Index; ++i) { for (i = x1Index; i < x2Index; ++i) {
char c = line[i]; char c = line[i];
if (c != ' ') lastPrintingCharIndex = i; if (c != ' ') lastPrintingCharIndex = i;
} }
} }
if (lastPrintingCharIndex != -1) builder.append(line, x1Index, lastPrintingCharIndex - x1Index + 1); if (lastPrintingCharIndex != -1)
if (!rowLineWrap && row < selY2 && row < mScreenRows - 1) builder.append('\n'); builder.append(line, x1Index, lastPrintingCharIndex - x1Index + 1);
} if (!rowLineWrap && row < selY2 && row < mScreenRows - 1) builder.append('\n');
return builder.toString(); }
} return builder.toString();
}
public int getActiveTranscriptRows() { public int getActiveTranscriptRows() {
return mActiveTranscriptRows; return mActiveTranscriptRows;
} }
public int getActiveRows() { public int getActiveRows() {
return mActiveTranscriptRows + mScreenRows; return mActiveTranscriptRows + mScreenRows;
} }
/** /**
* 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 ]
* [ ... ] [ ... ] * [ ... ] [ ... ]
* [ 0 (visible screen starts here) ] <-----> [ mScreenFirstRow ] * [ 0 (visible screen starts here) ] <-----> [ mScreenFirstRow ]
* [ ... ] [ ... ] * [ ... ] [ ... ]
* [ 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) { if (externalRow < -mActiveTranscriptRows || externalRow > mScreenRows)
if (externalRow < -mActiveTranscriptRows || externalRow > mScreenRows) throw new IllegalArgumentException("extRow=" + externalRow + ", mScreenRows=" + mScreenRows + ", mActiveTranscriptRows=" + mActiveTranscriptRows);
throw new IllegalArgumentException("extRow=" + externalRow + ", mScreenRows=" + mScreenRows + ", mActiveTranscriptRows=" + mActiveTranscriptRows); final int internalRow = mScreenFirstRow + externalRow;
final int internalRow = mScreenFirstRow + externalRow; return (internalRow < 0) ? (mTotalRows + internalRow) : (internalRow % mTotalRows);
return (internalRow < 0) ? (mTotalRows + internalRow) : (internalRow % mTotalRows); }
}
public void setLineWrap(int row) { public void setLineWrap(int row) {
mLines[externalToInternalRow(row)].mLineWrap = true; mLines[externalToInternalRow(row)].mLineWrap = true;
} }
public boolean getLineWrap(int row) { public boolean getLineWrap(int row) {
return mLines[externalToInternalRow(row)].mLineWrap; return mLines[externalToInternalRow(row)].mLineWrap;
} }
public void clearLineWrap(int row) { public void clearLineWrap(int row) {
mLines[externalToInternalRow(row)].mLineWrap = false; 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 public void resize(int newColumns, int newRows, int newTotalRows, int[] cursor, long currentStyle, boolean altScreen) {
* An int[2] containing the (column, row) cursor location. // newRows > mTotalRows should not normally happen since mTotalRows is TRANSCRIPT_ROWS (10000):
*/ if (newColumns == mColumns && newRows <= mTotalRows) {
public void resize(int newColumns, int newRows, int newTotalRows, int[] cursor, int currentStyle, boolean altScreen) { // Fast resize where just the rows changed.
// newRows > mTotalRows should not normally happen since mTotalRows is TRANSCRIPT_ROWS (10000): int shiftDownOfTopRow = mScreenRows - newRows;
if (newColumns == mColumns && newRows <= mTotalRows) { if (shiftDownOfTopRow > 0 && shiftDownOfTopRow < mScreenRows) {
// Fast resize where just the rows changed. // Shrinking. Check if we can skip blank rows at bottom below cursor.
int shiftDownOfTopRow = mScreenRows - newRows; for (int i = mScreenRows - 1; i > 0; i--) {
if (shiftDownOfTopRow > 0 && shiftDownOfTopRow < mScreenRows) { if (cursor[1] >= i) break;
// Shrinking. Check if we can skip blank rows at bottom below cursor. int r = externalToInternalRow(i);
for (int i = mScreenRows - 1; i > 0; i--) { if (mLines[r] == null || mLines[r].isBlank()) {
if (cursor[1] >= i) break; if (--shiftDownOfTopRow == 0) break;
int r = externalToInternalRow(i); }
if (mLines[r] == null || mLines[r].isBlank()) { }
if (--shiftDownOfTopRow == 0) break; } else if (shiftDownOfTopRow < 0) {
} // Negative shift down = expanding. Only move screen up if there is transcript to show:
} int actualShift = Math.max(shiftDownOfTopRow, -mActiveTranscriptRows);
} else if (shiftDownOfTopRow < 0) { if (shiftDownOfTopRow != actualShift) {
// Negative shift down = expanding. Only move screen up if there is transcript to show: // The new lines revealed by the resizing are not all from the transcript. Blank the below ones.
int actualShift = Math.max(shiftDownOfTopRow, -mActiveTranscriptRows); for (int i = 0; i < actualShift - shiftDownOfTopRow; i++)
if (shiftDownOfTopRow != actualShift) { allocateFullLineIfNecessary((mScreenFirstRow + mScreenRows + i) % mTotalRows).clear(currentStyle);
// The new lines revealed by the resizing are not all from the transcript. Blank the below ones. shiftDownOfTopRow = actualShift;
for (int i = 0; i < actualShift - shiftDownOfTopRow; i++) }
allocateFullLineIfNecessary((mScreenFirstRow + mScreenRows + i) % mTotalRows).clear(currentStyle); }
shiftDownOfTopRow = actualShift; mScreenFirstRow += shiftDownOfTopRow;
} mScreenFirstRow = (mScreenFirstRow < 0) ? (mScreenFirstRow + mTotalRows) : (mScreenFirstRow % mTotalRows);
} mTotalRows = newTotalRows;
mScreenFirstRow += shiftDownOfTopRow; mActiveTranscriptRows = altScreen ? 0 : Math.max(0, mActiveTranscriptRows + shiftDownOfTopRow);
mScreenFirstRow = (mScreenFirstRow < 0) ? (mScreenFirstRow + mTotalRows) : (mScreenFirstRow % mTotalRows); cursor[1] -= shiftDownOfTopRow;
mTotalRows = newTotalRows; mScreenRows = newRows;
mActiveTranscriptRows = altScreen ? 0 : Math.max(0, mActiveTranscriptRows + shiftDownOfTopRow); } else {
cursor[1] -= shiftDownOfTopRow; // Copy away old state and update new:
mScreenRows = newRows; TerminalRow[] oldLines = mLines;
} else { mLines = new TerminalRow[newTotalRows];
// Copy away old state and update new: for (int i = 0; i < newTotalRows; i++)
TerminalRow[] oldLines = mLines; mLines[i] = new TerminalRow(newColumns, currentStyle);
mLines = new TerminalRow[newTotalRows];
for (int i = 0; i < newTotalRows; i++)
mLines[i] = new TerminalRow(newColumns, currentStyle);
final int oldActiveTranscriptRows = mActiveTranscriptRows; final int oldActiveTranscriptRows = mActiveTranscriptRows;
final int oldScreenFirstRow = mScreenFirstRow; final int oldScreenFirstRow = mScreenFirstRow;
final int oldScreenRows = mScreenRows; final int oldScreenRows = mScreenRows;
final int oldTotalRows = mTotalRows; final int oldTotalRows = mTotalRows;
mTotalRows = newTotalRows; mTotalRows = newTotalRows;
mScreenRows = newRows; mScreenRows = newRows;
mActiveTranscriptRows = mScreenFirstRow = 0; mActiveTranscriptRows = mScreenFirstRow = 0;
mColumns = newColumns; mColumns = newColumns;
int newCursorRow = -1; int newCursorRow = -1;
int newCursorColumn = -1; int newCursorColumn = -1;
int oldCursorRow = cursor[1]; int oldCursorRow = cursor[1];
int oldCursorColumn = cursor[0]; int oldCursorColumn = cursor[0];
boolean newCursorPlaced = false; boolean newCursorPlaced = false;
int currentOutputExternalRow = 0; int currentOutputExternalRow = 0;
int currentOutputExternalColumn = 0; int currentOutputExternalColumn = 0;
// Loop over every character in the initial state. // Loop over every character in the initial state.
// Blank lines should be skipped only if at end of transcript (just as is done in the "fast" resize), so we // Blank lines should be skipped only if at end of transcript (just as is done in the "fast" resize), so we
// keep track how many blank lines we have skipped if we later on find a non-blank line. // keep track how many blank lines we have skipped if we later on find a non-blank line.
int skippedBlankLines = 0; int skippedBlankLines = 0;
for (int externalOldRow = -oldActiveTranscriptRows; externalOldRow < oldScreenRows; externalOldRow++) { for (int externalOldRow = -oldActiveTranscriptRows; externalOldRow < oldScreenRows; externalOldRow++) {
// Do what externalToInternalRow() does but for the old state: // Do what externalToInternalRow() does but for the old state:
int internalOldRow = oldScreenFirstRow + externalOldRow; int internalOldRow = oldScreenFirstRow + externalOldRow;
internalOldRow = (internalOldRow < 0) ? (oldTotalRows + internalOldRow) : (internalOldRow % oldTotalRows); internalOldRow = (internalOldRow < 0) ? (oldTotalRows + internalOldRow) : (internalOldRow % oldTotalRows);
TerminalRow oldLine = oldLines[internalOldRow]; TerminalRow oldLine = oldLines[internalOldRow];
boolean cursorAtThisRow = externalOldRow == oldCursorRow; boolean cursorAtThisRow = externalOldRow == oldCursorRow;
// The cursor may only be on a non-null line, which we should not skip: // The cursor may only be on a non-null line, which we should not skip:
if (oldLine == null || (!(!newCursorPlaced && cursorAtThisRow)) && oldLine.isBlank()) { if (oldLine == null || (!(!newCursorPlaced && cursorAtThisRow)) && oldLine.isBlank()) {
skippedBlankLines++; skippedBlankLines++;
continue; continue;
} else if (skippedBlankLines > 0) { } else if (skippedBlankLines > 0) {
// After skipping some blank lines we encounter a non-blank line. Insert the skipped blank lines. // After skipping some blank lines we encounter a non-blank line. Insert the skipped blank lines.
for (int i = 0; i < skippedBlankLines; i++) { for (int i = 0; i < skippedBlankLines; i++) {
if (currentOutputExternalRow == mScreenRows - 1) { if (currentOutputExternalRow == mScreenRows - 1) {
scrollDownOneLine(0, mScreenRows, currentStyle); scrollDownOneLine(0, mScreenRows, currentStyle);
} else { } else {
currentOutputExternalRow++; currentOutputExternalRow++;
} }
currentOutputExternalColumn = 0; currentOutputExternalColumn = 0;
} }
skippedBlankLines = 0; skippedBlankLines = 0;
} }
int lastNonSpaceIndex = 0; int lastNonSpaceIndex = 0;
boolean justToCursor = false; boolean justToCursor = false;
if (cursorAtThisRow || oldLine.mLineWrap) { if (cursorAtThisRow || oldLine.mLineWrap) {
// Take the whole line, either because of cursor on it, or if line wrapping. // Take the whole line, either because of cursor on it, or if line wrapping.
lastNonSpaceIndex = oldLine.getSpaceUsed(); lastNonSpaceIndex = oldLine.getSpaceUsed();
if (cursorAtThisRow) justToCursor = true; if (cursorAtThisRow) justToCursor = true;
} else { } else {
for (int i = 0; i < oldLine.getSpaceUsed(); i++) for (int i = 0; i < oldLine.getSpaceUsed(); i++)
// NEWLY INTRODUCED BUG! Should not index oldLine.mStyle with char indices // NEWLY INTRODUCED BUG! Should not index oldLine.mStyle with char indices
if (oldLine.mText[i] != ' '/* || oldLine.mStyle[i] != currentStyle */) lastNonSpaceIndex = i + 1; if (oldLine.mText[i] != ' '/* || oldLine.mStyle[i] != currentStyle */)
} lastNonSpaceIndex = i + 1;
}
int currentOldCol = 0; int currentOldCol = 0;
int styleAtCol = 0; long styleAtCol = 0;
for (int i = 0; i < lastNonSpaceIndex; i++) { for (int i = 0; i < lastNonSpaceIndex; i++) {
// Note that looping over java character, not cells. // Note that looping over java character, not cells.
char c = oldLine.mText[i]; char c = oldLine.mText[i];
int codePoint = (Character.isHighSurrogate(c)) ? Character.toCodePoint(c, oldLine.mText[++i]) : c; int codePoint = (Character.isHighSurrogate(c)) ? Character.toCodePoint(c, oldLine.mText[++i]) : c;
int displayWidth = WcWidth.width(codePoint); int displayWidth = WcWidth.width(codePoint);
// Use the last style if this is a zero-width character: // Use the last style if this is a zero-width character:
if (displayWidth > 0) styleAtCol = oldLine.getStyle(currentOldCol); if (displayWidth > 0) styleAtCol = oldLine.getStyle(currentOldCol);
// Line wrap as necessary: // Line wrap as necessary:
if (currentOutputExternalColumn + displayWidth > mColumns) { if (currentOutputExternalColumn + displayWidth > mColumns) {
setLineWrap(currentOutputExternalRow); setLineWrap(currentOutputExternalRow);
if (currentOutputExternalRow == mScreenRows - 1) { if (currentOutputExternalRow == mScreenRows - 1) {
if (newCursorPlaced) newCursorRow--; if (newCursorPlaced) newCursorRow--;
scrollDownOneLine(0, mScreenRows, currentStyle); scrollDownOneLine(0, mScreenRows, currentStyle);
} else { } else {
currentOutputExternalRow++; currentOutputExternalRow++;
} }
currentOutputExternalColumn = 0; currentOutputExternalColumn = 0;
} }
int offsetDueToCombiningChar = ((displayWidth <= 0 && currentOutputExternalColumn > 0) ? 1 : 0); int offsetDueToCombiningChar = ((displayWidth <= 0 && currentOutputExternalColumn > 0) ? 1 : 0);
int outputColumn = currentOutputExternalColumn - offsetDueToCombiningChar; int outputColumn = currentOutputExternalColumn - offsetDueToCombiningChar;
setChar(outputColumn, currentOutputExternalRow, codePoint, styleAtCol); setChar(outputColumn, currentOutputExternalRow, codePoint, styleAtCol);
if (displayWidth > 0) { if (displayWidth > 0) {
if (oldCursorRow == externalOldRow && oldCursorColumn == currentOldCol) { if (oldCursorRow == externalOldRow && oldCursorColumn == currentOldCol) {
newCursorColumn = currentOutputExternalColumn; newCursorColumn = currentOutputExternalColumn;
newCursorRow = currentOutputExternalRow; newCursorRow = currentOutputExternalRow;
newCursorPlaced = true; newCursorPlaced = true;
} }
currentOldCol += displayWidth; currentOldCol += displayWidth;
currentOutputExternalColumn += displayWidth; currentOutputExternalColumn += displayWidth;
if (justToCursor && newCursorPlaced) break; if (justToCursor && newCursorPlaced) break;
} }
} }
// Old row has been copied. Check if we need to insert newline if old line was not wrapping: // Old row has been copied. Check if we need to insert newline if old line was not wrapping:
if (externalOldRow != (oldScreenRows - 1) && !oldLine.mLineWrap) { if (externalOldRow != (oldScreenRows - 1) && !oldLine.mLineWrap) {
if (currentOutputExternalRow == mScreenRows - 1) { if (currentOutputExternalRow == mScreenRows - 1) {
if (newCursorPlaced) newCursorRow--; if (newCursorPlaced) newCursorRow--;
scrollDownOneLine(0, mScreenRows, currentStyle); scrollDownOneLine(0, mScreenRows, currentStyle);
} else { } else {
currentOutputExternalRow++; currentOutputExternalRow++;
} }
currentOutputExternalColumn = 0; currentOutputExternalColumn = 0;
} }
} }
cursor[0] = newCursorColumn; cursor[0] = newCursorColumn;
cursor[1] = newCursorRow; cursor[1] = newCursorRow;
} }
// Handle cursor scrolling off screen: // Handle cursor scrolling off screen:
if (cursor[0] < 0 || cursor[1] < 0) cursor[0] = cursor[1] = 0; if (cursor[0] < 0 || cursor[1] < 0) cursor[0] = cursor[1] = 0;
} }
/** /**
* 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) {
*/ if (len == 0) return;
private void blockCopyLinesDown(int srcInternal, int len) { int totalRows = mTotalRows;
if (len == 0) return;
int totalRows = mTotalRows;
int start = len - 1; int start = len - 1;
// Save away line to be overwritten: // Save away line to be overwritten:
TerminalRow lineToBeOverWritten = mLines[(srcInternal + start + 1) % totalRows]; TerminalRow lineToBeOverWritten = mLines[(srcInternal + start + 1) % totalRows];
// Do the copy from bottom to top. // Do the copy from bottom to top.
for (int i = start; i >= 0; --i) for (int i = start; i >= 0; --i)
mLines[(srcInternal + i + 1) % totalRows] = mLines[(srcInternal + i) % totalRows]; mLines[(srcInternal + i + 1) % totalRows] = mLines[(srcInternal + i) % totalRows];
// Put back overwritten line, now above the block: // Put back overwritten line, now above the block:
mLines[(srcInternal) % totalRows] = lineToBeOverWritten; mLines[(srcInternal) % totalRows] = lineToBeOverWritten;
} }
/** /**
* 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 public void scrollDownOneLine(int topMargin, int bottomMargin, long style) {
* the style for the newly exposed line. if (topMargin > bottomMargin - 1 || topMargin < 0 || bottomMargin > mScreenRows)
*/ throw new IllegalArgumentException("topMargin=" + topMargin + ", bottomMargin=" + bottomMargin + ", mScreenRows=" + mScreenRows);
public void scrollDownOneLine(int topMargin, int bottomMargin, int style) {
if (topMargin > bottomMargin - 1 || topMargin < 0 || bottomMargin > mScreenRows)
throw new IllegalArgumentException("topMargin=" + topMargin + ", bottomMargin=" + bottomMargin + ", mScreenRows=" + mScreenRows);
// Copy the fixed topMargin lines one line down so that they remain on screen in same position: // Copy the fixed topMargin lines one line down so that they remain on screen in same position:
blockCopyLinesDown(mScreenFirstRow, topMargin); blockCopyLinesDown(mScreenFirstRow, topMargin);
// Copy the fixed mScreenRows-bottomMargin lines one line down so that they remain on screen in same // Copy the fixed mScreenRows-bottomMargin lines one line down so that they remain on screen in same
// position: // position:
blockCopyLinesDown(externalToInternalRow(bottomMargin), mScreenRows - bottomMargin); blockCopyLinesDown(externalToInternalRow(bottomMargin), mScreenRows - bottomMargin);
// Update the screen location in the ring buffer: // Update the screen location in the ring buffer:
mScreenFirstRow = (mScreenFirstRow + 1) % mTotalRows; mScreenFirstRow = (mScreenFirstRow + 1) % mTotalRows;
// Note that the history has grown if not already full: // Note that the history has grown if not already full:
if (mActiveTranscriptRows < mTotalRows - mScreenRows) mActiveTranscriptRows++; if (mActiveTranscriptRows < mTotalRows - mScreenRows) mActiveTranscriptRows++;
// Blank the newly revealed line above the bottom margin: // Blank the newly revealed line above the bottom margin:
int blankRow = externalToInternalRow(bottomMargin - 1); int blankRow = externalToInternalRow(bottomMargin - 1);
if (mLines[blankRow] == null) { if (mLines[blankRow] == null) {
mLines[blankRow] = new TerminalRow(mColumns, style); mLines[blankRow] = new TerminalRow(mColumns, style);
} else { } else {
mLines[blankRow].clear(style); mLines[blankRow].clear(style);
} }
} }
/** /**
* Block copy characters from one position in the screen to another. The two positions can overlap. All characters * Block copy characters from one position in the screen to another. The two positions can overlap. All characters
* 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 public void blockCopy(int sx, int sy, int w, int h, int dx, int dy) {
* @param dx if (w == 0) return;
* destination X coordinate if (sx < 0 || sx + w > mColumns || sy < 0 || sy + h > mScreenRows || dx < 0 || dx + w > mColumns || dy < 0 || dy + h > mScreenRows)
* @param dy throw new IllegalArgumentException();
* destination Y coordinate boolean copyingUp = sy > dy;
*/ for (int y = 0; y < h; y++) {
public void blockCopy(int sx, int sy, int w, int h, int dx, int dy) { int y2 = copyingUp ? y : (h - (y + 1));
if (w == 0) return; TerminalRow sourceRow = allocateFullLineIfNecessary(externalToInternalRow(sy + y2));
if (sx < 0 || sx + w > mColumns || sy < 0 || sy + h > mScreenRows || dx < 0 || dx + w > mColumns || dy < 0 || dy + h > mScreenRows) allocateFullLineIfNecessary(externalToInternalRow(dy + y2)).copyInterval(sourceRow, sx, sx + w, dx);
throw new IllegalArgumentException(); }
boolean copyingUp = sy > dy; }
for (int y = 0; y < h; y++) {
int y2 = copyingUp ? y : (h - (y + 1));
TerminalRow sourceRow = allocateFullLineIfNecessary(externalToInternalRow(sy + y2));
allocateFullLineIfNecessary(externalToInternalRow(dy + y2)).copyInterval(sourceRow, sx, sx + w, dx);
}
}
/** /**
* Block set characters. All characters must be within the bounds of the screen, or else and * Block set characters. All characters must be within the bounds of the screen, or else and
* InvalidParemeterException will be thrown. Typically this is called with a "val" argument of 32 to clear a block * InvalidParemeterException will be thrown. Typically this is called with a "val" argument of 32 to clear a block
* of characters. * of characters.
*/ */
public void blockSet(int sx, int sy, int w, int h, int val, int style) { public void blockSet(int sx, int sy, int w, int h, int val, long style) {
if (sx < 0 || sx + w > mColumns || sy < 0 || sy + h > mScreenRows) { if (sx < 0 || sx + w > mColumns || sy < 0 || sy + h > mScreenRows) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
"Illegal arguments! blockSet(" + sx + ", " + sy + ", " + w + ", " + h + ", " + val + ", " + mColumns + ", " + mScreenRows + ")"); "Illegal arguments! blockSet(" + sx + ", " + sy + ", " + w + ", " + h + ", " + val + ", " + mColumns + ", " + mScreenRows + ")");
} }
for (int y = 0; y < h; y++) for (int y = 0; y < h; y++)
for (int x = 0; x < w; x++) for (int x = 0; x < w; x++)
setChar(sx + x, sy + y, val, style); setChar(sx + x, sy + y, val, style);
} }
public TerminalRow allocateFullLineIfNecessary(int row) { public TerminalRow allocateFullLineIfNecessary(int row) {
return (mLines[row] == null) ? (mLines[row] = new TerminalRow(mColumns, 0)) : mLines[row]; return (mLines[row] == null) ? (mLines[row] = new TerminalRow(mColumns, 0)) : mLines[row];
} }
public void setChar(int column, int row, int codePoint, int style) { public void setChar(int column, int row, int codePoint, long style) {
if (row >= mScreenRows || column >= mColumns) if (row >= mScreenRows || column >= mColumns)
throw new IllegalArgumentException("row=" + row + ", column=" + column + ", mScreenRows=" + mScreenRows + ", mColumns=" + mColumns); throw new IllegalArgumentException("row=" + row + ", column=" + column + ", mScreenRows=" + mScreenRows + ", mColumns=" + mColumns);
row = externalToInternalRow(row); row = externalToInternalRow(row);
allocateFullLineIfNecessary(row).setChar(column, codePoint, style); allocateFullLineIfNecessary(row).setChar(column, codePoint, style);
} }
public int getStyleAt(int externalRow, int column) { public long getStyleAt(int externalRow, int column) {
return allocateFullLineIfNecessary(externalToInternalRow(externalRow)).getStyle(column); return allocateFullLineIfNecessary(externalToInternalRow(externalRow)).getStyle(column);
} }
/** Support for http://vt100.net/docs/vt510-rm/DECCARA and http://vt100.net/docs/vt510-rm/DECCARA */ /** Support for http://vt100.net/docs/vt510-rm/DECCARA and http://vt100.net/docs/vt510-rm/DECCARA */
public void setOrClearEffect(int bits, boolean setOrClear, boolean reverse, boolean rectangular, int leftMargin, int rightMargin, int top, int left, public void setOrClearEffect(int bits, boolean setOrClear, boolean reverse, boolean rectangular, int leftMargin, int rightMargin, int top, int left,
int bottom, int right) { int bottom, int right) {
for (int y = top; y < bottom; y++) { for (int y = top; y < bottom; y++) {
TerminalRow line = mLines[externalToInternalRow(y)]; TerminalRow line = mLines[externalToInternalRow(y)];
int startOfLine = (rectangular || y == top) ? left : leftMargin; int startOfLine = (rectangular || y == top) ? left : leftMargin;
int endOfLine = (rectangular || y + 1 == bottom) ? right : rightMargin; int endOfLine = (rectangular || y + 1 == bottom) ? right : rightMargin;
for (int x = startOfLine; x < endOfLine; x++) { for (int x = startOfLine; x < endOfLine; x++) {
int currentStyle = line.getStyle(x); long currentStyle = line.getStyle(x);
int foreColor = TextStyle.decodeForeColor(currentStyle); int foreColor = TextStyle.decodeForeColor(currentStyle);
int backColor = TextStyle.decodeBackColor(currentStyle); int backColor = TextStyle.decodeBackColor(currentStyle);
int effect = TextStyle.decodeEffect(currentStyle); int effect = TextStyle.decodeEffect(currentStyle);
if (reverse) { if (reverse) {
// Clear out the bits to reverse and add them back in reversed: // Clear out the bits to reverse and add them back in reversed:
effect = (effect & ~bits) | (bits & ~effect); effect = (effect & ~bits) | (bits & ~effect);
} else if (setOrClear) { } else if (setOrClear) {
effect |= bits; effect |= bits;
} else { } else {
effect &= ~bits; effect &= ~bits;
} }
line.mStyle[x] = TextStyle.encode(foreColor, backColor, effect); line.mStyle[x] = TextStyle.encode(foreColor, backColor, effect);
} }
} }
} }
} }

View File

@@ -6,97 +6,98 @@ import java.util.Properties;
/** /**
* Color scheme for a terminal with default colors, which may be overridden (and then reset) from the shell using * Color scheme for a terminal with default colors, which may be overridden (and then reset) from the shell using
* Operating System Control (OSC) sequences. * Operating System Control (OSC) sequences.
* *
* @see TerminalColors * @see TerminalColors
*/ */
public final class TerminalColorScheme { public final class TerminalColorScheme {
/** http://upload.wikimedia.org/wikipedia/en/1/15/Xterm_256color_chart.svg, but with blue color brighter. */ /** http://upload.wikimedia.org/wikipedia/en/1/15/Xterm_256color_chart.svg, but with blue color brighter. */
private static final int[] DEFAULT_COLORSCHEME = { private static final int[] DEFAULT_COLORSCHEME = {
// 16 original colors. First 8 are dim. // 16 original colors. First 8 are dim.
0xff000000, // black 0xff000000, // black
0xffcd0000, // dim red 0xffcd0000, // dim red
0xff00cd00, // dim green 0xff00cd00, // dim green
0xffcdcd00, // dim yellow 0xffcdcd00, // dim yellow
0xff6495ed, // dim blue 0xff6495ed, // dim blue
0xffcd00cd, // dim magenta 0xffcd00cd, // dim magenta
0xff00cdcd, // dim cyan 0xff00cdcd, // dim cyan
0xffe5e5e5, // dim white 0xffe5e5e5, // dim white
// Second 8 are bright: // Second 8 are bright:
0xff7f7f7f, // medium grey 0xff7f7f7f, // medium grey
0xffff0000, // bright red 0xffff0000, // bright red
0xff00ff00, // bright green 0xff00ff00, // bright green
0xffffff00, // bright yellow 0xffffff00, // bright yellow
0xff5c5cff, // light blue 0xff5c5cff, // light blue
0xffff00ff, // bright magenta 0xffff00ff, // bright magenta
0xff00ffff, // bright cyan 0xff00ffff, // bright cyan
0xffffffff, // bright white 0xffffffff, // bright white
// 216 color cube, six shades of each color: // 216 color cube, six shades of each color:
0xff000000, 0xff00005f, 0xff000087, 0xff0000af, 0xff0000d7, 0xff0000ff, 0xff005f00, 0xff005f5f, 0xff005f87, 0xff005faf, 0xff005fd7, 0xff005fff, 0xff000000, 0xff00005f, 0xff000087, 0xff0000af, 0xff0000d7, 0xff0000ff, 0xff005f00, 0xff005f5f, 0xff005f87, 0xff005faf, 0xff005fd7, 0xff005fff,
0xff008700, 0xff00875f, 0xff008787, 0xff0087af, 0xff0087d7, 0xff0087ff, 0xff00af00, 0xff00af5f, 0xff00af87, 0xff00afaf, 0xff00afd7, 0xff00afff, 0xff008700, 0xff00875f, 0xff008787, 0xff0087af, 0xff0087d7, 0xff0087ff, 0xff00af00, 0xff00af5f, 0xff00af87, 0xff00afaf, 0xff00afd7, 0xff00afff,
0xff00d700, 0xff00d75f, 0xff00d787, 0xff00d7af, 0xff00d7d7, 0xff00d7ff, 0xff00ff00, 0xff00ff5f, 0xff00ff87, 0xff00ffaf, 0xff00ffd7, 0xff00ffff, 0xff00d700, 0xff00d75f, 0xff00d787, 0xff00d7af, 0xff00d7d7, 0xff00d7ff, 0xff00ff00, 0xff00ff5f, 0xff00ff87, 0xff00ffaf, 0xff00ffd7, 0xff00ffff,
0xff5f0000, 0xff5f005f, 0xff5f0087, 0xff5f00af, 0xff5f00d7, 0xff5f00ff, 0xff5f5f00, 0xff5f5f5f, 0xff5f5f87, 0xff5f5faf, 0xff5f5fd7, 0xff5f5fff, 0xff5f0000, 0xff5f005f, 0xff5f0087, 0xff5f00af, 0xff5f00d7, 0xff5f00ff, 0xff5f5f00, 0xff5f5f5f, 0xff5f5f87, 0xff5f5faf, 0xff5f5fd7, 0xff5f5fff,
0xff5f8700, 0xff5f875f, 0xff5f8787, 0xff5f87af, 0xff5f87d7, 0xff5f87ff, 0xff5faf00, 0xff5faf5f, 0xff5faf87, 0xff5fafaf, 0xff5fafd7, 0xff5fafff, 0xff5f8700, 0xff5f875f, 0xff5f8787, 0xff5f87af, 0xff5f87d7, 0xff5f87ff, 0xff5faf00, 0xff5faf5f, 0xff5faf87, 0xff5fafaf, 0xff5fafd7, 0xff5fafff,
0xff5fd700, 0xff5fd75f, 0xff5fd787, 0xff5fd7af, 0xff5fd7d7, 0xff5fd7ff, 0xff5fff00, 0xff5fff5f, 0xff5fff87, 0xff5fffaf, 0xff5fffd7, 0xff5fffff, 0xff5fd700, 0xff5fd75f, 0xff5fd787, 0xff5fd7af, 0xff5fd7d7, 0xff5fd7ff, 0xff5fff00, 0xff5fff5f, 0xff5fff87, 0xff5fffaf, 0xff5fffd7, 0xff5fffff,
0xff870000, 0xff87005f, 0xff870087, 0xff8700af, 0xff8700d7, 0xff8700ff, 0xff875f00, 0xff875f5f, 0xff875f87, 0xff875faf, 0xff875fd7, 0xff875fff, 0xff870000, 0xff87005f, 0xff870087, 0xff8700af, 0xff8700d7, 0xff8700ff, 0xff875f00, 0xff875f5f, 0xff875f87, 0xff875faf, 0xff875fd7, 0xff875fff,
0xff878700, 0xff87875f, 0xff878787, 0xff8787af, 0xff8787d7, 0xff8787ff, 0xff87af00, 0xff87af5f, 0xff87af87, 0xff87afaf, 0xff87afd7, 0xff87afff, 0xff878700, 0xff87875f, 0xff878787, 0xff8787af, 0xff8787d7, 0xff8787ff, 0xff87af00, 0xff87af5f, 0xff87af87, 0xff87afaf, 0xff87afd7, 0xff87afff,
0xff87d700, 0xff87d75f, 0xff87d787, 0xff87d7af, 0xff87d7d7, 0xff87d7ff, 0xff87ff00, 0xff87ff5f, 0xff87ff87, 0xff87ffaf, 0xff87ffd7, 0xff87ffff, 0xff87d700, 0xff87d75f, 0xff87d787, 0xff87d7af, 0xff87d7d7, 0xff87d7ff, 0xff87ff00, 0xff87ff5f, 0xff87ff87, 0xff87ffaf, 0xff87ffd7, 0xff87ffff,
0xffaf0000, 0xffaf005f, 0xffaf0087, 0xffaf00af, 0xffaf00d7, 0xffaf00ff, 0xffaf5f00, 0xffaf5f5f, 0xffaf5f87, 0xffaf5faf, 0xffaf5fd7, 0xffaf5fff, 0xffaf0000, 0xffaf005f, 0xffaf0087, 0xffaf00af, 0xffaf00d7, 0xffaf00ff, 0xffaf5f00, 0xffaf5f5f, 0xffaf5f87, 0xffaf5faf, 0xffaf5fd7, 0xffaf5fff,
0xffaf8700, 0xffaf875f, 0xffaf8787, 0xffaf87af, 0xffaf87d7, 0xffaf87ff, 0xffafaf00, 0xffafaf5f, 0xffafaf87, 0xffafafaf, 0xffafafd7, 0xffafafff, 0xffaf8700, 0xffaf875f, 0xffaf8787, 0xffaf87af, 0xffaf87d7, 0xffaf87ff, 0xffafaf00, 0xffafaf5f, 0xffafaf87, 0xffafafaf, 0xffafafd7, 0xffafafff,
0xffafd700, 0xffafd75f, 0xffafd787, 0xffafd7af, 0xffafd7d7, 0xffafd7ff, 0xffafff00, 0xffafff5f, 0xffafff87, 0xffafffaf, 0xffafffd7, 0xffafffff, 0xffafd700, 0xffafd75f, 0xffafd787, 0xffafd7af, 0xffafd7d7, 0xffafd7ff, 0xffafff00, 0xffafff5f, 0xffafff87, 0xffafffaf, 0xffafffd7, 0xffafffff,
0xffd70000, 0xffd7005f, 0xffd70087, 0xffd700af, 0xffd700d7, 0xffd700ff, 0xffd75f00, 0xffd75f5f, 0xffd75f87, 0xffd75faf, 0xffd75fd7, 0xffd75fff, 0xffd70000, 0xffd7005f, 0xffd70087, 0xffd700af, 0xffd700d7, 0xffd700ff, 0xffd75f00, 0xffd75f5f, 0xffd75f87, 0xffd75faf, 0xffd75fd7, 0xffd75fff,
0xffd78700, 0xffd7875f, 0xffd78787, 0xffd787af, 0xffd787d7, 0xffd787ff, 0xffd7af00, 0xffd7af5f, 0xffd7af87, 0xffd7afaf, 0xffd7afd7, 0xffd7afff, 0xffd78700, 0xffd7875f, 0xffd78787, 0xffd787af, 0xffd787d7, 0xffd787ff, 0xffd7af00, 0xffd7af5f, 0xffd7af87, 0xffd7afaf, 0xffd7afd7, 0xffd7afff,
0xffd7d700, 0xffd7d75f, 0xffd7d787, 0xffd7d7af, 0xffd7d7d7, 0xffd7d7ff, 0xffd7ff00, 0xffd7ff5f, 0xffd7ff87, 0xffd7ffaf, 0xffd7ffd7, 0xffd7ffff, 0xffd7d700, 0xffd7d75f, 0xffd7d787, 0xffd7d7af, 0xffd7d7d7, 0xffd7d7ff, 0xffd7ff00, 0xffd7ff5f, 0xffd7ff87, 0xffd7ffaf, 0xffd7ffd7, 0xffd7ffff,
0xffff0000, 0xffff005f, 0xffff0087, 0xffff00af, 0xffff00d7, 0xffff00ff, 0xffff5f00, 0xffff5f5f, 0xffff5f87, 0xffff5faf, 0xffff5fd7, 0xffff5fff, 0xffff0000, 0xffff005f, 0xffff0087, 0xffff00af, 0xffff00d7, 0xffff00ff, 0xffff5f00, 0xffff5f5f, 0xffff5f87, 0xffff5faf, 0xffff5fd7, 0xffff5fff,
0xffff8700, 0xffff875f, 0xffff8787, 0xffff87af, 0xffff87d7, 0xffff87ff, 0xffffaf00, 0xffffaf5f, 0xffffaf87, 0xffffafaf, 0xffffafd7, 0xffffafff, 0xffff8700, 0xffff875f, 0xffff8787, 0xffff87af, 0xffff87d7, 0xffff87ff, 0xffffaf00, 0xffffaf5f, 0xffffaf87, 0xffffafaf, 0xffffafd7, 0xffffafff,
0xffffd700, 0xffffd75f, 0xffffd787, 0xffffd7af, 0xffffd7d7, 0xffffd7ff, 0xffffff00, 0xffffff5f, 0xffffff87, 0xffffffaf, 0xffffffd7, 0xffffffff, 0xffffd700, 0xffffd75f, 0xffffd787, 0xffffd7af, 0xffffd7d7, 0xffffd7ff, 0xffffff00, 0xffffff5f, 0xffffff87, 0xffffffaf, 0xffffffd7, 0xffffffff,
// 24 grey scale ramp: // 24 grey scale ramp:
0xff080808, 0xff121212, 0xff1c1c1c, 0xff262626, 0xff303030, 0xff3a3a3a, 0xff444444, 0xff4e4e4e, 0xff585858, 0xff626262, 0xff6c6c6c, 0xff767676, 0xff080808, 0xff121212, 0xff1c1c1c, 0xff262626, 0xff303030, 0xff3a3a3a, 0xff444444, 0xff4e4e4e, 0xff585858, 0xff626262, 0xff6c6c6c, 0xff767676,
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];
public TerminalColorScheme() { public 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);
} }
public void updateWith(Properties props) { public void updateWith(Properties props) {
reset(); reset();
for (Map.Entry<Object, Object> entries : props.entrySet()) { for (Map.Entry<Object, Object> entries : props.entrySet()) {
String key = (String) entries.getKey(); String key = (String) entries.getKey();
String value = (String) entries.getValue(); String value = (String) entries.getValue();
int colorIndex; int colorIndex;
if (key.equals("foreground")) { if (key.equals("foreground")) {
colorIndex = TextStyle.COLOR_INDEX_FOREGROUND; colorIndex = TextStyle.COLOR_INDEX_FOREGROUND;
} else if (key.equals("background")) { } else if (key.equals("background")) {
colorIndex = TextStyle.COLOR_INDEX_BACKGROUND; colorIndex = TextStyle.COLOR_INDEX_BACKGROUND;
} else if (key.equals("cursor")) { } else if (key.equals("cursor")) {
colorIndex = TextStyle.COLOR_INDEX_CURSOR; colorIndex = TextStyle.COLOR_INDEX_CURSOR;
} else if (key.startsWith("color")) { } else if (key.startsWith("color")) {
try { try {
colorIndex = Integer.parseInt(key.substring(5)); colorIndex = Integer.parseInt(key.substring(5));
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid property: '" + key + "'"); throw new IllegalArgumentException("Invalid property: '" + key + "'");
} }
} else { } else {
throw new IllegalArgumentException("Invalid property: '" + key + "'"); throw new IllegalArgumentException("Invalid property: '" + key + "'");
} }
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

@@ -3,74 +3,74 @@ package com.termux.terminal;
/** Current terminal colors (if different from default). */ /** Current terminal colors (if different from default). */
public final class TerminalColors { public final class TerminalColors {
/** Static data - a bit ugly but ok for now. */ /** Static data - a bit ugly but ok for now. */
public static final TerminalColorScheme COLOR_SCHEME = new TerminalColorScheme(); public static final TerminalColorScheme COLOR_SCHEME = new TerminalColorScheme();
/** /**
* The current terminal colors, which are normally set from the color theme, but may be set dynamically with the OSC * The current terminal colors, which are normally set from the color theme, but may be set dynamically with the OSC
* 4 control sequence. * 4 control sequence.
*/ */
public final int[] mCurrentColors = new int[TextStyle.NUM_INDEXED_COLORS]; public final int[] mCurrentColors = new int[TextStyle.NUM_INDEXED_COLORS];
/** Create a new instance with default colors from the theme. */ /** Create a new instance with default colors from the theme. */
public TerminalColors() { public TerminalColors() {
reset(); reset();
} }
/** Reset a particular indexed color with the default color from the color theme. */ /** Reset a particular indexed color with the default color from the color theme. */
public void reset(int index) { public void reset(int index) {
mCurrentColors[index] = COLOR_SCHEME.mDefaultColors[index]; mCurrentColors[index] = COLOR_SCHEME.mDefaultColors[index];
} }
/** Reset all indexed colors with the default color from the color theme. */ /** Reset all indexed colors with the default color from the color theme. */
public void reset() { public void reset() {
System.arraycopy(COLOR_SCHEME.mDefaultColors, 0, mCurrentColors, 0, TextStyle.NUM_INDEXED_COLORS); System.arraycopy(COLOR_SCHEME.mDefaultColors, 0, mCurrentColors, 0, TextStyle.NUM_INDEXED_COLORS);
} }
/** /**
* 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) {
try { try {
int skipInitial, skipBetween; int skipInitial, skipBetween;
if (c.charAt(0) == '#') { if (c.charAt(0) == '#') {
// #RGB, #RRGGBB, #RRRGGGBBB or #RRRRGGGGBBBB. Most significant bits. // #RGB, #RRGGBB, #RRRGGGBBB or #RRRRGGGGBBBB. Most significant bits.
skipInitial = 1; skipInitial = 1;
skipBetween = 0; skipBetween = 0;
} else if (c.startsWith("rgb:")) { } else if (c.startsWith("rgb:")) {
// rgb:<red>/<green>/<blue> where <red>, <green>, <blue> := h | hh | hhh | hhhh. Scaled. // rgb:<red>/<green>/<blue> where <red>, <green>, <blue> := h | hh | hhh | hhhh. Scaled.
skipInitial = 4; skipInitial = 4;
skipBetween = 1; skipBetween = 1;
} else { } else {
return 0; return 0;
} }
int charsForColors = c.length() - skipInitial - 2 * skipBetween; int charsForColors = c.length() - skipInitial - 2 * skipBetween;
if (charsForColors % 3 != 0) return 0; // Unequal lengths. if (charsForColors % 3 != 0) return 0; // Unequal lengths.
int componentLength = charsForColors / 3; int componentLength = charsForColors / 3;
double mult = 255 / (Math.pow(2, componentLength * 4) - 1); double mult = 255 / (Math.pow(2, componentLength * 4) - 1);
int currentPosition = skipInitial; int currentPosition = skipInitial;
String rString = c.substring(currentPosition, currentPosition + componentLength); String rString = c.substring(currentPosition, currentPosition + componentLength);
currentPosition += componentLength + skipBetween; currentPosition += componentLength + skipBetween;
String gString = c.substring(currentPosition, currentPosition + componentLength); String gString = c.substring(currentPosition, currentPosition + componentLength);
currentPosition += componentLength + skipBetween; currentPosition += componentLength + skipBetween;
String bString = c.substring(currentPosition, currentPosition + componentLength); String bString = c.substring(currentPosition, currentPosition + componentLength);
int r = (int) (Integer.parseInt(rString, 16) * mult); int r = (int) (Integer.parseInt(rString, 16) * mult);
int g = (int) (Integer.parseInt(gString, 16) * mult); int g = (int) (Integer.parseInt(gString, 16) * mult);
int b = (int) (Integer.parseInt(bString, 16) * mult); int b = (int) (Integer.parseInt(bString, 16) * mult);
return 0xFF << 24 | r << 16 | g << 8 | b; return 0xFF << 24 | r << 16 | g << 8 | b;
} catch (NumberFormatException | IndexOutOfBoundsException e) { } catch (NumberFormatException | IndexOutOfBoundsException e) {
return 0; return 0;
} }
} }
/** Try parse a color from a text parameter and into a specified index. */ /** Try parse a color from a text parameter and into a specified index. */
public void tryParseColor(int intoIndex, String textParameter) { public void tryParseColor(int intoIndex, String textParameter) {
int c = parse(textParameter); int c = parse(textParameter);
if (c != 0) mCurrentColors[intoIndex] = c; if (c != 0) mCurrentColors[intoIndex] = c;
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -5,22 +5,24 @@ import java.nio.charset.StandardCharsets;
/** A client which receives callbacks from events triggered by feeding input to a {@link TerminalEmulator}. */ /** A client which receives callbacks from events triggered by feeding input to a {@link TerminalEmulator}. */
public abstract class TerminalOutput { public abstract class TerminalOutput {
/** Write a string using the UTF-8 encoding to the terminal client. */ /** Write a string using the UTF-8 encoding to the terminal client. */
public final void write(String data) { public final void write(String data) {
byte[] bytes = data.getBytes(StandardCharsets.UTF_8); byte[] bytes = data.getBytes(StandardCharsets.UTF_8);
write(bytes, 0, bytes.length); write(bytes, 0, bytes.length);
} }
/** Write bytes to the terminal client. */ /** Write bytes to the terminal client. */
public abstract void write(byte[] data, int offset, int count); public abstract void write(byte[] data, int offset, int count);
/** Notify the terminal client that the terminal title has changed. */ /** Notify the terminal client that the terminal title has changed. */
public abstract void titleChanged(String oldTitle, String newTitle); public abstract void titleChanged(String oldTitle, String newTitle);
/** Notify the terminal client that the terminal title has changed. */ /** Notify the terminal client that the terminal title has changed. */
public abstract void clipboardText(String text); public abstract void clipboardText(String text);
/** 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,229 +4,229 @@ 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 {
private static final float SPARE_CAPACITY_FACTOR = 1.5f; private static final float SPARE_CAPACITY_FACTOR = 1.5f;
/** The number of columns in this terminal row. */ /** The number of columns in this terminal row. */
private final int mColumns; private final int mColumns;
/** The text filling this terminal row. */ /** The text filling this terminal row. */
public char[] mText; public char[] mText;
/** The number of java char:s used in {@link #mText}. */ /** The number of java char:s used in {@link #mText}. */
private short mSpaceUsed; private short mSpaceUsed;
/** If this row has been line wrapped due to text output at the end of line. */ /** If this row has been line wrapped due to text output at the end of line. */
boolean mLineWrap; boolean mLineWrap;
/** The style bits of each cell in the row. See {@link TextStyle}. */ /** The style bits of each cell in the row. See {@link TextStyle}. */
final int[] mStyle; final long[] mStyle;
/** Construct a blank row (containing only whitespace, ' ') with a specified style. */ /** Construct a blank row (containing only whitespace, ' ') with a specified style. */
public TerminalRow(int columns, int style) { public TerminalRow(int columns, long style) {
mColumns = columns; mColumns = columns;
mText = new char[(int) (SPARE_CAPACITY_FACTOR * columns)]; mText = new char[(int) (SPARE_CAPACITY_FACTOR * columns)];
mStyle = new int[columns]; mStyle = new long[columns];
clear(style); clear(style);
} }
/** NOTE: The sourceX2 is exclusive. */ /** NOTE: The sourceX2 is exclusive. */
public void copyInterval(TerminalRow line, int sourceX1, int sourceX2, int destinationX) { public void copyInterval(TerminalRow line, int sourceX1, int sourceX2, int destinationX) {
final int x1 = line.findStartOfColumn(sourceX1); final int x1 = line.findStartOfColumn(sourceX1);
final int x2 = line.findStartOfColumn(sourceX2); final int x2 = line.findStartOfColumn(sourceX2);
boolean startingFromSecondHalfOfWideChar = (sourceX1 > 0 && line.wideDisplayCharacterStartingAt(sourceX1 - 1)); boolean startingFromSecondHalfOfWideChar = (sourceX1 > 0 && line.wideDisplayCharacterStartingAt(sourceX1 - 1));
final char[] sourceChars = (this == line) ? Arrays.copyOf(line.mText, line.mText.length) : line.mText; final char[] sourceChars = (this == line) ? Arrays.copyOf(line.mText, line.mText.length) : line.mText;
int latestNonCombiningWidth = 0; int latestNonCombiningWidth = 0;
for (int i = x1; i < x2; i++) { for (int i = x1; i < x2; i++) {
char sourceChar = sourceChars[i]; char sourceChar = sourceChars[i];
int codePoint = Character.isHighSurrogate(sourceChar) ? Character.toCodePoint(sourceChar, sourceChars[++i]) : sourceChar; int codePoint = Character.isHighSurrogate(sourceChar) ? Character.toCodePoint(sourceChar, sourceChars[++i]) : sourceChar;
if (startingFromSecondHalfOfWideChar) { if (startingFromSecondHalfOfWideChar) {
// Just treat copying second half of wide char as copying whitespace. // Just treat copying second half of wide char as copying whitespace.
codePoint = ' '; codePoint = ' ';
startingFromSecondHalfOfWideChar = false; startingFromSecondHalfOfWideChar = false;
} }
int w = WcWidth.width(codePoint); int w = WcWidth.width(codePoint);
if (w > 0) { if (w > 0) {
destinationX += latestNonCombiningWidth; destinationX += latestNonCombiningWidth;
sourceX1 += latestNonCombiningWidth; sourceX1 += latestNonCombiningWidth;
latestNonCombiningWidth = w; latestNonCombiningWidth = w;
} }
setChar(destinationX, codePoint, line.getStyle(sourceX1)); setChar(destinationX, codePoint, line.getStyle(sourceX1));
} }
} }
public int getSpaceUsed() { public int getSpaceUsed() {
return mSpaceUsed; return mSpaceUsed;
} }
/** Note that the column may end of second half of wide character. */ /** Note that the column may end of second half of wide character. */
public int findStartOfColumn(int column) { public int findStartOfColumn(int column) {
if (column == mColumns) return getSpaceUsed(); if (column == mColumns) return getSpaceUsed();
int currentColumn = 0; int currentColumn = 0;
int currentCharIndex = 0; int currentCharIndex = 0;
while (true) { // 0<2 1 < 2 while (true) { // 0<2 1 < 2
int newCharIndex = currentCharIndex; int newCharIndex = currentCharIndex;
char c = mText[newCharIndex++]; // cci=1, cci=2 char c = mText[newCharIndex++]; // cci=1, cci=2
boolean isHigh = Character.isHighSurrogate(c); boolean isHigh = Character.isHighSurrogate(c);
int codePoint = isHigh ? Character.toCodePoint(c, mText[newCharIndex++]) : c; int codePoint = isHigh ? Character.toCodePoint(c, mText[newCharIndex++]) : c;
int wcwidth = WcWidth.width(codePoint); // 1, 2 int wcwidth = WcWidth.width(codePoint); // 1, 2
if (wcwidth > 0) { if (wcwidth > 0) {
currentColumn += wcwidth; currentColumn += wcwidth;
if (currentColumn == column) { if (currentColumn == column) {
while (newCharIndex < mSpaceUsed) { while (newCharIndex < mSpaceUsed) {
// Skip combining chars. // Skip combining chars.
if (Character.isHighSurrogate(mText[newCharIndex])) { if (Character.isHighSurrogate(mText[newCharIndex])) {
if (WcWidth.width(Character.toCodePoint(mText[newCharIndex], mText[newCharIndex + 1])) <= 0) { if (WcWidth.width(Character.toCodePoint(mText[newCharIndex], mText[newCharIndex + 1])) <= 0) {
newCharIndex += 2; newCharIndex += 2;
} else { } else {
break; break;
} }
} else if (WcWidth.width(mText[newCharIndex]) <= 0) { } else if (WcWidth.width(mText[newCharIndex]) <= 0) {
newCharIndex++; newCharIndex++;
} else { } else {
break; break;
} }
} }
return newCharIndex; return newCharIndex;
} else if (currentColumn > column) { } else if (currentColumn > column) {
// Wide column going past end. // Wide column going past end.
return currentCharIndex; return currentCharIndex;
} }
} }
currentCharIndex = newCharIndex; currentCharIndex = newCharIndex;
} }
} }
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);
if (wcwidth > 0) { if (wcwidth > 0) {
if (currentColumn == column && wcwidth == 2) return true; if (currentColumn == column && wcwidth == 2) return true;
currentColumn += wcwidth; currentColumn += wcwidth;
if (currentColumn > column) return false; if (currentColumn > column) return false;
} }
} }
return false; return false;
} }
public void clear(int style) { public void clear(long style) {
Arrays.fill(mText, ' '); Arrays.fill(mText, ' ');
Arrays.fill(mStyle, style); Arrays.fill(mStyle, style);
mSpaceUsed = (short) mColumns; mSpaceUsed = (short) mColumns;
} }
// https://github.com/steven676/Android-Terminal-Emulator/commit/9a47042620bec87617f0b4f5d50568535668fe26 // https://github.com/steven676/Android-Terminal-Emulator/commit/9a47042620bec87617f0b4f5d50568535668fe26
public void setChar(int columnToSet, int codePoint, int style) { public void setChar(int columnToSet, int codePoint, long style) {
mStyle[columnToSet] = style; mStyle[columnToSet] = style;
final int newCodePointDisplayWidth = WcWidth.width(codePoint); final int newCodePointDisplayWidth = WcWidth.width(codePoint);
final boolean newIsCombining = newCodePointDisplayWidth <= 0; final boolean newIsCombining = newCodePointDisplayWidth <= 0;
boolean wasExtraColForWideChar = (columnToSet > 0) && wideDisplayCharacterStartingAt(columnToSet - 1); boolean wasExtraColForWideChar = (columnToSet > 0) && wideDisplayCharacterStartingAt(columnToSet - 1);
if (newIsCombining) { if (newIsCombining) {
// When standing at second half of wide character and inserting combining: // When standing at second half of wide character and inserting combining:
if (wasExtraColForWideChar) columnToSet--; if (wasExtraColForWideChar) columnToSet--;
} else { } else {
// Check if we are overwriting the second half of a wide character starting at the previous column: // Check if we are overwriting the second half of a wide character starting at the previous column:
if (wasExtraColForWideChar) setChar(columnToSet - 1, ' ', style); if (wasExtraColForWideChar) setChar(columnToSet - 1, ' ', style);
// Check if we are overwriting the first half of a wide character starting at the next column: // Check if we are overwriting the first half of a wide character starting at the next column:
boolean overwritingWideCharInNextColumn = newCodePointDisplayWidth == 2 && wideDisplayCharacterStartingAt(columnToSet + 1); boolean overwritingWideCharInNextColumn = newCodePointDisplayWidth == 2 && wideDisplayCharacterStartingAt(columnToSet + 1);
if (overwritingWideCharInNextColumn) setChar(columnToSet + 1, ' ', style); if (overwritingWideCharInNextColumn) setChar(columnToSet + 1, ' ', style);
} }
char[] text = mText; char[] text = mText;
final int oldStartOfColumnIndex = findStartOfColumn(columnToSet); final int oldStartOfColumnIndex = findStartOfColumn(columnToSet);
final int oldCodePointDisplayWidth = WcWidth.width(text, oldStartOfColumnIndex); final int oldCodePointDisplayWidth = WcWidth.width(text, oldStartOfColumnIndex);
// Get the number of elements in the mText array this column uses now // Get the number of elements in the mText array this column uses now
int oldCharactersUsedForColumn; int oldCharactersUsedForColumn;
if (columnToSet + oldCodePointDisplayWidth < mColumns) { if (columnToSet + oldCodePointDisplayWidth < mColumns) {
oldCharactersUsedForColumn = findStartOfColumn(columnToSet + oldCodePointDisplayWidth) - oldStartOfColumnIndex; oldCharactersUsedForColumn = findStartOfColumn(columnToSet + oldCodePointDisplayWidth) - oldStartOfColumnIndex;
} else { } else {
// Last character. // Last character.
oldCharactersUsedForColumn = mSpaceUsed - oldStartOfColumnIndex; oldCharactersUsedForColumn = mSpaceUsed - oldStartOfColumnIndex;
} }
// Find how many chars this column will need // Find how many chars this column will need
int newCharactersUsedForColumn = Character.charCount(codePoint); int newCharactersUsedForColumn = Character.charCount(codePoint);
if (newIsCombining) { if (newIsCombining) {
// Combining characters are added to the contents of the column instead of overwriting them, so that they // Combining characters are added to the contents of the column instead of overwriting them, so that they
// modify the existing contents. // modify the existing contents.
// FIXME: Put a limit of combining characters. // FIXME: Put a limit of combining characters.
// FIXME: Unassigned characters also get width=0. // FIXME: Unassigned characters also get width=0.
newCharactersUsedForColumn += oldCharactersUsedForColumn; newCharactersUsedForColumn += oldCharactersUsedForColumn;
} }
int oldNextColumnIndex = oldStartOfColumnIndex + oldCharactersUsedForColumn; int oldNextColumnIndex = oldStartOfColumnIndex + oldCharactersUsedForColumn;
int newNextColumnIndex = oldStartOfColumnIndex + newCharactersUsedForColumn; int newNextColumnIndex = oldStartOfColumnIndex + newCharactersUsedForColumn;
final int javaCharDifference = newCharactersUsedForColumn - oldCharactersUsedForColumn; final int javaCharDifference = newCharactersUsedForColumn - oldCharactersUsedForColumn;
if (javaCharDifference > 0) { if (javaCharDifference > 0) {
// Shift the rest of the line right. // Shift the rest of the line right.
int oldCharactersAfterColumn = mSpaceUsed - oldNextColumnIndex; int oldCharactersAfterColumn = mSpaceUsed - oldNextColumnIndex;
if (mSpaceUsed + javaCharDifference > text.length) { if (mSpaceUsed + javaCharDifference > text.length) {
// We need to grow the array // We need to grow the array
char[] newText = new char[text.length + mColumns]; char[] newText = new char[text.length + mColumns];
System.arraycopy(text, 0, newText, 0, oldStartOfColumnIndex + oldCharactersUsedForColumn); System.arraycopy(text, 0, newText, 0, oldStartOfColumnIndex + oldCharactersUsedForColumn);
System.arraycopy(text, oldNextColumnIndex, newText, newNextColumnIndex, oldCharactersAfterColumn); System.arraycopy(text, oldNextColumnIndex, newText, newNextColumnIndex, oldCharactersAfterColumn);
mText = text = newText; mText = text = newText;
} else { } else {
System.arraycopy(text, oldNextColumnIndex, text, newNextColumnIndex, oldCharactersAfterColumn); System.arraycopy(text, oldNextColumnIndex, text, newNextColumnIndex, oldCharactersAfterColumn);
} }
} else if (javaCharDifference < 0) { } else if (javaCharDifference < 0) {
// Shift the rest of the line left. // Shift the rest of the line left.
System.arraycopy(text, oldNextColumnIndex, text, newNextColumnIndex, mSpaceUsed - oldNextColumnIndex); System.arraycopy(text, oldNextColumnIndex, text, newNextColumnIndex, mSpaceUsed - oldNextColumnIndex);
} }
mSpaceUsed += javaCharDifference; mSpaceUsed += javaCharDifference;
// Store char. A combining character is stored at the end of the existing contents so that it modifies them: // Store char. A combining character is stored at the end of the existing contents so that it modifies them:
//noinspection ResultOfMethodCallIgnored - since we already now how many java chars is used. //noinspection ResultOfMethodCallIgnored - since we already now how many java chars is used.
Character.toChars(codePoint, text, oldStartOfColumnIndex + (newIsCombining ? oldCharactersUsedForColumn : 0)); Character.toChars(codePoint, text, oldStartOfColumnIndex + (newIsCombining ? oldCharactersUsedForColumn : 0));
if (oldCodePointDisplayWidth == 2 && newCodePointDisplayWidth == 1) { if (oldCodePointDisplayWidth == 2 && newCodePointDisplayWidth == 1) {
// Replace second half of wide char with a space. Which mean that we actually add a ' ' java character. // Replace second half of wide char with a space. Which mean that we actually add a ' ' java character.
if (mSpaceUsed + 1 > text.length) { if (mSpaceUsed + 1 > text.length) {
char[] newText = new char[text.length + mColumns]; char[] newText = new char[text.length + mColumns];
System.arraycopy(text, 0, newText, 0, newNextColumnIndex); System.arraycopy(text, 0, newText, 0, newNextColumnIndex);
System.arraycopy(text, newNextColumnIndex, newText, newNextColumnIndex + 1, mSpaceUsed - newNextColumnIndex); System.arraycopy(text, newNextColumnIndex, newText, newNextColumnIndex + 1, mSpaceUsed - newNextColumnIndex);
mText = text = newText; mText = text = newText;
} else { } else {
System.arraycopy(text, newNextColumnIndex, text, newNextColumnIndex + 1, mSpaceUsed - newNextColumnIndex); System.arraycopy(text, newNextColumnIndex, text, newNextColumnIndex + 1, mSpaceUsed - newNextColumnIndex);
} }
text[newNextColumnIndex] = ' '; text[newNextColumnIndex] = ' ';
++mSpaceUsed; ++mSpaceUsed;
} else if (oldCodePointDisplayWidth == 1 && newCodePointDisplayWidth == 2) { } else if (oldCodePointDisplayWidth == 1 && newCodePointDisplayWidth == 2) {
if (columnToSet == mColumns - 1) { if (columnToSet == mColumns - 1) {
throw new IllegalArgumentException("Cannot put wide character in last column"); throw new IllegalArgumentException("Cannot put wide character in last column");
} else if (columnToSet == mColumns - 2) { } else if (columnToSet == mColumns - 2) {
// Truncate the line to the second part of this wide char: // Truncate the line to the second part of this wide char:
mSpaceUsed = (short) newNextColumnIndex; mSpaceUsed = (short) newNextColumnIndex;
} else { } else {
// Overwrite the contents of the next column, which mean we actually remove java characters. Due to the // Overwrite the contents of the next column, which mean we actually remove java characters. Due to the
// check at the beginning of this method we know that we are not overwriting a wide char. // check at the beginning of this method we know that we are not overwriting a wide char.
int newNextNextColumnIndex = newNextColumnIndex + (Character.isHighSurrogate(mText[newNextColumnIndex]) ? 2 : 1); int newNextNextColumnIndex = newNextColumnIndex + (Character.isHighSurrogate(mText[newNextColumnIndex]) ? 2 : 1);
int nextLen = newNextNextColumnIndex - newNextColumnIndex; int nextLen = newNextNextColumnIndex - newNextColumnIndex;
// Shift the array leftwards. // Shift the array leftwards.
System.arraycopy(text, newNextNextColumnIndex, text, newNextColumnIndex, mSpaceUsed - newNextNextColumnIndex); System.arraycopy(text, newNextNextColumnIndex, text, newNextColumnIndex, mSpaceUsed - newNextNextColumnIndex);
mSpaceUsed -= nextLen; mSpaceUsed -= nextLen;
} }
} }
} }
boolean isBlank() { boolean isBlank() {
for (int charIndex = 0, charLen = getSpaceUsed(); charIndex < charLen; charIndex++) for (int charIndex = 0, charLen = getSpaceUsed(); charIndex < charLen; charIndex++)
if (mText[charIndex] != ' ') return false; if (mText[charIndex] != ' ') return false;
return true; return true;
} }
public final int getStyle(int column) { public final long getStyle(int column) {
return mStyle[column]; return mStyle[column];
} }
} }

View File

@@ -1,5 +1,13 @@
package com.termux.terminal; package com.termux.terminal;
import android.annotation.SuppressLint;
import android.os.Handler;
import android.os.Message;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
import android.util.Log;
import java.io.FileDescriptor; import java.io.FileDescriptor;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
@@ -9,320 +17,326 @@ 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 {
/** Callback to be invoked when a {@link TerminalSession} changes. */ /** Callback to be invoked when a {@link TerminalSession} changes. */
public interface SessionChangedCallback { public interface SessionChangedCallback {
void onTextChanged(TerminalSession changedSession); void onTextChanged(TerminalSession changedSession);
void onTitleChanged(TerminalSession changedSession); void onTitleChanged(TerminalSession changedSession);
void onSessionFinished(TerminalSession finishedSession); void onSessionFinished(TerminalSession finishedSession);
void onClipboardText(TerminalSession session, String text); void onClipboardText(TerminalSession session, String text);
void onBell(TerminalSession session); void onBell(TerminalSession session);
}
private static FileDescriptor wrapFileDescriptor(int fileDescriptor) { void onColorsChanged(TerminalSession session);
FileDescriptor result = new FileDescriptor();
try {
Field descriptorField;
try {
descriptorField = FileDescriptor.class.getDeclaredField("descriptor");
} catch (NoSuchFieldException e) {
// For desktop java:
descriptorField = FileDescriptor.class.getDeclaredField("fd");
}
descriptorField.setAccessible(true);
descriptorField.set(result, fileDescriptor);
} catch (NoSuchFieldException | IllegalAccessException | IllegalArgumentException e) {
Log.wtf(EmulatorDebug.LOG_TAG, "Error accessing FileDescriptor#descriptor private field", e);
System.exit(1);
}
return result;
}
private static final int MSG_NEW_INPUT = 1; }
private static final int MSG_PROCESS_EXITED = 4;
public final String mHandle = UUID.randomUUID().toString(); private static FileDescriptor wrapFileDescriptor(int fileDescriptor) {
FileDescriptor result = new FileDescriptor();
try {
Field descriptorField;
try {
descriptorField = FileDescriptor.class.getDeclaredField("descriptor");
} catch (NoSuchFieldException e) {
// For desktop java:
descriptorField = FileDescriptor.class.getDeclaredField("fd");
}
descriptorField.setAccessible(true);
descriptorField.set(result, fileDescriptor);
} catch (NoSuchFieldException | IllegalAccessException | IllegalArgumentException e) {
Log.wtf(EmulatorDebug.LOG_TAG, "Error accessing FileDescriptor#descriptor private field", e);
System.exit(1);
}
return result;
}
TerminalEmulator mEmulator; private static final int MSG_NEW_INPUT = 1;
private static final int MSG_PROCESS_EXITED = 4;
/** public final String mHandle = UUID.randomUUID().toString();
* A queue written to from a separate thread when the process outputs, and read by main thread to process by
* terminal emulator.
*/
final ByteQueue mProcessToTerminalIOQueue = new ByteQueue(4096);
/**
* A queue written to from the main thread due to user interaction, and read by another thread which forwards by
* writing to the {@link #mTerminalFileDescriptor}.
*/
final ByteQueue mTerminalToProcessIOQueue = new ByteQueue(4096);
/** Buffer to write translate code points into utf8 before writing to mTerminalToProcessIOQueue */
private final byte[] mUtf8InputBuffer = new byte[5];
/** Callback which gets notified when a session finishes or changes title. */ TerminalEmulator mEmulator;
final SessionChangedCallback mChangeCallback;
/** The pid of the shell process. 0 if not started and -1 if finished running. */ /**
int mShellPid; * A queue written to from a separate thread when the process outputs, and read by main thread to process by
* terminal emulator.
*/
final ByteQueue mProcessToTerminalIOQueue = new ByteQueue(4096);
/**
* A queue written to from the main thread due to user interaction, and read by another thread which forwards by
* writing to the {@link #mTerminalFileDescriptor}.
*/
final ByteQueue mTerminalToProcessIOQueue = new ByteQueue(4096);
/** Buffer to write translate code points into utf8 before writing to mTerminalToProcessIOQueue */
private final byte[] mUtf8InputBuffer = new byte[5];
/** The exit status of the shell process. Only valid if ${@link #mShellPid} is -1. */ /** Callback which gets notified when a session finishes or changes title. */
int mShellExitStatus; final SessionChangedCallback mChangeCallback;
/** /** The pid of the shell process. 0 if not started and -1 if finished running. */
* The file descriptor referencing the master half of a pseudo-terminal pair, resulting from calling int mShellPid;
* {@link JNI#createSubprocess(String, String, String[], String[], int[], int, int)}.
*/
private int mTerminalFileDescriptor;
/** Set by the application for user identification of session, not by terminal. */ /** The exit status of the shell process. Only valid if ${@link #mShellPid} is -1. */
public String mSessionName; int mShellExitStatus;
@SuppressLint("HandlerLeak") /**
final Handler mMainThreadHandler = new Handler() { * The file descriptor referencing the master half of a pseudo-terminal pair, resulting from calling
final byte[] mReceiveBuffer = new byte[4 * 1024]; * {@link JNI#createSubprocess(String, String, String[], String[], int[], int, int)}.
*/
private int mTerminalFileDescriptor;
@Override /** Set by the application for user identification of session, not by terminal. */
public void handleMessage(Message msg) { public String mSessionName;
if (msg.what == MSG_NEW_INPUT && isRunning()) {
int bytesRead = mProcessToTerminalIOQueue.read(mReceiveBuffer, false);
if (bytesRead > 0) {
mEmulator.append(mReceiveBuffer, bytesRead);
notifyScreenUpdate();
}
} else if (msg.what == MSG_PROCESS_EXITED) {
int exitCode = (Integer) msg.obj;
cleanupResources(exitCode);
mChangeCallback.onSessionFinished(TerminalSession.this);
String exitDescription = "\r\n[Process completed"; @SuppressLint("HandlerLeak")
if (exitCode > 0) { final Handler mMainThreadHandler = new Handler() {
// Non-zero process exit. final byte[] mReceiveBuffer = new byte[4 * 1024];
exitDescription += " with code " + exitCode;
} else if (exitCode < 0) {
// Negated signal.
exitDescription += " with signal " + (-exitCode);
}
exitDescription += " - press Enter to close]";
byte[] bytesToWrite = exitDescription.getBytes(StandardCharsets.UTF_8); @Override
mEmulator.append(bytesToWrite, bytesToWrite.length); public void handleMessage(Message msg) {
notifyScreenUpdate(); if (msg.what == MSG_NEW_INPUT && isRunning()) {
} int bytesRead = mProcessToTerminalIOQueue.read(mReceiveBuffer, false);
} if (bytesRead > 0) {
}; mEmulator.append(mReceiveBuffer, bytesRead);
notifyScreenUpdate();
}
} else if (msg.what == MSG_PROCESS_EXITED) {
int exitCode = (Integer) msg.obj;
cleanupResources(exitCode);
mChangeCallback.onSessionFinished(TerminalSession.this);
private final String mShellPath; String exitDescription = "\r\n[Process completed";
private final String mCwd; if (exitCode > 0) {
private final String[] mArgs; // Non-zero process exit.
private final String[] mEnv; exitDescription += " with code " + exitCode;
} else if (exitCode < 0) {
// Negated signal.
exitDescription += " with signal " + (-exitCode);
}
exitDescription += " - press Enter to close]";
public TerminalSession(String shellPath, String cwd, String[] args, String[] env, SessionChangedCallback changeCallback) { byte[] bytesToWrite = exitDescription.getBytes(StandardCharsets.UTF_8);
mChangeCallback = changeCallback; mEmulator.append(bytesToWrite, bytesToWrite.length);
notifyScreenUpdate();
}
}
};
this.mShellPath = shellPath; private final String mShellPath;
this.mCwd = cwd; private final String mCwd;
this.mArgs = args; private final String[] mArgs;
this.mEnv = env; private final String[] mEnv;
}
/** Inform the attached pty of the new size and reflow or initialize the emulator. */ public TerminalSession(String shellPath, String cwd, String[] args, String[] env, SessionChangedCallback changeCallback) {
public void updateSize(int columns, int rows) { mChangeCallback = changeCallback;
if (mEmulator == null) {
initializeEmulator(columns, rows);
} else {
JNI.setPtyWindowSize(mTerminalFileDescriptor, rows, columns);
mEmulator.resize(columns, rows);
}
}
/** The terminal title as set through escape sequences or null if none set. */ this.mShellPath = shellPath;
public String getTitle() { this.mCwd = cwd;
return (mEmulator == null) ? null : mEmulator.getTitle(); this.mArgs = args;
} this.mEnv = env;
}
/** /** Inform the attached pty of the new size and reflow or initialize the emulator. */
* Set the terminal emulator's window size and start terminal emulation. public void updateSize(int columns, int rows) {
* if (mEmulator == null) {
* @param columns initializeEmulator(columns, rows);
* The number of columns in the terminal window. } else {
* @param rows JNI.setPtyWindowSize(mTerminalFileDescriptor, rows, columns);
* The number of rows in the terminal window. mEmulator.resize(columns, rows);
*/ }
public void initializeEmulator(int columns, int rows) { }
mEmulator = new TerminalEmulator(this, columns, rows, /* transcript= */5000);
int[] processId = new int[1]; /** The terminal title as set through escape sequences or null if none set. */
mTerminalFileDescriptor = JNI.createSubprocess(mShellPath, mCwd, mArgs, mEnv, processId, rows, columns); public String getTitle() {
mShellPid = processId[0]; return (mEmulator == null) ? null : mEmulator.getTitle();
}
final FileDescriptor terminalFileDescriptorWrapped = wrapFileDescriptor(mTerminalFileDescriptor); /**
* Set the terminal emulator's window size and start terminal emulation.
*
* @param columns The number of columns in the terminal window.
* @param rows The number of rows in the terminal window.
*/
public void initializeEmulator(int columns, int rows) {
mEmulator = new TerminalEmulator(this, columns, rows, /* transcript= */2000);
new Thread("TermSessionInputReader[pid=" + mShellPid + "]") { int[] processId = new int[1];
@Override mTerminalFileDescriptor = JNI.createSubprocess(mShellPath, mCwd, mArgs, mEnv, processId, rows, columns);
public void run() { mShellPid = processId[0];
try (InputStream termIn = new FileInputStream(terminalFileDescriptorWrapped)) {
final byte[] buffer = new byte[4096];
while (true) {
int read = termIn.read(buffer);
if (read == -1) return;
if (!mProcessToTerminalIOQueue.write(buffer, 0, read)) return;
mMainThreadHandler.sendEmptyMessage(MSG_NEW_INPUT);
}
} catch (Exception e) {
// Ignore, just shutting down.
} finally {
// Now wait for process exit:
int processExitCode = JNI.waitFor(mShellPid);
mMainThreadHandler.sendMessage(mMainThreadHandler.obtainMessage(MSG_PROCESS_EXITED, processExitCode));
}
}
}.start();
new Thread("TermSessionOutputWriter[pid=" + mShellPid + "]") { final FileDescriptor terminalFileDescriptorWrapped = wrapFileDescriptor(mTerminalFileDescriptor);
@Override
public void run() {
final byte[] buffer = new byte[4096];
try (FileOutputStream termOut = new FileOutputStream(terminalFileDescriptorWrapped)) {
while (true) {
int bytesToWrite = mTerminalToProcessIOQueue.read(buffer, true);
if (bytesToWrite == -1) return;
termOut.write(buffer, 0, bytesToWrite);
}
} catch (IOException e) {
// Ignore.
}
}
}.start();
}
/** Write data to the shell process. */ new Thread("TermSessionInputReader[pid=" + mShellPid + "]") {
@Override @Override
public void write(byte[] data, int offset, int count) { public void run() {
if (mShellPid > 0) mTerminalToProcessIOQueue.write(data, offset, count); try (InputStream termIn = new FileInputStream(terminalFileDescriptorWrapped)) {
} final byte[] buffer = new byte[4096];
while (true) {
int read = termIn.read(buffer);
if (read == -1) return;
if (!mProcessToTerminalIOQueue.write(buffer, 0, read)) return;
mMainThreadHandler.sendEmptyMessage(MSG_NEW_INPUT);
}
} catch (Exception e) {
// Ignore, just shutting down.
}
}
}.start();
/** Write the Unicode code point to the terminal encoded in UTF-8. */ new Thread("TermSessionOutputWriter[pid=" + mShellPid + "]") {
public void writeCodePoint(boolean prependEscape, int codePoint) { @Override
if (codePoint > 1114111 || (codePoint >= 0xD800 && codePoint <= 0xDFFF)) { public void run() {
// 1114111 (= 2**16 + 1024**2 - 1) is the highest code point, [0xD800,0xDFFF] is the surrogate range. final byte[] buffer = new byte[4096];
throw new IllegalArgumentException("Invalid code point: " + codePoint); try (FileOutputStream termOut = new FileOutputStream(terminalFileDescriptorWrapped)) {
} while (true) {
int bytesToWrite = mTerminalToProcessIOQueue.read(buffer, true);
if (bytesToWrite == -1) return;
termOut.write(buffer, 0, bytesToWrite);
}
} catch (IOException e) {
// Ignore.
}
}
}.start();
int bufferPosition = 0; new Thread("TermSessionWaiter[pid=" + mShellPid + "]") {
if (prependEscape) mUtf8InputBuffer[bufferPosition++] = 27; @Override
public void run() {
int processExitCode = JNI.waitFor(mShellPid);
mMainThreadHandler.sendMessage(mMainThreadHandler.obtainMessage(MSG_PROCESS_EXITED, processExitCode));
}
}.start();
if (codePoint <= /* 7 bits */0b1111111) { }
mUtf8InputBuffer[bufferPosition++] = (byte) codePoint;
} else if (codePoint <= /* 11 bits */0b11111111111) { /** Write data to the shell process. */
/* 110xxxxx leading byte with leading 5 bits */ @Override
mUtf8InputBuffer[bufferPosition++] = (byte) (0b11000000 | (codePoint >> 6)); public void write(byte[] data, int offset, int count) {
if (mShellPid > 0) mTerminalToProcessIOQueue.write(data, offset, count);
}
/** Write the Unicode code point to the terminal encoded in UTF-8. */
public void writeCodePoint(boolean prependEscape, int codePoint) {
if (codePoint > 1114111 || (codePoint >= 0xD800 && codePoint <= 0xDFFF)) {
// 1114111 (= 2**16 + 1024**2 - 1) is the highest code point, [0xD800,0xDFFF] is the surrogate range.
throw new IllegalArgumentException("Invalid code point: " + codePoint);
}
int bufferPosition = 0;
if (prependEscape) mUtf8InputBuffer[bufferPosition++] = 27;
if (codePoint <= /* 7 bits */0b1111111) {
mUtf8InputBuffer[bufferPosition++] = (byte) codePoint;
} else if (codePoint <= /* 11 bits */0b11111111111) {
/* 110xxxxx leading byte with leading 5 bits */
mUtf8InputBuffer[bufferPosition++] = (byte) (0b11000000 | (codePoint >> 6));
/* 10xxxxxx continuation byte with following 6 bits */ /* 10xxxxxx continuation byte with following 6 bits */
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | (codePoint & 0b111111)); mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | (codePoint & 0b111111));
} else if (codePoint <= /* 16 bits */0b1111111111111111) { } else if (codePoint <= /* 16 bits */0b1111111111111111) {
/* 1110xxxx leading byte with leading 4 bits */ /* 1110xxxx leading byte with leading 4 bits */
mUtf8InputBuffer[bufferPosition++] = (byte) (0b11100000 | (codePoint >> 12)); mUtf8InputBuffer[bufferPosition++] = (byte) (0b11100000 | (codePoint >> 12));
/* 10xxxxxx continuation byte with following 6 bits */ /* 10xxxxxx continuation byte with following 6 bits */
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | ((codePoint >> 6) & 0b111111)); mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | ((codePoint >> 6) & 0b111111));
/* 10xxxxxx continuation byte with following 6 bits */ /* 10xxxxxx continuation byte with following 6 bits */
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | (codePoint & 0b111111)); mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | (codePoint & 0b111111));
} else { /* We have checked codePoint <= 1114111 above, so we have max 21 bits = 0b111111111111111111111 */ } else { /* We have checked codePoint <= 1114111 above, so we have max 21 bits = 0b111111111111111111111 */
/* 11110xxx leading byte with leading 3 bits */ /* 11110xxx leading byte with leading 3 bits */
mUtf8InputBuffer[bufferPosition++] = (byte) (0b11110000 | (codePoint >> 18)); mUtf8InputBuffer[bufferPosition++] = (byte) (0b11110000 | (codePoint >> 18));
/* 10xxxxxx continuation byte with following 6 bits */ /* 10xxxxxx continuation byte with following 6 bits */
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | ((codePoint >> 12) & 0b111111)); mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | ((codePoint >> 12) & 0b111111));
/* 10xxxxxx continuation byte with following 6 bits */ /* 10xxxxxx continuation byte with following 6 bits */
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | ((codePoint >> 6) & 0b111111)); mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | ((codePoint >> 6) & 0b111111));
/* 10xxxxxx continuation byte with following 6 bits */ /* 10xxxxxx continuation byte with following 6 bits */
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | (codePoint & 0b111111)); mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | (codePoint & 0b111111));
} }
write(mUtf8InputBuffer, 0, bufferPosition); write(mUtf8InputBuffer, 0, bufferPosition);
} }
public TerminalEmulator getEmulator() { public TerminalEmulator getEmulator() {
return mEmulator; return mEmulator;
} }
/** Notify the {@link #mChangeCallback} that the screen has changed. */ /** Notify the {@link #mChangeCallback} that the screen has changed. */
protected void notifyScreenUpdate() { protected void notifyScreenUpdate() {
mChangeCallback.onTextChanged(this); mChangeCallback.onTextChanged(this);
} }
/** Reset state for terminal emulator state. */ /** Reset state for terminal emulator state. */
public void reset() { public void reset() {
mEmulator.reset(); mEmulator.reset();
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 public void finishIfRunning() {
* <code>InputStream</code> and <code>OutputStream</code>. if (isRunning()) {
*/ try {
public void finishIfRunning() { Os.kill(mShellPid, OsConstants.SIGKILL);
if (isRunning()) { } catch (ErrnoException e) {
JNI.hangupProcessGroup(mShellPid); Log.w("termux", "Failed sending SIGKILL: " + e.getMessage());
// Stop the reader and writer threads, and close the I/O streams. Note that }
// cleanupResources() will be run later. }
mTerminalToProcessIOQueue.close(); }
mProcessToTerminalIOQueue.close();
JNI.close(mTerminalFileDescriptor);
}
}
/** Cleanup resources when the process exits. */ /** Cleanup resources when the process exits. */
void cleanupResources(int exitStatus) { void cleanupResources(int exitStatus) {
synchronized (this) { synchronized (this) {
mShellPid = -1; mShellPid = -1;
mShellExitStatus = exitStatus; mShellExitStatus = exitStatus;
} }
// Stop the reader and writer threads, and close the I/O streams // Stop the reader and writer threads, and close the I/O streams
mTerminalToProcessIOQueue.close(); mTerminalToProcessIOQueue.close();
mProcessToTerminalIOQueue.close(); mProcessToTerminalIOQueue.close();
JNI.close(mTerminalFileDescriptor); JNI.close(mTerminalFileDescriptor);
} }
@Override @Override
public void titleChanged(String oldTitle, String newTitle) { public void titleChanged(String oldTitle, String newTitle) {
mChangeCallback.onTitleChanged(this); mChangeCallback.onTitleChanged(this);
} }
public synchronized boolean isRunning() { public synchronized boolean isRunning() {
return mShellPid != -1; return mShellPid != -1;
} }
/** Only valid if not {@link #isRunning()}. */ /** Only valid if not {@link #isRunning()}. */
public synchronized int getExitStatus() { public synchronized int getExitStatus() {
return mShellExitStatus; return mShellExitStatus;
} }
@Override @Override
public void clipboardText(String text) { public void clipboardText(String text) {
mChangeCallback.onClipboardText(this, text); mChangeCallback.onClipboardText(this, text);
} }
@Override @Override
public void onBell() { public void onBell() {
mChangeCallback.onBell(this); mChangeCallback.onBell(this);
} }
@Override
public void onColorsChanged() {
mChangeCallback.onColorsChanged(this);
}
public int getPid() {
return mShellPid;
}
} }

View File

@@ -1,55 +1,86 @@
package com.termux.terminal; package com.termux.terminal;
/** /**
* Encodes effects, foreground and background colors into a 32 bit integer, which are stored for each cell in a terminal * Encodes effects, foreground and background colors into a 64 bit long, which are stored for each cell in a terminal
* row in {@link TerminalRow#mStyle}. * row in {@link TerminalRow#mStyle}.
* * <p/>
* The foreground and background colors take 9 bits each, leaving (32-9-9)=14 bits for effect flags. Using 9 for now * The bit layout is:
* (the different CHARACTER_ATTRIBUTE_* bits). * - 16 flags (11 currently used).
* - 24 for foreground color (only 9 first bits if a color index).
* - 24 for background color (only 9 first bits if a color index).
*/ */
public final class TextStyle { public final class TextStyle {
public final static int CHARACTER_ATTRIBUTE_BOLD = 1; public final static int CHARACTER_ATTRIBUTE_BOLD = 1;
public final static int CHARACTER_ATTRIBUTE_ITALIC = 1 << 1; public final static int CHARACTER_ATTRIBUTE_ITALIC = 1 << 1;
public final static int CHARACTER_ATTRIBUTE_UNDERLINE = 1 << 2; public final static int CHARACTER_ATTRIBUTE_UNDERLINE = 1 << 2;
public final static int CHARACTER_ATTRIBUTE_BLINK = 1 << 3; public final static int CHARACTER_ATTRIBUTE_BLINK = 1 << 3;
public final static int CHARACTER_ATTRIBUTE_INVERSE = 1 << 4; public final static int CHARACTER_ATTRIBUTE_INVERSE = 1 << 4;
public final static int CHARACTER_ATTRIBUTE_INVISIBLE = 1 << 5; public final static int CHARACTER_ATTRIBUTE_INVISIBLE = 1 << 5;
public final static int CHARACTER_ATTRIBUTE_STRIKETHROUGH = 1 << 6; public final static int CHARACTER_ATTRIBUTE_STRIKETHROUGH = 1 << 6;
/** /**
* The selective erase control functions (DECSED and DECSEL) can only erase characters defined as erasable. * The selective erase control functions (DECSED and DECSEL) can only erase characters defined as erasable.
* * <p/>
* This bit is set if DECSCA (Select Character Protection Attribute) has been used to define the characters that * This bit is set if DECSCA (Select Character Protection Attribute) has been used to define the characters that
* come after it as erasable from the screen. * come after it as erasable from the screen.
*/ */
public final static int CHARACTER_ATTRIBUTE_PROTECTED = 1 << 7; public final static int CHARACTER_ATTRIBUTE_PROTECTED = 1 << 7;
/** Dim colors. Also known as faint or half intensity. */ /** Dim colors. Also known as faint or half intensity. */
public final static int CHARACTER_ATTRIBUTE_DIM = 1 << 8; public final static int CHARACTER_ATTRIBUTE_DIM = 1 << 8;
/** If true (24-bit) color is used for the cell for foreground. */
private final static int CHARACTER_ATTRIBUTE_TRUECOLOR_FOREGROUND = 1 << 9;
/** If true (24-bit) color is used for the cell for foreground. */
private final static int CHARACTER_ATTRIBUTE_TRUECOLOR_BACKGROUND= 1 << 10;
public final static int COLOR_INDEX_FOREGROUND = 256; public final static int COLOR_INDEX_FOREGROUND = 256;
public final static int COLOR_INDEX_BACKGROUND = 257; public final static int COLOR_INDEX_BACKGROUND = 257;
public final static int COLOR_INDEX_CURSOR = 258; public final static int COLOR_INDEX_CURSOR = 258;
/** The 256 standard color entries and the three special (foreground, background and cursor) ones. */ /** The 256 standard color entries and the three special (foreground, background and cursor) ones. */
public final static int NUM_INDEXED_COLORS = 259; public final static int NUM_INDEXED_COLORS = 259;
/** Normal foreground and background colors and no effects. */ /** Normal foreground and background colors and no effects. */
final static int NORMAL = encode(COLOR_INDEX_FOREGROUND, COLOR_INDEX_BACKGROUND, 0); final static long NORMAL = encode(COLOR_INDEX_FOREGROUND, COLOR_INDEX_BACKGROUND, 0);
static int encode(int foreColor, int backColor, int effect) { static long encode(int foreColor, int backColor, int effect) {
return ((effect & 0b111111111) << 18) | ((foreColor & 0b111111111) << 9) | (backColor & 0b111111111); long result = effect & 0b111111111;
} if ((0xff000000 & foreColor) == 0xff000000) {
// 24-bit color.
result |= CHARACTER_ATTRIBUTE_TRUECOLOR_FOREGROUND | ((foreColor & 0x00ffffffL) << 40L);
} else {
// Indexed color.
result |= (foreColor & 0b111111111L) << 40;
}
if ((0xff000000 & backColor) == 0xff000000) {
// 24-bit color.
result |= CHARACTER_ATTRIBUTE_TRUECOLOR_BACKGROUND | ((backColor & 0x00ffffffL) << 16L);
} else {
// Indexed color.
result |= (backColor & 0b111111111L) << 16L;
}
public static int decodeForeColor(int encodedColor) { return result;
return (encodedColor >> 9) & 0b111111111; }
}
public static int decodeBackColor(int encodedColor) { public static int decodeForeColor(long style) {
return encodedColor & 0b111111111; if ((style & CHARACTER_ATTRIBUTE_TRUECOLOR_FOREGROUND) == 0) {
} return (int) ((style >>> 40) & 0b111111111L);
} else {
return 0xff000000 | (int) ((style >>> 40) & 0x00ffffffL);
}
public static int decodeEffect(int encodedColor) { }
return (encodedColor >> 18) & 0b111111111;
} public static int decodeBackColor(long style) {
if ((style & CHARACTER_ATTRIBUTE_TRUECOLOR_BACKGROUND) == 0) {
return (int) ((style >>> 16) & 0b111111111L);
} else {
return 0xff000000 | (int) ((style >>> 16) & 0x00ffffffL);
}
}
public static int decodeEffect(long style) {
return (int) (style & 0b11111111111);
}
} }

View File

@@ -1,108 +1,458 @@
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) {
if (wc < 0xff) return (wc + 1 & 0x7f) >= 0x21 ? 1 : (wc != 0) ? 0 : 0;
if ((wc & 0xfffeffff) < 0xfffe) {
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;
return 1;
}
if ((wc & 0xfffe) == 0xfffe) return 0;
if (wc - 0x20000 < 0x20000) return 2;
if (wc == 0xe0001 || wc - 0xe0020 < 0x5f || wc - 0xe0100 < 0xef) return 0;
return 1;
}
/** The width at an index position in a java char array. */ private static boolean intable(int[][] table, int c) {
public static int width(char[] chars, int index) { // First quick check f|| Latin1 etc. characters.
char c = chars[index]; if (c < table[0][0]) return false;
return Character.isHighSurrogate(c) ? width(Character.toCodePoint(c, chars[index + 1])) : width(c);
} // Binary search in table.
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;
}
}
return false;
}
/** 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. */
public static int width(char[] chars, int index) {
char c = chars[index];
return Character.isHighSurrogate(c) ? width(Character.toCodePoint(c, chars[index + 1])) : width(c);
}
} }

View File

@@ -8,104 +8,104 @@ import android.view.ScaleGestureDetector;
/** A combination of {@link GestureDetector} and {@link ScaleGestureDetector}. */ /** A combination of {@link GestureDetector} and {@link ScaleGestureDetector}. */
public final class GestureAndScaleRecognizer { public final class GestureAndScaleRecognizer {
public interface Listener { public interface Listener {
boolean onSingleTapUp(MotionEvent e); boolean onSingleTapUp(MotionEvent e);
boolean onDoubleTap(MotionEvent e); boolean onDoubleTap(MotionEvent e);
boolean onScroll(MotionEvent e2, float dx, float dy); boolean onScroll(MotionEvent e2, float dx, float dy);
boolean onFling(MotionEvent e, float velocityX, float velocityY); boolean onFling(MotionEvent e, float velocityX, float velocityY);
boolean onScale(float focusX, float focusY, float scale); boolean onScale(float focusX, float focusY, float scale);
boolean onDown(float x, float y); boolean onDown(float x, float y);
boolean onUp(MotionEvent e); boolean onUp(MotionEvent e);
void onLongPress(MotionEvent e); void onLongPress(MotionEvent e);
} }
private final GestureDetector mGestureDetector; private final GestureDetector mGestureDetector;
private final ScaleGestureDetector mScaleDetector; private final ScaleGestureDetector mScaleDetector;
final Listener mListener; final Listener mListener;
boolean isAfterLongPress; boolean isAfterLongPress;
public GestureAndScaleRecognizer(Context context, Listener listener) { public GestureAndScaleRecognizer(Context context, Listener listener) {
mListener = listener; mListener = listener;
mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() { mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
@Override @Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float dx, float dy) { public boolean onScroll(MotionEvent e1, MotionEvent e2, float dx, float dy) {
return mListener.onScroll(e2, dx, dy); return mListener.onScroll(e2, dx, dy);
} }
@Override @Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return mListener.onFling(e2, velocityX, velocityY); return mListener.onFling(e2, velocityX, velocityY);
} }
@Override @Override
public boolean onDown(MotionEvent e) { public boolean onDown(MotionEvent e) {
return mListener.onDown(e.getX(), e.getY()); return mListener.onDown(e.getX(), e.getY());
} }
@Override @Override
public void onLongPress(MotionEvent e) { public void onLongPress(MotionEvent e) {
mListener.onLongPress(e); mListener.onLongPress(e);
isAfterLongPress = true; isAfterLongPress = true;
} }
}, null, true /* ignoreMultitouch */); }, null, true /* ignoreMultitouch */);
mGestureDetector.setOnDoubleTapListener(new GestureDetector.OnDoubleTapListener() { mGestureDetector.setOnDoubleTapListener(new GestureDetector.OnDoubleTapListener() {
@Override @Override
public boolean onSingleTapConfirmed(MotionEvent e) { public boolean onSingleTapConfirmed(MotionEvent e) {
return mListener.onSingleTapUp(e); return mListener.onSingleTapUp(e);
} }
@Override @Override
public boolean onDoubleTap(MotionEvent e) { public boolean onDoubleTap(MotionEvent e) {
return mListener.onDoubleTap(e); return mListener.onDoubleTap(e);
} }
@Override @Override
public boolean onDoubleTapEvent(MotionEvent e) { public boolean onDoubleTapEvent(MotionEvent e) {
return true; return true;
} }
}); });
mScaleDetector = new ScaleGestureDetector(context, new ScaleGestureDetector.SimpleOnScaleGestureListener() { mScaleDetector = new ScaleGestureDetector(context, new ScaleGestureDetector.SimpleOnScaleGestureListener() {
@Override @Override
public boolean onScaleBegin(ScaleGestureDetector detector) { public boolean onScaleBegin(ScaleGestureDetector detector) {
return true; return true;
} }
@Override @Override
public boolean onScale(ScaleGestureDetector detector) { public boolean onScale(ScaleGestureDetector detector) {
return mListener.onScale(detector.getFocusX(), detector.getFocusY(), detector.getScaleFactor()); return mListener.onScale(detector.getFocusX(), detector.getFocusY(), detector.getScaleFactor());
} }
}); });
} }
public void onTouchEvent(MotionEvent event) { public void onTouchEvent(MotionEvent event) {
mGestureDetector.onTouchEvent(event); mGestureDetector.onTouchEvent(event);
mScaleDetector.onTouchEvent(event); mScaleDetector.onTouchEvent(event);
switch (event.getAction()) { switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_DOWN:
isAfterLongPress = false; isAfterLongPress = false;
break; break;
case MotionEvent.ACTION_UP: case MotionEvent.ACTION_UP:
if (!isAfterLongPress) { if (!isAfterLongPress) {
// This behaviour is desired when in e.g. vim with mouse events, where we do not // This behaviour is desired when in e.g. vim with mouse events, where we do not
// want to move the cursor when lifting finger after a long press. // want to move the cursor when lifting finger after a long press.
mListener.onUp(event); mListener.onUp(event);
} }
break; break;
} }
} }
public boolean isInProgress() { public boolean isInProgress() {
return mScaleDetector.isInProgress(); return mScaleDetector.isInProgress();
} }
} }

View File

@@ -1,24 +1,37 @@
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 {
/** Callback function on scale events according to {@link ScaleGestureDetector#getScaleFactor()}. */ /** Callback function on scale events according to {@link ScaleGestureDetector#getScaleFactor()}. */
float onScale(float scale); float onScale(float scale);
/** On a single tap on the terminal if terminal mouse reporting not enabled. */ /** On a single tap on the terminal if terminal mouse reporting not enabled. */
void onSingleTapUp(MotionEvent e); void onSingleTapUp(MotionEvent e);
boolean shouldBackButtonBeMappedToEscape(); boolean shouldBackButtonBeMappedToEscape();
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,224 +13,220 @@ 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 {
final int mTextSize; final int mTextSize;
final Typeface mTypeface; final Typeface mTypeface;
private final Paint mTextPaint = new Paint(); private final Paint mTextPaint = new Paint();
/** The width of a single mono spaced character obtained by {@link Paint#measureText(String)} on a single 'X'. */ /** The width of a single mono spaced character obtained by {@link Paint#measureText(String)} on a single 'X'. */
final float mFontWidth; final float mFontWidth;
/** The {@link Paint#getFontSpacing()}. See http://www.fampennings.nl/maarten/android/08numgrid/font.png */ /** The {@link Paint#getFontSpacing()}. See http://www.fampennings.nl/maarten/android/08numgrid/font.png */
final int mFontLineSpacing; final int mFontLineSpacing;
/** The {@link Paint#ascent()}. See http://www.fampennings.nl/maarten/android/08numgrid/font.png */ /** The {@link Paint#ascent()}. See http://www.fampennings.nl/maarten/android/08numgrid/font.png */
private final int mFontAscent; private final int mFontAscent;
/** The {@link #mFontLineSpacing} + {@link #mFontAscent}. */ /** The {@link #mFontLineSpacing} + {@link #mFontAscent}. */
final int mFontLineSpacingAndAscent; final int mFontLineSpacingAndAscent;
private final float[] asciiMeasures = new float[127]; private final float[] asciiMeasures = new float[127];
public TerminalRenderer(int textSize, Typeface typeface) { public TerminalRenderer(int textSize, Typeface typeface) {
mTextSize = textSize; mTextSize = textSize;
mTypeface = typeface; mTypeface = typeface;
mTextPaint.setTypeface(typeface); mTextPaint.setTypeface(typeface);
mTextPaint.setAntiAlias(true); mTextPaint.setAntiAlias(true);
mTextPaint.setTextSize(textSize); mTextPaint.setTextSize(textSize);
mFontLineSpacing = (int) Math.ceil(mTextPaint.getFontSpacing()); mFontLineSpacing = (int) Math.ceil(mTextPaint.getFontSpacing());
mFontAscent = (int) Math.ceil(mTextPaint.ascent()); mFontAscent = (int) Math.ceil(mTextPaint.ascent());
mFontLineSpacingAndAscent = mFontLineSpacing + mFontAscent; mFontLineSpacingAndAscent = mFontLineSpacing + mFontAscent;
mFontWidth = mTextPaint.measureText("X"); mFontWidth = mTextPaint.measureText("X");
StringBuilder sb = new StringBuilder(" "); StringBuilder sb = new StringBuilder(" ");
for (int i = 0; i < asciiMeasures.length; i++) { for (int i = 0; i < asciiMeasures.length; i++) {
sb.setCharAt(0, (char) i); sb.setCharAt(0, (char) i);
asciiMeasures[i] = mTextPaint.measureText(sb, 0, 1); asciiMeasures[i] = mTextPaint.measureText(sb, 0, 1);
} }
} }
/** Render the terminal to a canvas with at a specified row scroll, and an optional rectangular selection. */ /** Render the terminal to a canvas with at a specified row scroll, and an optional rectangular selection. */
public final void render(TerminalEmulator mEmulator, Canvas canvas, int topRow, int selectionY1, int selectionY2, int selectionX1, int selectionX2) { public final void render(TerminalEmulator mEmulator, Canvas canvas, int topRow, int selectionY1, int selectionY2, int selectionX1, int selectionX2) {
final boolean reverseVideo = mEmulator.isReverseVideo(); final boolean reverseVideo = mEmulator.isReverseVideo();
final int endRow = topRow + mEmulator.mRows; final int endRow = topRow + mEmulator.mRows;
final int columns = mEmulator.mColumns; final int columns = mEmulator.mColumns;
final int cursorCol = mEmulator.getCursorCol(); final int cursorCol = mEmulator.getCursorCol();
final int cursorRow = mEmulator.getCursorRow(); final int cursorRow = mEmulator.getCursorRow();
final boolean cursorVisible = mEmulator.isShowingCursor(); final boolean cursorVisible = mEmulator.isShowingCursor();
final TerminalBuffer screen = mEmulator.getScreen(); final TerminalBuffer screen = mEmulator.getScreen();
final int[] palette = mEmulator.mColors.mCurrentColors; final int[] palette = mEmulator.mColors.mCurrentColors;
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++) {
heightOffset += mFontLineSpacing; heightOffset += mFontLineSpacing;
final int cursorX = (row == cursorRow && cursorVisible) ? cursorCol : -1; final int cursorX = (row == cursorRow && cursorVisible) ? cursorCol : -1;
int selx1 = -1, selx2 = -1; int selx1 = -1, selx2 = -1;
if (row >= selectionY1 && row <= selectionY2) { if (row >= selectionY1 && row <= selectionY2) {
if (row == selectionY1) selx1 = selectionX1; if (row == selectionY1) selx1 = selectionX1;
selx2 = (row == selectionY2) ? selectionX2 : mEmulator.mColumns; selx2 = (row == selectionY2) ? selectionX2 : mEmulator.mColumns;
} }
TerminalRow lineObject = screen.allocateFullLineIfNecessary(screen.externalToInternalRow(row)); TerminalRow lineObject = screen.allocateFullLineIfNecessary(screen.externalToInternalRow(row));
final char[] line = lineObject.mText; final char[] line = lineObject.mText;
final int charsUsedInLine = lineObject.getSpaceUsed(); final int charsUsedInLine = lineObject.getSpaceUsed();
int lastRunStyle = 0; long lastRunStyle = 0;
boolean lastRunInsideCursor = false; boolean lastRunInsideCursor = false;
int lastRunStartColumn = -1; int lastRunStartColumn = -1;
int lastRunStartIndex = 0; int lastRunStartIndex = 0;
boolean lastRunFontWidthMismatch = false; boolean lastRunFontWidthMismatch = false;
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;
final int codePoint = charIsHighsurrogate ? Character.toCodePoint(charAtIndex, line[currentCharIndex + 1]) : charAtIndex; final int codePoint = charIsHighsurrogate ? Character.toCodePoint(charAtIndex, line[currentCharIndex + 1]) : charAtIndex;
final int codePointWcWidth = WcWidth.width(codePoint); final int codePointWcWidth = WcWidth.width(codePoint);
final boolean insideCursor = (column >= selx1 && column <= selx2) || (cursorX == column || (codePointWcWidth == 2 && cursorX == column + 1)); final boolean insideCursor = (column >= selx1 && column <= selx2) || (cursorX == column || (codePointWcWidth == 2 && cursorX == column + 1));
final int style = lineObject.getStyle(column); final long style = lineObject.getStyle(column);
// Check if the measured text width for this code point is not the same as that expected by wcwidth(). // Check if the measured text width for this code point is not the same as that expected by wcwidth().
// This could happen for some fonts which are not truly monospace, or for more exotic characters such as // This could happen for some fonts which are not truly monospace, or for more exotic characters such as
// smileys which android font renders as wide. // smileys which android font renders as wide.
// If this is detected, we draw this code point scaled to match what wcwidth() expects. // If this is detected, we draw this code point scaled to match what wcwidth() expects.
final float measuredCodePointWidth = (codePoint < asciiMeasures.length) ? asciiMeasures[codePoint] : mTextPaint.measureText(line, final float measuredCodePointWidth = (codePoint < asciiMeasures.length) ? asciiMeasures[codePoint] : mTextPaint.measureText(line,
currentCharIndex, charsForCodePoint); currentCharIndex, charsForCodePoint);
final boolean fontWidthMismatch = Math.abs(measuredCodePointWidth / mFontWidth - codePointWcWidth) > 0.01; final boolean fontWidthMismatch = Math.abs(measuredCodePointWidth / mFontWidth - codePointWcWidth) > 0.01;
if (style != lastRunStyle || insideCursor != lastRunInsideCursor || fontWidthMismatch || lastRunFontWidthMismatch) { if (style != lastRunStyle || insideCursor != lastRunInsideCursor || fontWidthMismatch || lastRunFontWidthMismatch) {
if (column == 0) { if (column == 0) {
// Skip first column as there is nothing to draw, just record the current style. // Skip first column as there is nothing to draw, just record the current style.
} else { } else {
final int columnWidthSinceLastRun = column - lastRunStartColumn; final int columnWidthSinceLastRun = column - lastRunStartColumn;
final int charsSinceLastRun = currentCharIndex - lastRunStartIndex; final int charsSinceLastRun = currentCharIndex - lastRunStartIndex;
drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun, lastRunStartIndex, charsSinceLastRun, drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun, lastRunStartIndex, charsSinceLastRun,
measuredWidthForRun, lastRunInsideCursor, lastRunStyle, reverseVideo); measuredWidthForRun, lastRunInsideCursor, lastRunStyle, reverseVideo);
} }
measuredWidthForRun = 0.f; measuredWidthForRun = 0.f;
lastRunStyle = style; lastRunStyle = style;
lastRunInsideCursor = insideCursor; lastRunInsideCursor = insideCursor;
lastRunStartColumn = column; lastRunStartColumn = column;
lastRunStartIndex = currentCharIndex; lastRunStartIndex = currentCharIndex;
lastRunFontWidthMismatch = fontWidthMismatch; lastRunFontWidthMismatch = fontWidthMismatch;
} }
measuredWidthForRun += measuredCodePointWidth; measuredWidthForRun += measuredCodePointWidth;
column += codePointWcWidth; column += codePointWcWidth;
currentCharIndex += charsForCodePoint; currentCharIndex += charsForCodePoint;
while (currentCharIndex < charsUsedInLine && WcWidth.width(line, currentCharIndex) <= 0) { while (currentCharIndex < charsUsedInLine && WcWidth.width(line, currentCharIndex) <= 0) {
// Eat combining chars so that they are treated as part of the last non-combining code point, // Eat combining chars so that they are treated as part of the last non-combining code point,
// instead of e.g. being considered inside the cursor in the next run. // instead of e.g. being considered inside the cursor in the next run.
currentCharIndex += Character.isHighSurrogate(line[currentCharIndex]) ? 2 : 1; currentCharIndex += Character.isHighSurrogate(line[currentCharIndex]) ? 2 : 1;
} }
} }
final int columnWidthSinceLastRun = columns - lastRunStartColumn; final int columnWidthSinceLastRun = columns - lastRunStartColumn;
final int charsSinceLastRun = currentCharIndex - lastRunStartIndex; final int charsSinceLastRun = currentCharIndex - lastRunStartIndex;
drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun, lastRunStartIndex, charsSinceLastRun, drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun, lastRunStartIndex, charsSinceLastRun,
measuredWidthForRun, lastRunInsideCursor, lastRunStyle, reverseVideo); measuredWidthForRun, lastRunInsideCursor, lastRunStyle, reverseVideo);
} }
} }
/** /**
* @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 private void drawTextRun(Canvas canvas, char[] text, int[] palette, float y, int startColumn, int runWidthColumns, int startCharIndex, int runWidthChars,
* index into the text array where to start float mes, boolean cursor, long textStyle, boolean reverseVideo) {
* @param runWidthChars int foreColor = TextStyle.decodeForeColor(textStyle);
* number of java characters from the text array to render int backColor = TextStyle.decodeBackColor(textStyle);
* @param cursor
* true if rendering a cursor or selection
* @param textStyle
* the background, foreground and effect encoded using {@link TextStyle}
* @param reverseVideo
* if the screen is rendered with the global reverse video flag set
*/
private void drawTextRun(Canvas canvas, char[] text, int[] palette, float y, int startColumn, int runWidthColumns, int startCharIndex, int runWidthChars,
float mes, boolean cursor, int textStyle, boolean reverseVideo) {
int foreColor = TextStyle.decodeForeColor(textStyle);
int backColor = TextStyle.decodeBackColor(textStyle);
final int effect = TextStyle.decodeEffect(textStyle);
float left = startColumn * mFontWidth;
float right = left + runWidthColumns * mFontWidth;
mes = mes / mFontWidth; int foreColorIndex = -1;
boolean savedMatrix = false; if ((foreColor & 0xff000000) != 0xff000000) {
if (Math.abs(mes - runWidthColumns) > 0.01) { foreColorIndex = foreColor;
canvas.save(); foreColor = palette[foreColor];
canvas.scale(runWidthColumns / mes, 1.f); }
left *= mes / runWidthColumns; if ((backColor & 0xff000000) != 0xff000000) backColor = palette[backColor];
right *= mes / runWidthColumns;
savedMatrix = true;
}
// Reverse video here if _one and only one_ of the reverse flags are set: final int effect = TextStyle.decodeEffect(textStyle);
boolean reverseVideoHere = reverseVideo ^ (effect & (TextStyle.CHARACTER_ATTRIBUTE_INVERSE)) != 0; float left = startColumn * mFontWidth;
// Switch if _one and only one_ of reverse video and cursor is set: float right = left + runWidthColumns * mFontWidth;
if (reverseVideoHere ^ cursor) {
int tmp = foreColor;
foreColor = backColor;
backColor = tmp;
}
if (backColor != TextStyle.COLOR_INDEX_BACKGROUND) { mes = mes / mFontWidth;
// Only draw non-default background. boolean savedMatrix = false;
mTextPaint.setColor(palette[backColor]); if (Math.abs(mes - runWidthColumns) > 0.01) {
canvas.drawRect(left, y - mFontLineSpacingAndAscent + mFontAscent, right, y, mTextPaint); canvas.save();
} canvas.scale(runWidthColumns / mes, 1.f);
left *= mes / runWidthColumns;
right *= mes / runWidthColumns;
savedMatrix = true;
}
if ((effect & TextStyle.CHARACTER_ATTRIBUTE_INVISIBLE) == 0) { // Reverse video here if _one and only one_ of the reverse flags are set:
// Treat blink as bold: boolean reverseVideoHere = reverseVideo ^ (effect & (TextStyle.CHARACTER_ATTRIBUTE_INVERSE)) != 0;
final boolean bold = (effect & (TextStyle.CHARACTER_ATTRIBUTE_BOLD | TextStyle.CHARACTER_ATTRIBUTE_BLINK)) != 0; // Switch if _one and only one_ of reverse video and cursor is set:
final boolean underline = (effect & TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE) != 0; if (reverseVideoHere ^ cursor) {
final boolean italic = (effect & TextStyle.CHARACTER_ATTRIBUTE_ITALIC) != 0; int tmp = foreColor;
final boolean strikeThrough = (effect & TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH) != 0; foreColor = backColor;
final boolean dim = (effect & TextStyle.CHARACTER_ATTRIBUTE_DIM) != 0; backColor = tmp;
}
// Let bold have bright colors if applicable (one of the first 8): if (backColor != palette[TextStyle.COLOR_INDEX_BACKGROUND]) {
final int actualForeColor = foreColor + (bold && foreColor < 8 ? 8 : 0); // Only draw non-default background.
mTextPaint.setColor(backColor);
canvas.drawRect(left, y - mFontLineSpacingAndAscent + mFontAscent, right, y, mTextPaint);
}
int foreColorARGB = palette[actualForeColor]; if ((effect & TextStyle.CHARACTER_ATTRIBUTE_INVISIBLE) == 0) {
if (dim) { // Treat blink as bold:
int red = (0xFF & (foreColorARGB >> 16)); final boolean bold = (effect & (TextStyle.CHARACTER_ATTRIBUTE_BOLD | TextStyle.CHARACTER_ATTRIBUTE_BLINK)) != 0;
int green = (0xFF & (foreColorARGB >> 8)); final boolean underline = (effect & TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE) != 0;
int blue = (0xFF & foreColorARGB); final boolean italic = (effect & TextStyle.CHARACTER_ATTRIBUTE_ITALIC) != 0;
// Dim color handling used by libvte which in turn took it from xterm final boolean strikeThrough = (effect & TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH) != 0;
// (https://bug735245.bugzilla-attachments.gnome.org/attachment.cgi?id=284267): final boolean dim = (effect & TextStyle.CHARACTER_ATTRIBUTE_DIM) != 0;
red = red * 2 / 3;
green = green * 2 / 3;
blue = blue * 2 / 3;
foreColorARGB = 0xFF000000 + (red << 16) + (green << 8) + blue;
}
mTextPaint.setFakeBoldText(bold); // Let bold have bright colors if applicable (one of the first 8):
mTextPaint.setUnderlineText(underline); if (bold && foreColorIndex >= 0 && foreColorIndex < 8) foreColor = palette[foreColorIndex + 8];
mTextPaint.setTextSkewX(italic ? -0.35f : 0.f);
mTextPaint.setStrikeThruText(strikeThrough);
mTextPaint.setColor(foreColorARGB);
// The text alignment is the default Paint.Align.LEFT. if (dim) {
canvas.drawText(text, startCharIndex, runWidthChars, left, y - mFontLineSpacingAndAscent, mTextPaint); int red = (0xFF & (foreColor >> 16));
} int green = (0xFF & (foreColor >> 8));
int blue = (0xFF & foreColor);
// Dim color handling used by libvte which in turn took it from xterm
// (https://bug735245.bugzilla-attachments.gnome.org/attachment.cgi?id=284267):
red = red * 2 / 3;
green = green * 2 / 3;
blue = blue * 2 / 3;
foreColor = 0xFF000000 + (red << 16) + (green << 8) + blue;
}
if (savedMatrix) canvas.restore(); mTextPaint.setFakeBoldText(bold);
} mTextPaint.setUnderlineText(underline);
mTextPaint.setTextSkewX(italic ? -0.35f : 0.f);
mTextPaint.setStrikeThruText(strikeThrough);
mTextPaint.setColor(foreColor);
// The text alignment is the default Paint.Align.LEFT.
canvas.drawText(text, startCharIndex, runWidthChars, left, y - mFontLineSpacingAndAscent, mTextPaint);
}
if (savedMatrix) canvas.restore();
}
} }

File diff suppressed because it is too large Load Diff

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 := arm64-v8a 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

@@ -17,203 +17,198 @@
static int throw_runtime_exception(JNIEnv* env, char const* message) static int throw_runtime_exception(JNIEnv* env, char const* message)
{ {
jclass exClass = (*env)->FindClass(env, "java/lang/RuntimeException"); jclass exClass = (*env)->FindClass(env, "java/lang/RuntimeException");
(*env)->ThrowNew(env, exClass, message); (*env)->ThrowNew(env, exClass, message);
return -1; return -1;
} }
static int create_subprocess(JNIEnv* env, static int create_subprocess(JNIEnv* env,
char const* cmd, char const* cmd,
char const* cwd, char const* cwd,
char* const argv[], char* const argv[],
char** envp, char** envp,
int* pProcessId, int* pProcessId,
jint rows, jint rows,
jint columns) 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");
#ifdef LACKS_PTSNAME_R #ifdef LACKS_PTSNAME_R
char* devname; char* devname;
#else #else
char devname[64]; char devname[64];
#endif #endif
if (grantpt(ptm) || unlockpt(ptm) || if (grantpt(ptm) || unlockpt(ptm) ||
#ifdef LACKS_PTSNAME_R #ifdef LACKS_PTSNAME_R
(devname = ptsname(ptm)) == NULL (devname = ptsname(ptm)) == NULL
#else #else
ptsname_r(ptm, devname, sizeof(devname)) ptsname_r(ptm, devname, sizeof(devname))
#endif #endif
) { ) {
return throw_runtime_exception(env, "Cannot grantpt()/unlockpt()/ptsname_r() on /dev/ptmx"); return throw_runtime_exception(env, "Cannot grantpt()/unlockpt()/ptsname_r() on /dev/ptmx");
}
// Enable UTF-8 mode and disable flow control to prevent Ctrl+S from locking up the display.
struct termios tios;
tcgetattr(ptm, &tios);
tios.c_iflag |= IUTF8;
tios.c_iflag &= ~(IXON | IXOFF);
tcsetattr(ptm, TCSANOW, &tios);
/** Set initial winsize. */
struct winsize sz = { .ws_row = rows, .ws_col = columns };
ioctl(ptm, TIOCSWINSZ, &sz);
pid_t pid = fork();
if (pid < 0) {
return throw_runtime_exception(env, "Fork failed");
} else if (pid > 0) {
*pProcessId = (int) pid;
return ptm;
} else {
// Clear signals which the Android java process may have blocked:
sigset_t signals_to_unblock;
sigfillset(&signals_to_unblock);
sigprocmask(SIG_UNBLOCK, &signals_to_unblock, 0);
close(ptm);
setsid();
int pts = open(devname, O_RDWR);
if (pts < 0) exit(-1);
dup2(pts, 0);
dup2(pts, 1);
dup2(pts, 2);
DIR* self_dir = opendir("/proc/self/fd");
if (self_dir != NULL) {
int self_dir_fd = dirfd(self_dir);
struct dirent* entry;
while ((entry = readdir(self_dir)) != NULL) {
int fd = atoi(entry->d_name);
if(fd > 2 && fd != self_dir_fd) close(fd);
}
closedir(self_dir);
} }
// Enable UTF-8 mode and disable flow control to prevent Ctrl+S from locking up the display. clearenv();
struct termios tios; if (envp) for (; *envp; ++envp) putenv(*envp);
tcgetattr(ptm, &tios);
tios.c_iflag |= IUTF8;
tios.c_iflag &= ~(IXON | IXOFF);
tcsetattr(ptm, TCSANOW, &tios);
/** Set initial winsize. */ if (chdir(cwd) != 0) {
struct winsize sz = { .ws_row = rows, .ws_col = columns }; char* error_message;
ioctl(ptm, TIOCSWINSZ, &sz); // No need to free asprintf()-allocated memory since doing execvp() or exit() below.
if (asprintf(&error_message, "chdir(\"%s\")", cwd) == -1) error_message = "chdir()";
pid_t pid = fork(); perror(error_message);
if (pid < 0) { fflush(stderr);
return throw_runtime_exception(env, "Fork failed");
} else if (pid > 0) {
*pProcessId = (int) pid;
return ptm;
} else {
// Clear signals which the Android java process may have blocked:
sigset_t signals_to_unblock;
sigfillset(&signals_to_unblock);
sigprocmask(SIG_UNBLOCK, &signals_to_unblock, 0);
close(ptm);
setsid();
int pts = open(devname, O_RDWR);
if (pts < 0) exit(-1);
dup2(pts, 0);
dup2(pts, 1);
dup2(pts, 2);
DIR* self_dir = opendir("/proc/self/fd");
if (self_dir != NULL) {
int self_dir_fd = dirfd(self_dir);
struct dirent* entry;
while ((entry = readdir(self_dir)) != NULL) {
int fd = atoi(entry->d_name);
if(fd > 2 && fd != self_dir_fd) close(fd);
}
closedir(self_dir);
}
clearenv();
if (envp) for (; *envp; ++envp) putenv(*envp);
if (chdir(cwd) != 0) {
char* error_message;
// No need to free asprintf()-allocated memory since doing execvp() or exit() below.
if (asprintf(&error_message, "chdir(\"%s\")", cwd) == -1) error_message = "chdir()";
perror(error_message);
fflush(stderr);
}
execvp(cmd, argv);
// Show terminal output about failing exec() call:
char* error_message;
if (asprintf(&error_message, "exec(\"%s\")", cmd) == -1) error_message = "exec()";
perror(error_message);
_exit(1);
} }
execvp(cmd, argv);
// Show terminal output about failing exec() call:
char* error_message;
if (asprintf(&error_message, "exec(\"%s\")", cmd) == -1) error_message = "exec()";
perror(error_message);
_exit(1);
}
} }
JNIEXPORT jint JNICALL Java_com_termux_terminal_JNI_createSubprocess( JNIEXPORT jint JNICALL Java_com_termux_terminal_JNI_createSubprocess(
JNIEnv* env, JNIEnv* env,
jclass TERMUX_UNUSED(clazz), jclass TERMUX_UNUSED(clazz),
jstring cmd, jstring cmd,
jstring cwd, jstring cwd,
jobjectArray args, jobjectArray args,
jobjectArray envVars, jobjectArray envVars,
jintArray processIdArray, jintArray processIdArray,
jint rows, jint rows,
jint columns) jint columns)
{ {
jsize size = args ? (*env)->GetArrayLength(env, args) : 0; jsize size = args ? (*env)->GetArrayLength(env, args) : 0;
char** argv = NULL; char** argv = NULL;
if (size > 0) { if (size > 0) {
argv = (char**) malloc((size + 1) * sizeof(char*)); argv = (char**) malloc((size + 1) * sizeof(char*));
if (!argv) return throw_runtime_exception(env, "Couldn't allocate argv array"); if (!argv) return throw_runtime_exception(env, "Couldn't allocate argv array");
for (int i = 0; i < size; ++i) { for (int i = 0; i < size; ++i) {
jstring arg_java_string = (jstring) (*env)->GetObjectArrayElement(env, args, i); jstring arg_java_string = (jstring) (*env)->GetObjectArrayElement(env, args, i);
char const* arg_utf8 = (*env)->GetStringUTFChars(env, arg_java_string, NULL); char const* arg_utf8 = (*env)->GetStringUTFChars(env, arg_java_string, NULL);
if (!arg_utf8) return throw_runtime_exception(env, "GetStringUTFChars() failed for argv"); if (!arg_utf8) return throw_runtime_exception(env, "GetStringUTFChars() failed for argv");
argv[i] = strdup(arg_utf8); argv[i] = strdup(arg_utf8);
(*env)->ReleaseStringUTFChars(env, arg_java_string, arg_utf8); (*env)->ReleaseStringUTFChars(env, arg_java_string, arg_utf8);
}
argv[size] = NULL;
} }
argv[size] = NULL;
}
size = envVars ? (*env)->GetArrayLength(env, envVars) : 0; size = envVars ? (*env)->GetArrayLength(env, envVars) : 0;
char** envp = NULL; char** envp = NULL;
if (size > 0) { if (size > 0) {
envp = (char**) malloc((size + 1) * sizeof(char *)); envp = (char**) malloc((size + 1) * sizeof(char *));
if (!envp) return throw_runtime_exception(env, "malloc() for envp array failed"); if (!envp) return throw_runtime_exception(env, "malloc() for envp array failed");
for (int i = 0; i < size; ++i) { for (int i = 0; i < size; ++i) {
jstring env_java_string = (jstring) (*env)->GetObjectArrayElement(env, envVars, i); jstring env_java_string = (jstring) (*env)->GetObjectArrayElement(env, envVars, i);
char const* env_utf8 = (*env)->GetStringUTFChars(env, env_java_string, 0); char const* env_utf8 = (*env)->GetStringUTFChars(env, env_java_string, 0);
if (!env_utf8) return throw_runtime_exception(env, "GetStringUTFChars() failed for env"); if (!env_utf8) return throw_runtime_exception(env, "GetStringUTFChars() failed for env");
envp[i] = strdup(env_utf8); envp[i] = strdup(env_utf8);
(*env)->ReleaseStringUTFChars(env, env_java_string, env_utf8); (*env)->ReleaseStringUTFChars(env, env_java_string, env_utf8);
}
envp[size] = NULL;
} }
envp[size] = NULL;
}
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, rows, columns); 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);
if (argv) { if (argv) {
for (char** tmp = argv; *tmp; ++tmp) free(*tmp); for (char** tmp = argv; *tmp; ++tmp) free(*tmp);
free(argv); free(argv);
} }
if (envp) { if (envp) {
for (char** tmp = envp; *tmp; ++tmp) free(*tmp); for (char** tmp = envp; *tmp; ++tmp) free(*tmp);
free(envp); free(envp);
} }
int* pProcId = (int*) (*env)->GetPrimitiveArrayCritical(env, processIdArray, NULL); int* pProcId = (int*) (*env)->GetPrimitiveArrayCritical(env, processIdArray, NULL);
if (!pProcId) return throw_runtime_exception(env, "JNI call GetPrimitiveArrayCritical(processIdArray, &isCopy) failed"); if (!pProcId) return throw_runtime_exception(env, "JNI call GetPrimitiveArrayCritical(processIdArray, &isCopy) failed");
*pProcId = procId; *pProcId = procId;
(*env)->ReleasePrimitiveArrayCritical(env, processIdArray, pProcId, 0); (*env)->ReleasePrimitiveArrayCritical(env, processIdArray, pProcId, 0);
return ptm; return ptm;
} }
JNIEXPORT void JNICALL Java_com_termux_terminal_JNI_setPtyWindowSize(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint fd, jint rows, jint cols) JNIEXPORT void JNICALL Java_com_termux_terminal_JNI_setPtyWindowSize(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint fd, jint rows, jint cols)
{ {
struct winsize sz = { .ws_row = rows, .ws_col = cols }; struct winsize sz = { .ws_row = rows, .ws_col = cols };
ioctl(fd, TIOCSWINSZ, &sz); ioctl(fd, TIOCSWINSZ, &sz);
} }
JNIEXPORT void JNICALL Java_com_termux_terminal_JNI_setPtyUTF8Mode(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint fd) JNIEXPORT void JNICALL Java_com_termux_terminal_JNI_setPtyUTF8Mode(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint fd)
{ {
struct termios tios; struct termios tios;
tcgetattr(fd, &tios); tcgetattr(fd, &tios);
if ((tios.c_iflag & IUTF8) == 0) { if ((tios.c_iflag & IUTF8) == 0) {
tios.c_iflag |= IUTF8; tios.c_iflag |= IUTF8;
tcsetattr(fd, TCSANOW, &tios); tcsetattr(fd, TCSANOW, &tios);
} }
} }
JNIEXPORT int JNICALL Java_com_termux_terminal_JNI_waitFor(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint pid) JNIEXPORT int JNICALL Java_com_termux_terminal_JNI_waitFor(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint pid)
{ {
int status; int status;
waitpid(pid, &status, 0); waitpid(pid, &status, 0);
if (WIFEXITED(status)) { if (WIFEXITED(status)) {
return WEXITSTATUS(status); return WEXITSTATUS(status);
} else if (WIFSIGNALED(status)) { } else if (WIFSIGNALED(status)) {
return -WTERMSIG(status); return -WTERMSIG(status);
} else { } else {
// Should never happen - waitpid(2) says "One of the first three macros will evaluate to a non-zero (true) value". // Should never happen - waitpid(2) says "One of the first three macros will evaluate to a non-zero (true) value".
return 0; return 0;
} }
}
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,58 +1,75 @@
<com.termux.drawer.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/drawer_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" > android:layout_height="match_parent"
android:orientation="vertical">
<com.termux.view.TerminalView <android.support.v4.widget.DrawerLayout
android:id="@+id/terminal_view" 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:focusableInTouchMode="true" android:layout_above="@+id/viewpager"
android:scrollbarThumbVertical="@drawable/terminal_scroll_shape" android:layout_height="match_parent">
android:scrollbars="vertical" />
<LinearLayout <com.termux.view.TerminalView
android:id="@+id/left_drawer" android:id="@+id/terminal_view"
android:layout_width="240dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:background="@android:color/white"
android:choiceMode="singleChoice"
android:divider="@android:color/transparent"
android:dividerHeight="0dp"
android:orientation="vertical" >
<ListView
android:id="@+id/left_drawer_list"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="match_parent"
android:layout_gravity="top" android:focusableInTouchMode="true"
android:layout_weight="1" android:scrollbarThumbVertical="@drawable/terminal_scroll_shape"
android:choiceMode="singleChoice" android:scrollbars="vertical" />
android:longClickable="true" />
<LinearLayout <LinearLayout
style="?android:attr/buttonBarStyle" android:id="@+id/left_drawer"
android:layout_width="match_parent" android:layout_width="240dp"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:orientation="horizontal" > android:layout_gravity="start"
android:background="@android:color/white"
android:choiceMode="singleChoice"
android:divider="@android:color/transparent"
android:dividerHeight="0dp"
android:descendantFocusability="blocksDescendants"
android:orientation="vertical">
<Button <ListView
android:id="@+id/toggle_keyboard_button" android:id="@+id/left_drawer_list"
style="?android:attr/buttonBarButtonStyle" android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_gravity="top"
android:layout_weight="1"
android:choiceMode="singleChoice"
android:longClickable="true" />
<LinearLayout
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:layout_weight="1" android:orientation="horizontal">
android:text="@string/toggle_soft_keyboard" />
<Button <Button
android:id="@+id/new_session_button" android:id="@+id/toggle_keyboard_button"
style="?android:attr/buttonBarButtonStyle" style="?android:attr/buttonBarButtonStyle"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:text="@string/new_session" /> android:text="@string/toggle_soft_keyboard" />
<Button
android:id="@+id/new_session_button"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/new_session" />
</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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -15,6 +15,12 @@ public class WcWidthTest extends TestCase {
} }
} }
public void testSomeWidthOne() {
assertWidthIs(1, 'å');
assertWidthIs(1, 'ä');
assertWidthIs(1, 'ö');
}
public void testSomeWide() { public void testSomeWide() {
assertWidthIs(2, ''); assertWidthIs(2, '');
assertWidthIs(2, ''); assertWidthIs(2, '');
@@ -37,13 +43,21 @@ public class WcWidthTest extends TestCase {
} }
public void testCombining() { public void testCombining() {
assertWidthIs(0, 0x0302); assertWidthIs(0, 0x0302);
assertWidthIs(0, 0x0308); assertWidthIs(0, 0x0308);
assertWidthIs(0, 0x2060); }
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);
} }
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,10 +5,7 @@ buildscript {
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:2.0.0' classpath 'com.android.tools.build:gradle:2.1.3'
// NOTE: Do not place your application dependencies here; they belong
// 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 @@
#Tue Mar 15 00:24:33 CET 2016 #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.12-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