Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f24628fd2 | ||
|
|
debbe44809 | ||
|
|
b2ff0e4051 | ||
|
|
9e7029b76a | ||
|
|
51370799c7 | ||
|
|
0910844896 | ||
|
|
f33ebf810f | ||
|
|
930029b5d2 | ||
|
|
33def928cf | ||
|
|
fc04a93990 | ||
|
|
8d302aa9fe | ||
|
|
2224d917a3 | ||
|
|
664ec43f94 | ||
|
|
6cf742460c | ||
|
|
72981fb981 | ||
|
|
2c5534e2c1 | ||
|
|
5b32540635 | ||
|
|
db3ff7b24a | ||
|
|
5f71e3e73a | ||
|
|
3e04ea4cb0 | ||
|
|
0af823607a | ||
|
|
4d9c0c315e | ||
|
|
9c32935ca2 | ||
|
|
669c336e2f | ||
|
|
35842cf4a6 | ||
|
|
b086270a5a | ||
|
|
f39f06a540 | ||
|
|
58440bc88d | ||
|
|
9f438e2912 | ||
|
|
f794bfcadc | ||
|
|
b6d7831646 | ||
|
|
d212198e30 | ||
|
|
c2843897ac | ||
|
|
c4c4912a7e | ||
|
|
38a3319ca2 | ||
|
|
7e13b8aa2e | ||
|
|
6dca19ae00 | ||
|
|
2659c06c5d | ||
|
|
9b7c7102b2 | ||
|
|
1819087ca0 | ||
|
|
366a61f052 | ||
|
|
6e224cabcf | ||
|
|
8c8fa96133 | ||
|
|
537f2ed97e | ||
|
|
0e23315c41 | ||
|
|
2cde986419 | ||
|
|
d28939810c | ||
|
|
93724b7aa6 | ||
|
|
5d06f040e8 | ||
|
|
ed9afa082a | ||
|
|
9703bd31ad | ||
|
|
9749f25eba | ||
|
|
453b838b24 | ||
|
|
9fdf2a49fd | ||
|
|
3270506bff | ||
|
|
a240f4cf45 | ||
|
|
4647beb0d2 | ||
|
|
d92e806461 | ||
|
|
b75cf0bb84 | ||
|
|
f928efed4e | ||
|
|
36db64d585 | ||
|
|
b8f0430699 |
20
.cirrus.yml
20
.cirrus.yml
@@ -1,20 +0,0 @@
|
|||||||
container:
|
|
||||||
image: cirrusci/android-sdk:28
|
|
||||||
cpu: 2
|
|
||||||
memory: 8G
|
|
||||||
|
|
||||||
task:
|
|
||||||
name: tests
|
|
||||||
script: ./gradlew test
|
|
||||||
|
|
||||||
task:
|
|
||||||
name: debug-build
|
|
||||||
|
|
||||||
depends_on:
|
|
||||||
- tests
|
|
||||||
|
|
||||||
script: |
|
|
||||||
./gradlew assembleDebug
|
|
||||||
|
|
||||||
output_artifacts:
|
|
||||||
path: "./app/build/outputs/apk/debug/*.apk"
|
|
||||||
27
.github/ISSUE_TEMPLATE/bug_report.md
vendored
27
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,20 +1,35 @@
|
|||||||
---
|
---
|
||||||
name: Bug report
|
name: Bug report
|
||||||
about: Create a report to help us improve termux-app
|
about: Create a report to help us improve Termux application
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<!-- Important note: Refusing to provide needed information may result in issue closing. If you are having problems with a package in termux then a bug report should be filed in the termux-packages repo: https://github.com/termux/termux-packages -->
|
<!--
|
||||||
|
IMPORTANT:
|
||||||
|
|
||||||
|
1. Support of Android 5.x - 6.x is finished.
|
||||||
|
2. Fill the template AFTER comments.
|
||||||
|
-->
|
||||||
|
|
||||||
**Problem description**
|
**Problem description**
|
||||||
A clear and concise description of what the problem with the termux app is. You may post screenshots in addition to description.
|
<!--
|
||||||
|
A clear and concise description of what the problem is.
|
||||||
|
You may post screenshots in addition to description.
|
||||||
|
-->
|
||||||
|
|
||||||
**Steps to reproduce**
|
**Steps to reproduce**
|
||||||
Please post all steps that are needed to reproduce the issue.
|
<!--
|
||||||
|
Steps to reproduce the behavior. Please post all necessary
|
||||||
|
commands that are needed to reproduce the issue.
|
||||||
|
-->
|
||||||
|
|
||||||
**Expected behavior**
|
**Expected behavior**
|
||||||
|
<!--
|
||||||
A clear and concise description of what you expected to happen.
|
A clear and concise description of what you expected to happen.
|
||||||
|
-->
|
||||||
|
|
||||||
**Additional information**
|
**Additional information**
|
||||||
Post output of command `termux-info`.
|
|
||||||
If you are rooted or have access to adb then capture a logcat with `logcat -d "*:W"`, from a adb or root shell.
|
* Termux application version:
|
||||||
|
* Android OS version:
|
||||||
|
* Device model:
|
||||||
|
|||||||
14
.github/ISSUE_TEMPLATE/feature_request.md
vendored
14
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,12 +1,22 @@
|
|||||||
---
|
---
|
||||||
name: Feature request
|
name: Feature request
|
||||||
about: Suggest a new feature in termux-app
|
about: Suggest a new feature for Termux application
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
<!--
|
||||||
|
IMPORTANT:
|
||||||
|
|
||||||
|
1. Support of Android 5.x - 6.x is finished.
|
||||||
|
2. Fill the template AFTER comments.
|
||||||
|
-->
|
||||||
|
|
||||||
**Feature description**
|
**Feature description**
|
||||||
|
<!--
|
||||||
Describe the feature and why you want it.
|
Describe the feature and why you want it.
|
||||||
|
-->
|
||||||
|
|
||||||
**Reference implementation**
|
**Reference implementation**
|
||||||
|
|
||||||
Does another app/terminal emulator have this feature?
|
Does another app/terminal emulator have this feature?
|
||||||
Provide links to more background information
|
Provide links to more background information.
|
||||||
|
|||||||
24
.github/workflows/debug_build.yml
vendored
Normal file
24
.github/workflows/debug_build.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
name: Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Clone repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
./gradlew assembleDebug
|
||||||
|
- name: Store generated APK file
|
||||||
|
uses: actions/upload-artifact@v1
|
||||||
|
with:
|
||||||
|
name: termux-app
|
||||||
|
path: ./app/build/outputs/apk/debug/app-debug.apk
|
||||||
17
.github/workflows/gradle-wrapper-validation.yml
vendored
Normal file
17
.github/workflows/gradle-wrapper-validation.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
name: "Validate Gradle Wrapper"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validation:
|
||||||
|
name: "Validation"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: gradle/wrapper-validation-action@v1
|
||||||
19
.github/workflows/run_tests.yml
vendored
Normal file
19
.github/workflows/run_tests.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
name: Unit tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
testing:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Clone repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Execute tests
|
||||||
|
run: |
|
||||||
|
./gradlew test
|
||||||
10
README.md
10
README.md
@@ -1,6 +1,7 @@
|
|||||||
# Termux application
|
# Termux application
|
||||||
|
|
||||||
[](https://cirrus-ci.com/termux/termux-app)
|
[](https://github.com/termux/termux-app/actions)
|
||||||
|
[](https://github.com/termux/termux-app/actions)
|
||||||
[](https://gitter.im/termux/termux)
|
[](https://gitter.im/termux/termux)
|
||||||
|
|
||||||
[Termux](https://termux.com) is an Android terminal application and Linux environment.
|
[Termux](https://termux.com) is an Android terminal application and Linux environment.
|
||||||
@@ -21,9 +22,10 @@ Termux:Widget application can be obtained from:
|
|||||||
- [F-Droid](https://f-droid.org/en/packages/com.termux/)
|
- [F-Droid](https://f-droid.org/en/packages/com.termux/)
|
||||||
- [Kali Nethunter Store](https://store.nethunter.com/en/packages/com.termux/)
|
- [Kali Nethunter Store](https://store.nethunter.com/en/packages/com.termux/)
|
||||||
|
|
||||||
Additionally we offer development builds for those who want to try out latest
|
Additionally we provide per-commit debug builds for those who want to try
|
||||||
features ready to be included in future versions. Such build can be obtained
|
out the latest features or test their pull request. This build can be obtained
|
||||||
directly from [Cirrus CI artifacts](https://api.cirrus-ci.com/v1/artifact/github/termux/termux-app/debug-build/output/app/build/outputs/apk/debug/app-debug.apk).
|
from one of the workflow runs listed on [Github Actions](https://github.com/termux/termux-app/actions)
|
||||||
|
page.
|
||||||
|
|
||||||
Signature keys of all offered builds are different. Before you switch the
|
Signature keys of all offered builds are different. Before you switch the
|
||||||
installation source, you will have to uninstall the Termux application and
|
installation source, you will have to uninstall the Termux application and
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ plugins {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 28
|
compileSdkVersion 28
|
||||||
|
ndkVersion '21.3.6528147'
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "androidx.annotation:annotation:1.1.0"
|
implementation "androidx.annotation:annotation:1.1.0"
|
||||||
@@ -16,8 +17,8 @@ android {
|
|||||||
applicationId "com.termux"
|
applicationId "com.termux"
|
||||||
minSdkVersion 24
|
minSdkVersion 24
|
||||||
targetSdkVersion 28
|
targetSdkVersion 28
|
||||||
versionCode 88
|
versionCode 96
|
||||||
versionName "0.88"
|
versionName "0.96"
|
||||||
|
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
ndkBuild {
|
ndkBuild {
|
||||||
@@ -72,7 +73,7 @@ android {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testImplementation 'junit:junit:4.13'
|
testImplementation 'junit:junit:4.13'
|
||||||
testImplementation 'org.robolectric:robolectric:4.3'
|
testImplementation 'org.robolectric:robolectric:4.3.1'
|
||||||
}
|
}
|
||||||
|
|
||||||
task versionName {
|
task versionName {
|
||||||
@@ -132,11 +133,11 @@ clean {
|
|||||||
|
|
||||||
task downloadBootstraps(){
|
task downloadBootstraps(){
|
||||||
doLast {
|
doLast {
|
||||||
def version = 18
|
def version = 26
|
||||||
downloadBootstrap("aarch64", "1a4c08a696d452b58f69102428239ec0c07521c0ca9f48b23ef70ae0e5e3d4f8", version)
|
downloadBootstrap("aarch64", "c7830de73aa437933aef1957bdea29640afa34ca292fd7e27a6bdd1c9271bfe3", version)
|
||||||
downloadBootstrap("arm", "bff11f2c7e9c1055a22fc5f20bb7507b75f6034e0f5d591ec6725b3407981b85", version)
|
downloadBootstrap("arm", "d6c250d853ef235da6f64ee946018cfaca1ca5a416d2ce0bdd82d97c35972bc5", version)
|
||||||
downloadBootstrap("i686", "6fb93020db2807337d82a1537e24612400cacbd10cf4bccaeb0714d51e653da1", version)
|
downloadBootstrap("i686", "d1b272e022a6bbee9d6bff024f1e42d26c58d0d3dfe64ca68846b94b40db352d", version)
|
||||||
downloadBootstrap("x86_64", "a6067e5decc486dcad190c1ed9e15366c798e5e7d9b9b9ee6b4b8231290524c3", version)
|
downloadBootstrap("x86_64", "866793074541aa1ead0927affc8f1b5279a2cf9d683688a8379e6090754e077e", version)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,4 +146,3 @@ afterEvaluate {
|
|||||||
variant.javaCompileProvider.get().dependsOn(downloadBootstraps)
|
variant.javaCompileProvider.get().dependsOn(downloadBootstraps)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,12 @@
|
|||||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||||
|
|
||||||
|
<permission android:name="com.termux.permission.RUN_COMMAND"
|
||||||
|
android:label="@string/run_command_permission_label"
|
||||||
|
android:description="@string/run_command_permission_description"
|
||||||
|
android:icon="@drawable/ic_launcher"
|
||||||
|
android:protectionLevel="dangerous" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
@@ -31,11 +37,11 @@
|
|||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.termux.app.TermuxActivity"
|
android:name="com.termux.app.TermuxActivity"
|
||||||
android:label="@string/application_name"
|
android:label="@string/application_name"
|
||||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
android:configChanges="orientation|screenSize|smallestScreenSize|density|screenLayout|uiMode|keyboard|keyboardHidden|navigation"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:windowSoftInputMode="adjustResize|stateAlwaysVisible" >
|
android:windowSoftInputMode="adjustResize|stateAlwaysVisible" >
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
@@ -79,6 +85,7 @@
|
|||||||
<action android:name="android.intent.action.VIEW"/>
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<data android:mimeType="text/*" />
|
<data android:mimeType="text/*" />
|
||||||
|
<data android:mimeType="application/*log*" />
|
||||||
<data android:mimeType="application/json" />
|
<data android:mimeType="application/json" />
|
||||||
<data android:mimeType="application/*xml*" />
|
<data android:mimeType="application/*xml*" />
|
||||||
<data android:mimeType="application/*latex*" />
|
<data android:mimeType="application/*latex*" />
|
||||||
@@ -113,6 +120,15 @@
|
|||||||
android:name="com.termux.app.TermuxService"
|
android:name="com.termux.app.TermuxService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".app.RunCommandService"
|
||||||
|
android:exported="true"
|
||||||
|
android:permission="com.termux.permission.RUN_COMMAND" >
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="com.termux.RUN_COMMAND" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
|
||||||
<receiver android:name=".app.TermuxOpenReceiver" />
|
<receiver android:name=".app.TermuxOpenReceiver" />
|
||||||
|
|
||||||
<provider android:authorities="com.termux.files"
|
<provider android:authorities="com.termux.files"
|
||||||
@@ -120,8 +136,8 @@
|
|||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:grantUriPermissions="true"
|
android:grantUriPermissions="true"
|
||||||
android:name="com.termux.app.TermuxOpenReceiver$ContentProvider" />
|
android:name="com.termux.app.TermuxOpenReceiver$ContentProvider" />
|
||||||
|
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
|
||||||
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
|
<meta-data android:name="com.samsung.android.multidisplay.keep_process_alive" android:value="true"/>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ public final class BackgroundJob {
|
|||||||
List<String> environment = new ArrayList<>();
|
List<String> environment = new ArrayList<>();
|
||||||
|
|
||||||
environment.add("TERM=xterm-256color");
|
environment.add("TERM=xterm-256color");
|
||||||
|
environment.add("COLORTERM=truecolor");
|
||||||
environment.add("HOME=" + TermuxService.HOME_PATH);
|
environment.add("HOME=" + TermuxService.HOME_PATH);
|
||||||
environment.add("PREFIX=" + TermuxService.PREFIX_PATH);
|
environment.add("PREFIX=" + TermuxService.PREFIX_PATH);
|
||||||
environment.add("BOOTCLASSPATH=" + System.getenv("BOOTCLASSPATH"));
|
environment.add("BOOTCLASSPATH=" + System.getenv("BOOTCLASSPATH"));
|
||||||
@@ -147,16 +148,18 @@ public final class BackgroundJob {
|
|||||||
// 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.
|
||||||
environment.add("EXTERNAL_STORAGE=" + System.getenv("EXTERNAL_STORAGE"));
|
environment.add("EXTERNAL_STORAGE=" + System.getenv("EXTERNAL_STORAGE"));
|
||||||
// ANDROID_RUNTIME_ROOT and ANDROID_TZDATA_ROOT are required for `am` to run on Android Q
|
|
||||||
|
// These variables are needed if running on Android 10 and higher.
|
||||||
|
addToEnvIfPresent(environment, "ANDROID_ART_ROOT");
|
||||||
|
addToEnvIfPresent(environment, "DEX2OATBOOTCLASSPATH");
|
||||||
|
addToEnvIfPresent(environment, "ANDROID_I18N_ROOT");
|
||||||
addToEnvIfPresent(environment, "ANDROID_RUNTIME_ROOT");
|
addToEnvIfPresent(environment, "ANDROID_RUNTIME_ROOT");
|
||||||
addToEnvIfPresent(environment, "ANDROID_TZDATA_ROOT");
|
addToEnvIfPresent(environment, "ANDROID_TZDATA_ROOT");
|
||||||
|
|
||||||
if (failSafe) {
|
if (failSafe) {
|
||||||
// Keep the default path so that system binaries can be used in the failsafe session.
|
// Keep the default path so that system binaries can be used in the failsafe session.
|
||||||
environment.add("PATH= " + System.getenv("PATH"));
|
environment.add("PATH= " + System.getenv("PATH"));
|
||||||
} else {
|
} else {
|
||||||
if (shouldAddLdLibraryPath()) {
|
|
||||||
environment.add("LD_LIBRARY_PATH=" + TermuxService.PREFIX_PATH + "/lib");
|
|
||||||
}
|
|
||||||
environment.add("LANG=en_US.UTF-8");
|
environment.add("LANG=en_US.UTF-8");
|
||||||
environment.add("PATH=" + TermuxService.PREFIX_PATH + "/bin:" + TermuxService.PREFIX_PATH + "/bin/applets");
|
environment.add("PATH=" + TermuxService.PREFIX_PATH + "/bin:" + TermuxService.PREFIX_PATH + "/bin/applets");
|
||||||
environment.add("PWD=" + cwd);
|
environment.add("PWD=" + cwd);
|
||||||
@@ -166,20 +169,6 @@ public final class BackgroundJob {
|
|||||||
return environment.toArray(new String[0]);
|
return environment.toArray(new String[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean shouldAddLdLibraryPath() {
|
|
||||||
try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(TermuxService.PREFIX_PATH + "/etc/apt/sources.list")))) {
|
|
||||||
String line;
|
|
||||||
while ((line = in.readLine()) != null) {
|
|
||||||
if (!line.startsWith("#") && line.contains("//termux.net stable")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.e(LOG_TAG, "Error trying to read sources.list", e);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int getPid(Process p) {
|
public static int getPid(Process p) {
|
||||||
try {
|
try {
|
||||||
Field f = p.getClass().getDeclaredField("pid");
|
Field f = p.getClass().getDeclaredField("pid");
|
||||||
|
|||||||
338
app/src/main/java/com/termux/app/ExtraKeysInfos.java
Normal file
338
app/src/main/java/com/termux/app/ExtraKeysInfos.java
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
package com.termux.app;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class ExtraKeysInfos {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matrix of buttons displayed
|
||||||
|
*/
|
||||||
|
private ExtraKeyButton[][] buttons;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This corresponds to one of the CharMapDisplay below
|
||||||
|
*/
|
||||||
|
private String style = "default";
|
||||||
|
|
||||||
|
public ExtraKeysInfos(String propertiesInfo, String style) throws JSONException {
|
||||||
|
this.style = style;
|
||||||
|
|
||||||
|
// Convert String propertiesInfo to Array of Arrays
|
||||||
|
JSONArray arr = new JSONArray(propertiesInfo);
|
||||||
|
Object[][] matrix = new Object[arr.length()][];
|
||||||
|
for (int i = 0; i < arr.length(); i++) {
|
||||||
|
JSONArray line = arr.getJSONArray(i);
|
||||||
|
matrix[i] = new Object[line.length()];
|
||||||
|
for (int j = 0; j < line.length(); j++) {
|
||||||
|
matrix[i][j] = line.get(j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert matrix to buttons
|
||||||
|
this.buttons = new ExtraKeyButton[matrix.length][];
|
||||||
|
for (int i = 0; i < matrix.length; i++) {
|
||||||
|
this.buttons[i] = new ExtraKeyButton[matrix[i].length];
|
||||||
|
for (int j = 0; j < matrix[i].length; j++) {
|
||||||
|
Object key = matrix[i][j];
|
||||||
|
|
||||||
|
JSONObject jobject = normalizeKeyConfig(key);
|
||||||
|
|
||||||
|
ExtraKeyButton button;
|
||||||
|
|
||||||
|
if(! jobject.has("popup")) {
|
||||||
|
// no popup
|
||||||
|
button = new ExtraKeyButton(getSelectedCharMap(), jobject);
|
||||||
|
} else {
|
||||||
|
// a popup
|
||||||
|
JSONObject popupJobject = normalizeKeyConfig(jobject.get("popup"));
|
||||||
|
ExtraKeyButton popup = new ExtraKeyButton(getSelectedCharMap(), popupJobject);
|
||||||
|
button = new ExtraKeyButton(getSelectedCharMap(), jobject, popup);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.buttons[i][j] = button;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "hello" -> {"key": "hello"}
|
||||||
|
*/
|
||||||
|
private static JSONObject normalizeKeyConfig(Object key) throws JSONException {
|
||||||
|
JSONObject jobject;
|
||||||
|
if(key instanceof String) {
|
||||||
|
jobject = new JSONObject();
|
||||||
|
jobject.put("key", key);
|
||||||
|
} else if(key instanceof JSONObject) {
|
||||||
|
jobject = (JSONObject) key;
|
||||||
|
} else {
|
||||||
|
throw new JSONException("An key in the extra-key matrix must be a string or an object");
|
||||||
|
}
|
||||||
|
return jobject;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExtraKeyButton[][] getMatrix() {
|
||||||
|
return buttons;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HashMap that implements Python dict.get(key, default) function.
|
||||||
|
* Default java.util .get(key) is then the same as .get(key, null);
|
||||||
|
*/
|
||||||
|
static class CleverMap<K,V> extends HashMap<K,V> {
|
||||||
|
V get(K key, V defaultValue) {
|
||||||
|
if(containsKey(key))
|
||||||
|
return get(key);
|
||||||
|
else
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class CharDisplayMap extends CleverMap<String, String> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keys are displayed in a natural looking way, like "→" for "RIGHT"
|
||||||
|
*/
|
||||||
|
static final CharDisplayMap classicArrowsDisplay = new CharDisplayMap() {{
|
||||||
|
// classic arrow keys (for ◀ ▶ ▲ ▼ @see arrowVariationDisplay)
|
||||||
|
put("LEFT", "←"); // U+2190 ← LEFTWARDS ARROW
|
||||||
|
put("RIGHT", "→"); // U+2192 → RIGHTWARDS ARROW
|
||||||
|
put("UP", "↑"); // U+2191 ↑ UPWARDS ARROW
|
||||||
|
put("DOWN", "↓"); // U+2193 ↓ DOWNWARDS ARROW
|
||||||
|
}};
|
||||||
|
|
||||||
|
static final CharDisplayMap wellKnownCharactersDisplay = new CharDisplayMap() {{
|
||||||
|
// well known characters // https://en.wikipedia.org/wiki/{Enter_key, Tab_key, Delete_key}
|
||||||
|
put("ENTER", "↲"); // U+21B2 ↲ DOWNWARDS ARROW WITH TIP LEFTWARDS
|
||||||
|
put("TAB", "↹"); // U+21B9 ↹ LEFTWARDS ARROW TO BAR OVER RIGHTWARDS ARROW TO BAR
|
||||||
|
put("BKSP", "⌫"); // U+232B ⌫ ERASE TO THE LEFT sometimes seen and easy to understand
|
||||||
|
put("DEL", "⌦"); // U+2326 ⌦ ERASE TO THE RIGHT not well known but easy to understand
|
||||||
|
put("DRAWER", "☰"); // U+2630 ☰ TRIGRAM FOR HEAVEN not well known but easy to understand
|
||||||
|
put("KEYBOARD", "⌨"); // U+2328 ⌨ KEYBOARD not well known but easy to understand
|
||||||
|
}};
|
||||||
|
|
||||||
|
static final CharDisplayMap lessKnownCharactersDisplay = new CharDisplayMap() {{
|
||||||
|
// https://en.wikipedia.org/wiki/{Home_key, End_key, Page_Up_and_Page_Down_keys}
|
||||||
|
// home key can mean "goto the beginning of line" or "goto first page" depending on context, hence the diagonal
|
||||||
|
put("HOME", "⇱"); // from IEC 9995 // U+21F1 ⇱ NORTH WEST ARROW TO CORNER
|
||||||
|
put("END", "⇲"); // from IEC 9995 // ⇲ // U+21F2 ⇲ SOUTH EAST ARROW TO CORNER
|
||||||
|
put("PGUP", "⇑"); // no ISO character exists, U+21D1 ⇑ UPWARDS DOUBLE ARROW will do the trick
|
||||||
|
put("PGDN", "⇓"); // no ISO character exists, U+21D3 ⇓ DOWNWARDS DOUBLE ARROW will do the trick
|
||||||
|
}};
|
||||||
|
|
||||||
|
static final CharDisplayMap arrowTriangleVariationDisplay = new CharDisplayMap() {{
|
||||||
|
// alternative to classic arrow keys
|
||||||
|
put("LEFT", "◀"); // U+25C0 ◀ BLACK LEFT-POINTING TRIANGLE
|
||||||
|
put("RIGHT", "▶"); // U+25B6 ▶ BLACK RIGHT-POINTING TRIANGLE
|
||||||
|
put("UP", "▲"); // U+25B2 ▲ BLACK UP-POINTING TRIANGLE
|
||||||
|
put("DOWN", "▼"); // U+25BC ▼ BLACK DOWN-POINTING TRIANGLE
|
||||||
|
}};
|
||||||
|
|
||||||
|
static final CharDisplayMap notKnownIsoCharacters = new CharDisplayMap() {{
|
||||||
|
// Control chars that are more clear as text // https://en.wikipedia.org/wiki/{Function_key, Alt_key, Control_key, Esc_key}
|
||||||
|
// put("FN", "FN"); // no ISO character exists
|
||||||
|
put("CTRL", "⎈"); // ISO character "U+2388 ⎈ HELM SYMBOL" is unknown to people and never printed on computers, however "U+25C7 ◇ WHITE DIAMOND" is a nice presentation, and "^" for terminal app and mac is often used
|
||||||
|
put("ALT", "⎇"); // ISO character "U+2387 ⎇ ALTERNATIVE KEY SYMBOL'" is unknown to people and only printed as the Option key "⌥" on Mac computer
|
||||||
|
put("ESC", "⎋"); // ISO character "U+238B ⎋ BROKEN CIRCLE WITH NORTHWEST ARROW" is unknown to people and not often printed on computers
|
||||||
|
}};
|
||||||
|
|
||||||
|
static final CharDisplayMap nicerLookingDisplay = new CharDisplayMap() {{
|
||||||
|
// nicer looking for most cases
|
||||||
|
put("-", "―"); // U+2015 ― HORIZONTAL BAR
|
||||||
|
}};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Multiple maps are available to quickly change
|
||||||
|
* the style of the keys.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Some classic symbols everybody knows
|
||||||
|
*/
|
||||||
|
private static final CharDisplayMap defaultCharDisplay = new CharDisplayMap() {{
|
||||||
|
putAll(classicArrowsDisplay);
|
||||||
|
putAll(wellKnownCharactersDisplay);
|
||||||
|
putAll(nicerLookingDisplay);
|
||||||
|
// all other characters are displayed as themselves
|
||||||
|
}};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Classic symbols and less known symbols
|
||||||
|
*/
|
||||||
|
private static final CharDisplayMap lotsOfArrowsCharDisplay = new CharDisplayMap() {{
|
||||||
|
putAll(classicArrowsDisplay);
|
||||||
|
putAll(wellKnownCharactersDisplay);
|
||||||
|
putAll(lessKnownCharactersDisplay); // NEW
|
||||||
|
putAll(nicerLookingDisplay);
|
||||||
|
}};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only arrows
|
||||||
|
*/
|
||||||
|
private static final CharDisplayMap arrowsOnlyCharDisplay = new CharDisplayMap() {{
|
||||||
|
putAll(classicArrowsDisplay);
|
||||||
|
// putAll(wellKnownCharactersDisplay); // REMOVED
|
||||||
|
// putAll(lessKnownCharactersDisplay); // REMOVED
|
||||||
|
putAll(nicerLookingDisplay);
|
||||||
|
}};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full Iso
|
||||||
|
*/
|
||||||
|
private static final CharDisplayMap fullIsoCharDisplay = new CharDisplayMap() {{
|
||||||
|
putAll(classicArrowsDisplay);
|
||||||
|
putAll(wellKnownCharactersDisplay);
|
||||||
|
putAll(lessKnownCharactersDisplay); // NEW
|
||||||
|
putAll(nicerLookingDisplay);
|
||||||
|
putAll(notKnownIsoCharacters); // NEW
|
||||||
|
}};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Some people might call our keys differently
|
||||||
|
*/
|
||||||
|
static private final CharDisplayMap controlCharsAliases = new CharDisplayMap() {{
|
||||||
|
put("ESCAPE", "ESC");
|
||||||
|
put("CONTROL", "CTRL");
|
||||||
|
put("RETURN", "ENTER"); // Technically different keys, but most applications won't see the difference
|
||||||
|
put("FUNCTION", "FN");
|
||||||
|
// no alias for ALT
|
||||||
|
|
||||||
|
// Directions are sometimes written as first and last letter for brevety
|
||||||
|
put("LT", "LEFT");
|
||||||
|
put("RT", "RIGHT");
|
||||||
|
put("DN", "DOWN");
|
||||||
|
// put("UP", "UP"); well, "UP" is already two letters
|
||||||
|
|
||||||
|
put("PAGEUP", "PGUP");
|
||||||
|
put("PAGE_UP", "PGUP");
|
||||||
|
put("PAGE UP", "PGUP");
|
||||||
|
put("PAGE-UP", "PGUP");
|
||||||
|
|
||||||
|
// no alias for HOME
|
||||||
|
// no alias for END
|
||||||
|
|
||||||
|
put("PAGEDOWN", "PGDN");
|
||||||
|
put("PAGE_DOWN", "PGDN");
|
||||||
|
put("PAGE-DOWN", "PGDN");
|
||||||
|
|
||||||
|
put("DELETE", "DEL");
|
||||||
|
put("BACKSPACE", "BKSP");
|
||||||
|
|
||||||
|
// easier for writing in termux.properties
|
||||||
|
put("BACKSLASH", "\\");
|
||||||
|
put("QUOTE", "\"");
|
||||||
|
put("APOSTROPHE", "'");
|
||||||
|
}};
|
||||||
|
|
||||||
|
CharDisplayMap getSelectedCharMap() {
|
||||||
|
switch (style) {
|
||||||
|
case "arrows-only":
|
||||||
|
return arrowsOnlyCharDisplay;
|
||||||
|
case "arrows-all":
|
||||||
|
return lotsOfArrowsCharDisplay;
|
||||||
|
case "all":
|
||||||
|
return fullIsoCharDisplay;
|
||||||
|
case "none":
|
||||||
|
return new CharDisplayMap();
|
||||||
|
default:
|
||||||
|
return defaultCharDisplay;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies the 'controlCharsAliases' mapping to all the strings in *buttons*
|
||||||
|
* Modifies the array, doesn't return a new one.
|
||||||
|
*/
|
||||||
|
public static String replaceAlias(String key) {
|
||||||
|
return controlCharsAliases.get(key, key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExtraKeyButton {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The key that will be sent to the terminal, either a control character
|
||||||
|
* defined in ExtraKeysView.keyCodesForString (LEFT, RIGHT, PGUP...) or
|
||||||
|
* some text.
|
||||||
|
*/
|
||||||
|
private String key;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the key is a macro, i.e. a sequence of keys separated by space.
|
||||||
|
*/
|
||||||
|
private boolean macro;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The text that will be shown on the button.
|
||||||
|
*/
|
||||||
|
private String display;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The information of the popup (triggered by swipe up).
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
private ExtraKeyButton popup = null;
|
||||||
|
|
||||||
|
public ExtraKeyButton(ExtraKeysInfos.CharDisplayMap charDisplayMap, JSONObject config) throws JSONException {
|
||||||
|
this(charDisplayMap, config, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExtraKeyButton(ExtraKeysInfos.CharDisplayMap charDisplayMap, JSONObject config, ExtraKeyButton popup) throws JSONException {
|
||||||
|
String keyFromConfig = config.optString("key", null);
|
||||||
|
String macroFromConfig = config.optString("macro", null);
|
||||||
|
String[] keys;
|
||||||
|
if (keyFromConfig != null && macroFromConfig != null) {
|
||||||
|
throw new JSONException("Both key and macro can't be set for the same key");
|
||||||
|
} else if (keyFromConfig != null) {
|
||||||
|
keys = new String[]{keyFromConfig};
|
||||||
|
this.macro = false;
|
||||||
|
} else if (macroFromConfig != null) {
|
||||||
|
keys = macroFromConfig.split(" ");
|
||||||
|
this.macro = true;
|
||||||
|
} else {
|
||||||
|
throw new JSONException("All keys have to specify either key or macro");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < keys.length; i++) {
|
||||||
|
keys[i] = ExtraKeysInfos.replaceAlias(keys[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.key = String.join(" ", keys);
|
||||||
|
|
||||||
|
String displayFromConfig = config.optString("display", null);
|
||||||
|
if (displayFromConfig != null) {
|
||||||
|
this.display = displayFromConfig;
|
||||||
|
} else {
|
||||||
|
this.display = Arrays.stream(keys)
|
||||||
|
.map(key -> charDisplayMap.get(key, key))
|
||||||
|
.collect(Collectors.joining(" "));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.popup = popup;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getKey() {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isMacro() {
|
||||||
|
return macro;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplay() {
|
||||||
|
return display;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public ExtraKeyButton getPopup() {
|
||||||
|
return popup;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.termux.app;
|
package com.termux.app;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
@@ -13,19 +14,22 @@ import java.util.Map;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import android.view.Gravity;
|
||||||
import android.view.HapticFeedbackConstants;
|
import android.view.HapticFeedbackConstants;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.view.inputmethod.InputMethodManager;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.GridLayout;
|
import android.widget.GridLayout;
|
||||||
import android.widget.PopupWindow;
|
import android.widget.PopupWindow;
|
||||||
import android.widget.ToggleButton;
|
import android.widget.ToggleButton;
|
||||||
|
|
||||||
import com.termux.R;
|
import com.termux.R;
|
||||||
import com.termux.terminal.TerminalSession;
|
|
||||||
import com.termux.view.TerminalView;
|
import com.termux.view.TerminalView;
|
||||||
|
|
||||||
|
import androidx.drawerlayout.widget.DrawerLayout;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A view showing extra keys (such as Escape, Ctrl, Alt) not normally available on an Android soft
|
* A view showing extra keys (such as Escape, Ctrl, Alt) not normally available on an Android soft
|
||||||
* keyboard.
|
* keyboard.
|
||||||
@@ -35,31 +39,14 @@ public final class ExtraKeysView extends GridLayout {
|
|||||||
private static final int TEXT_COLOR = 0xFFFFFFFF;
|
private static final int TEXT_COLOR = 0xFFFFFFFF;
|
||||||
private static final int BUTTON_COLOR = 0x00000000;
|
private static final int BUTTON_COLOR = 0x00000000;
|
||||||
private static final int INTERESTING_COLOR = 0xFF80DEEA;
|
private static final int INTERESTING_COLOR = 0xFF80DEEA;
|
||||||
private static final int BUTTON_PRESSED_COLOR = 0x7FFFFFFF;
|
private static final int BUTTON_PRESSED_COLOR = 0xFF7F7F7F;
|
||||||
|
|
||||||
public ExtraKeysView(Context context, AttributeSet attrs) {
|
public ExtraKeysView(Context context, AttributeSet attrs) {
|
||||||
super(context, attrs);
|
super(context, attrs);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* HashMap that implements Python dict.get(key, default) function.
|
|
||||||
* Default java.util .get(key) is then the same as .get(key, null);
|
|
||||||
*/
|
|
||||||
static class CleverMap<K,V> extends HashMap<K,V> {
|
|
||||||
V get(K key, V defaultValue) {
|
|
||||||
if(containsKey(key))
|
|
||||||
return get(key);
|
|
||||||
else
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static class CharDisplayMap extends CleverMap<String, String> {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Keys are displayed in a natural looking way, like "→" for "RIGHT"
|
|
||||||
*/
|
|
||||||
static final Map<String, Integer> keyCodesForString = new HashMap<String, Integer>() {{
|
static final Map<String, Integer> keyCodesForString = new HashMap<String, Integer>() {{
|
||||||
|
put("SPACE", KeyEvent.KEYCODE_SPACE);
|
||||||
put("ESC", KeyEvent.KEYCODE_ESCAPE);
|
put("ESC", KeyEvent.KEYCODE_ESCAPE);
|
||||||
put("TAB", KeyEvent.KEYCODE_TAB);
|
put("TAB", KeyEvent.KEYCODE_TAB);
|
||||||
put("HOME", KeyEvent.KEYCODE_MOVE_HOME);
|
put("HOME", KeyEvent.KEYCODE_MOVE_HOME);
|
||||||
@@ -87,45 +74,79 @@ public final class ExtraKeysView extends GridLayout {
|
|||||||
put("F11", KeyEvent.KEYCODE_F11);
|
put("F11", KeyEvent.KEYCODE_F11);
|
||||||
put("F12", KeyEvent.KEYCODE_F12);
|
put("F12", KeyEvent.KEYCODE_F12);
|
||||||
}};
|
}};
|
||||||
|
|
||||||
static void sendKey(View view, String keyName) {
|
private void sendKey(View view, String keyName, boolean forceCtrlDown, boolean forceLeftAltDown) {
|
||||||
TerminalView terminalView = view.findViewById(R.id.terminal_view);
|
TerminalView terminalView = view.findViewById(R.id.terminal_view);
|
||||||
if (keyCodesForString.containsKey(keyName)) {
|
if ("KEYBOARD".equals(keyName)) {
|
||||||
|
InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||||
|
imm.toggleSoftInput(0, 0);
|
||||||
|
} else if ("DRAWER".equals(keyName)) {
|
||||||
|
DrawerLayout drawer = view.findViewById(R.id.drawer_layout);
|
||||||
|
drawer.openDrawer(Gravity.LEFT);
|
||||||
|
} else if (keyCodesForString.containsKey(keyName)) {
|
||||||
int keyCode = keyCodesForString.get(keyName);
|
int keyCode = keyCodesForString.get(keyName);
|
||||||
terminalView.onKeyDown(keyCode, new KeyEvent(KeyEvent.ACTION_UP, keyCode));
|
int metaState = 0;
|
||||||
// view.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode));
|
if (forceCtrlDown) {
|
||||||
|
metaState |= KeyEvent.META_CTRL_ON | KeyEvent.META_CTRL_LEFT_ON;
|
||||||
|
}
|
||||||
|
if (forceLeftAltDown) {
|
||||||
|
metaState |= KeyEvent.META_ALT_ON | KeyEvent.META_ALT_LEFT_ON;
|
||||||
|
}
|
||||||
|
KeyEvent keyEvent = new KeyEvent(0, 0, KeyEvent.ACTION_UP, keyCode, 0, metaState);
|
||||||
|
terminalView.onKeyDown(keyCode, keyEvent);
|
||||||
} else {
|
} else {
|
||||||
// not a control char
|
// not a control char
|
||||||
TerminalSession session = terminalView.getCurrentSession();
|
keyName.codePoints().forEach(codePoint -> {
|
||||||
if (session != null && keyName.length() > 0)
|
terminalView.inputCodePoint(codePoint, forceCtrlDown, forceLeftAltDown);
|
||||||
session.write(keyName);
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void sendKey(View view, ExtraKeyButton buttonInfo) {
|
||||||
|
if (buttonInfo.isMacro()) {
|
||||||
|
String[] keys = buttonInfo.getKey().split(" ");
|
||||||
|
boolean ctrlDown = false;
|
||||||
|
boolean altDown = false;
|
||||||
|
for (String key : keys) {
|
||||||
|
if ("CTRL".equals(key)) {
|
||||||
|
ctrlDown = true;
|
||||||
|
} else if ("ALT".equals(key)) {
|
||||||
|
altDown = true;
|
||||||
|
} else {
|
||||||
|
sendKey(view, key, ctrlDown, altDown);
|
||||||
|
ctrlDown = false;
|
||||||
|
altDown = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sendKey(view, buttonInfo.getKey(), false, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public enum SpecialButton {
|
public enum SpecialButton {
|
||||||
CTRL, ALT, FN
|
CTRL, ALT, FN
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class SpecialButtonState {
|
private static class SpecialButtonState {
|
||||||
boolean isOn = false;
|
boolean isOn = false;
|
||||||
ToggleButton button = null;
|
ToggleButton button = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<SpecialButton, SpecialButtonState> specialButtons = new HashMap<SpecialButton, SpecialButtonState>() {{
|
private Map<SpecialButton, SpecialButtonState> specialButtons = new HashMap<SpecialButton, SpecialButtonState>() {{
|
||||||
put(SpecialButton.CTRL, new SpecialButtonState());
|
put(SpecialButton.CTRL, new SpecialButtonState());
|
||||||
put(SpecialButton.ALT, new SpecialButtonState());
|
put(SpecialButton.ALT, new SpecialButtonState());
|
||||||
put(SpecialButton.FN, new SpecialButtonState());
|
put(SpecialButton.FN, new SpecialButtonState());
|
||||||
}};
|
}};
|
||||||
|
|
||||||
private ScheduledExecutorService scheduledExecutor;
|
private ScheduledExecutorService scheduledExecutor;
|
||||||
private PopupWindow popupWindow;
|
private PopupWindow popupWindow;
|
||||||
private int longPressCount;
|
private int longPressCount;
|
||||||
|
|
||||||
public boolean readSpecialButton(SpecialButton name) {
|
public boolean readSpecialButton(SpecialButton name) {
|
||||||
SpecialButtonState state = specialButtons.get(name);
|
SpecialButtonState state = specialButtons.get(name);
|
||||||
if (state == null)
|
if (state == null)
|
||||||
throw new RuntimeException("Must be a valid special button (see source)");
|
throw new RuntimeException("Must be a valid special button (see source)");
|
||||||
|
|
||||||
if (! state.isOn)
|
if (! state.isOn)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
@@ -166,145 +187,21 @@ public final class ExtraKeysView extends GridLayout {
|
|||||||
popupWindow.setFocusable(false);
|
popupWindow.setFocusable(false);
|
||||||
popupWindow.showAsDropDown(view, 0, -2 * height);
|
popupWindow.showAsDropDown(view, 0, -2 * height);
|
||||||
}
|
}
|
||||||
|
|
||||||
static final CharDisplayMap classicArrowsDisplay = new CharDisplayMap() {{
|
|
||||||
// classic arrow keys (for ◀ ▶ ▲ ▼ @see arrowVariationDisplay)
|
|
||||||
put("LEFT", "←"); // U+2190 ← LEFTWARDS ARROW
|
|
||||||
put("RIGHT", "→"); // U+2192 → RIGHTWARDS ARROW
|
|
||||||
put("UP", "↑"); // U+2191 ↑ UPWARDS ARROW
|
|
||||||
put("DOWN", "↓"); // U+2193 ↓ DOWNWARDS ARROW
|
|
||||||
}};
|
|
||||||
|
|
||||||
static final CharDisplayMap wellKnownCharactersDisplay = new CharDisplayMap() {{
|
|
||||||
// well known characters // https://en.wikipedia.org/wiki/{Enter_key, Tab_key, Delete_key}
|
|
||||||
put("ENTER", "↲"); // U+21B2 ↲ DOWNWARDS ARROW WITH TIP LEFTWARDS
|
|
||||||
put("TAB", "↹"); // U+21B9 ↹ LEFTWARDS ARROW TO BAR OVER RIGHTWARDS ARROW TO BAR
|
|
||||||
put("BKSP", "⌫"); // U+232B ⌫ ERASE TO THE LEFT sometimes seen and easy to understand
|
|
||||||
put("DEL", "⌦"); // U+2326 ⌦ ERASE TO THE RIGHT not well known but easy to understand
|
|
||||||
}};
|
|
||||||
|
|
||||||
static final CharDisplayMap lessKnownCharactersDisplay = new CharDisplayMap() {{
|
|
||||||
// https://en.wikipedia.org/wiki/{Home_key, End_key, Page_Up_and_Page_Down_keys}
|
|
||||||
// home key can mean "goto the beginning of line" or "goto first page" depending on context, hence the diagonal
|
|
||||||
put("HOME", "⇱"); // from IEC 9995 // U+21F1 ⇱ NORTH WEST ARROW TO CORNER
|
|
||||||
put("END", "⇲"); // from IEC 9995 // ⇲ // U+21F2 ⇲ SOUTH EAST ARROW TO CORNER
|
|
||||||
put("PGUP", "⇑"); // no ISO character exists, U+21D1 ⇑ UPWARDS DOUBLE ARROW will do the trick
|
|
||||||
put("PGDN", "⇓"); // no ISO character exists, U+21D3 ⇓ DOWNWARDS DOUBLE ARROW will do the trick
|
|
||||||
}};
|
|
||||||
|
|
||||||
static final CharDisplayMap arrowTriangleVariationDisplay = new CharDisplayMap() {{
|
|
||||||
// alternative to classic arrow keys
|
|
||||||
put("LEFT", "◀"); // U+25C0 ◀ BLACK LEFT-POINTING TRIANGLE
|
|
||||||
put("RIGHT", "▶"); // U+25B6 ▶ BLACK RIGHT-POINTING TRIANGLE
|
|
||||||
put("UP", "▲"); // U+25B2 ▲ BLACK UP-POINTING TRIANGLE
|
|
||||||
put("DOWN", "▼"); // U+25BC ▼ BLACK DOWN-POINTING TRIANGLE
|
|
||||||
}};
|
|
||||||
|
|
||||||
static final CharDisplayMap notKnownIsoCharacters = new CharDisplayMap() {{
|
|
||||||
// Control chars that are more clear as text // https://en.wikipedia.org/wiki/{Function_key, Alt_key, Control_key, Esc_key}
|
|
||||||
// put("FN", "FN"); // no ISO character exists
|
|
||||||
put("CTRL", "⎈"); // ISO character "U+2388 ⎈ HELM SYMBOL" is unknown to people and never printed on computers, however "U+25C7 ◇ WHITE DIAMOND" is a nice presentation, and "^" for terminal app and mac is often used
|
|
||||||
put("ALT", "⎇"); // ISO character "U+2387 ⎇ ALTERNATIVE KEY SYMBOL'" is unknown to people and only printed as the Option key "⌥" on Mac computer
|
|
||||||
put("ESC", "⎋"); // ISO character "U+238B ⎋ BROKEN CIRCLE WITH NORTHWEST ARROW" is unknown to people and not often printed on computers
|
|
||||||
}};
|
|
||||||
|
|
||||||
static final CharDisplayMap nicerLookingDisplay = new CharDisplayMap() {{
|
|
||||||
// nicer looking for most cases
|
|
||||||
put("-", "―"); // U+2015 ― HORIZONTAL BAR
|
|
||||||
}};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Keys are displayed in a natural looking way, like "→" for "RIGHT" or "↲" for ENTER
|
|
||||||
*/
|
|
||||||
public static final CharDisplayMap defaultCharDisplay = new CharDisplayMap() {{
|
|
||||||
putAll(classicArrowsDisplay);
|
|
||||||
putAll(wellKnownCharactersDisplay);
|
|
||||||
putAll(nicerLookingDisplay);
|
|
||||||
// all other characters are displayed as themselves
|
|
||||||
}};
|
|
||||||
|
|
||||||
public static final CharDisplayMap lotsOfArrowsCharDisplay = new CharDisplayMap() {{
|
|
||||||
putAll(classicArrowsDisplay);
|
|
||||||
putAll(wellKnownCharactersDisplay);
|
|
||||||
putAll(lessKnownCharactersDisplay); // NEW
|
|
||||||
putAll(nicerLookingDisplay);
|
|
||||||
}};
|
|
||||||
|
|
||||||
public static final CharDisplayMap arrowsOnlyCharDisplay = new CharDisplayMap() {{
|
|
||||||
putAll(classicArrowsDisplay);
|
|
||||||
// putAll(wellKnownCharactersDisplay); // REMOVED
|
|
||||||
// putAll(lessKnownCharactersDisplay); // REMOVED
|
|
||||||
putAll(nicerLookingDisplay);
|
|
||||||
}};
|
|
||||||
|
|
||||||
public static final CharDisplayMap fullIsoCharDisplay = new CharDisplayMap() {{
|
|
||||||
putAll(classicArrowsDisplay);
|
|
||||||
putAll(wellKnownCharactersDisplay);
|
|
||||||
putAll(lessKnownCharactersDisplay); // NEW
|
|
||||||
putAll(nicerLookingDisplay);
|
|
||||||
putAll(notKnownIsoCharacters); // NEW
|
|
||||||
}};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Some people might call our keys differently
|
|
||||||
*/
|
|
||||||
static final CharDisplayMap controlCharsAliases = new CharDisplayMap() {{
|
|
||||||
put("ESCAPE", "ESC");
|
|
||||||
put("CONTROL", "CTRL");
|
|
||||||
put("RETURN", "ENTER"); // Technically different keys, but most applications won't see the difference
|
|
||||||
put("FUNCTION", "FN");
|
|
||||||
// no alias for ALT
|
|
||||||
|
|
||||||
// Directions are sometimes written as first and last letter for brevety
|
|
||||||
put("LT", "LEFT");
|
|
||||||
put("RT", "RIGHT");
|
|
||||||
put("DN", "DOWN");
|
|
||||||
// put("UP", "UP"); well, "UP" is already two letters
|
|
||||||
|
|
||||||
put("PAGEUP", "PGUP");
|
|
||||||
put("PAGE_UP", "PGUP");
|
|
||||||
put("PAGE UP", "PGUP");
|
|
||||||
put("PAGE-UP", "PGUP");
|
|
||||||
|
|
||||||
// no alias for HOME
|
|
||||||
// no alias for END
|
|
||||||
|
|
||||||
put("PAGEDOWN", "PGDN");
|
|
||||||
put("PAGE_DOWN", "PGDN");
|
|
||||||
put("PAGE-DOWN", "PGDN");
|
|
||||||
|
|
||||||
put("DELETE", "DEL");
|
|
||||||
put("BACKSPACE", "BKSP");
|
|
||||||
|
|
||||||
// easier for writing in termux.properties
|
|
||||||
put("BACKSLASH", "\\");
|
|
||||||
put("QUOTE", "\"");
|
|
||||||
put("APOSTROPHE", "'");
|
|
||||||
}};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies the 'controlCharsAliases' mapping to all the strings in *buttons*
|
|
||||||
* Modifies the array, doesn't return a new one.
|
|
||||||
*/
|
|
||||||
void replaceAliases(String[][] buttons) {
|
|
||||||
for(int i = 0; i < buttons.length; i++)
|
|
||||||
for(int j = 0; j < buttons[i].length; j++)
|
|
||||||
buttons[i][j] = controlCharsAliases.get(buttons[i][j], buttons[i][j]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* General util function to compute the longest column length in a matrix.
|
* General util function to compute the longest column length in a matrix.
|
||||||
*/
|
*/
|
||||||
static int maximumLength(String[][] matrix) {
|
static int maximumLength(Object[][] matrix) {
|
||||||
int m = 0;
|
int m = 0;
|
||||||
for (String[] aMatrix : matrix) m = Math.max(m, aMatrix.length);
|
for (Object[] row : matrix)
|
||||||
|
m = Math.max(m, row.length);
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reload the view given parameters in termux.properties
|
* Reload the view given parameters in termux.properties
|
||||||
*
|
*
|
||||||
* @param buttons matrix of String as defined in termux.properties extrakeys
|
* @param infos matrix as defined in termux.properties extrakeys
|
||||||
* Can Contain The Strings CTRL ALT TAB FN ENTER LEFT RIGHT UP DOWN or normal strings
|
* Can Contain The Strings CTRL ALT TAB FN ENTER LEFT RIGHT UP DOWN or normal strings
|
||||||
* Some aliases are possible like RETURN for ENTER, LT for LEFT and more (@see controlCharsAliases for the whole list).
|
* Some aliases are possible like RETURN for ENTER, LT for LEFT and more (@see controlCharsAliases for the whole list).
|
||||||
* Any string of length > 1 in total Uppercase will print a warning
|
* Any string of length > 1 in total Uppercase will print a warning
|
||||||
@@ -316,36 +213,36 @@ public final class ExtraKeysView extends GridLayout {
|
|||||||
* "−" will input a "−" character
|
* "−" will input a "−" character
|
||||||
* "-_-" will input the string "-_-"
|
* "-_-" will input the string "-_-"
|
||||||
*/
|
*/
|
||||||
void reload(String[][] buttons, CharDisplayMap charDisplayMap) {
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
|
void reload(ExtraKeysInfos infos) {
|
||||||
|
if(infos == null)
|
||||||
|
return;
|
||||||
|
|
||||||
for(SpecialButtonState state : specialButtons.values())
|
for(SpecialButtonState state : specialButtons.values())
|
||||||
state.button = null;
|
state.button = null;
|
||||||
|
|
||||||
removeAllViews();
|
removeAllViews();
|
||||||
|
|
||||||
replaceAliases(buttons); // modifies the array
|
|
||||||
|
|
||||||
final int rows = buttons.length;
|
ExtraKeyButton[][] buttons = infos.getMatrix();
|
||||||
final int cols = maximumLength(buttons);
|
|
||||||
|
|
||||||
setRowCount(rows);
|
setRowCount(buttons.length);
|
||||||
setColumnCount(cols);
|
setColumnCount(maximumLength(buttons));
|
||||||
|
|
||||||
for (int row = 0; row < rows; row++) {
|
for (int row = 0; row < buttons.length; row++) {
|
||||||
for (int col = 0; col < buttons[row].length; col++) {
|
for (int col = 0; col < buttons[row].length; col++) {
|
||||||
final String buttonText = buttons[row][col];
|
final ExtraKeyButton buttonInfo = buttons[row][col];
|
||||||
|
|
||||||
Button button;
|
Button button;
|
||||||
if(Arrays.asList("CTRL", "ALT", "FN").contains(buttonText)) {
|
if(Arrays.asList("CTRL", "ALT", "FN").contains(buttonInfo.getKey())) {
|
||||||
SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonText)); // for valueOf: https://stackoverflow.com/a/604426/1980630
|
SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonInfo.getKey())); // for valueOf: https://stackoverflow.com/a/604426/1980630
|
||||||
state.isOn = true;
|
state.isOn = true;
|
||||||
button = state.button = new ToggleButton(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
button = state.button = new ToggleButton(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
||||||
button.setClickable(true);
|
button.setClickable(true);
|
||||||
} else {
|
} else {
|
||||||
button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
||||||
}
|
}
|
||||||
|
|
||||||
final String displayedText = charDisplayMap.get(buttonText, buttonText);
|
button.setText(buttonInfo.getDisplay());
|
||||||
button.setText(displayedText);
|
|
||||||
button.setTextColor(TEXT_COLOR);
|
button.setTextColor(TEXT_COLOR);
|
||||||
button.setPadding(0, 0, 0, 0);
|
button.setPadding(0, 0, 0, 0);
|
||||||
|
|
||||||
@@ -365,12 +262,12 @@ public final class ExtraKeysView extends GridLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
View root = getRootView();
|
View root = getRootView();
|
||||||
if(Arrays.asList("CTRL", "ALT", "FN").contains(buttonText)) {
|
if (Arrays.asList("CTRL", "ALT", "FN").contains(buttonInfo.getKey())) {
|
||||||
ToggleButton self = (ToggleButton) finalButton;
|
ToggleButton self = (ToggleButton) finalButton;
|
||||||
self.setChecked(self.isChecked());
|
self.setChecked(self.isChecked());
|
||||||
self.setTextColor(self.isChecked() ? INTERESTING_COLOR : TEXT_COLOR);
|
self.setTextColor(self.isChecked() ? INTERESTING_COLOR : TEXT_COLOR);
|
||||||
} else {
|
} else {
|
||||||
sendKey(root, buttonText);
|
sendKey(root, buttonInfo);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -380,22 +277,26 @@ public final class ExtraKeysView extends GridLayout {
|
|||||||
case MotionEvent.ACTION_DOWN:
|
case MotionEvent.ACTION_DOWN:
|
||||||
longPressCount = 0;
|
longPressCount = 0;
|
||||||
v.setBackgroundColor(BUTTON_PRESSED_COLOR);
|
v.setBackgroundColor(BUTTON_PRESSED_COLOR);
|
||||||
if (Arrays.asList("UP", "DOWN", "LEFT", "RIGHT").contains(buttonText)) {
|
if (Arrays.asList("UP", "DOWN", "LEFT", "RIGHT", "BKSP", "DEL").contains(buttonInfo.getKey())) {
|
||||||
|
// autorepeat
|
||||||
scheduledExecutor = Executors.newSingleThreadScheduledExecutor();
|
scheduledExecutor = Executors.newSingleThreadScheduledExecutor();
|
||||||
scheduledExecutor.scheduleWithFixedDelay(() -> {
|
scheduledExecutor.scheduleWithFixedDelay(() -> {
|
||||||
longPressCount++;
|
longPressCount++;
|
||||||
sendKey(root, buttonText);
|
sendKey(root, buttonInfo);
|
||||||
}, 400, 80, TimeUnit.MILLISECONDS);
|
}, 400, 80, TimeUnit.MILLISECONDS);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case MotionEvent.ACTION_MOVE:
|
case MotionEvent.ACTION_MOVE:
|
||||||
// These two keys have a Move-Up button appearing
|
if (buttonInfo.getPopup() != null) {
|
||||||
if (Arrays.asList("/", "-").contains(buttonText)) {
|
|
||||||
if (popupWindow == null && event.getY() < 0) {
|
if (popupWindow == null && event.getY() < 0) {
|
||||||
|
if (scheduledExecutor != null) {
|
||||||
|
scheduledExecutor.shutdownNow();
|
||||||
|
scheduledExecutor = null;
|
||||||
|
}
|
||||||
v.setBackgroundColor(BUTTON_COLOR);
|
v.setBackgroundColor(BUTTON_COLOR);
|
||||||
String text = "-".equals(buttonText) ? "|" : "\\";
|
String extraButtonDisplayedText = buttonInfo.getPopup().getDisplay();
|
||||||
popup(v, text);
|
popup(v, extraButtonDisplayedText);
|
||||||
}
|
}
|
||||||
if (popupWindow != null && event.getY() > 0) {
|
if (popupWindow != null && event.getY() > 0) {
|
||||||
v.setBackgroundColor(BUTTON_PRESSED_COLOR);
|
v.setBackgroundColor(BUTTON_PRESSED_COLOR);
|
||||||
@@ -418,12 +319,14 @@ public final class ExtraKeysView extends GridLayout {
|
|||||||
scheduledExecutor.shutdownNow();
|
scheduledExecutor.shutdownNow();
|
||||||
scheduledExecutor = null;
|
scheduledExecutor = null;
|
||||||
}
|
}
|
||||||
if (longPressCount == 0) {
|
if (longPressCount == 0 || popupWindow != null) {
|
||||||
if (popupWindow != null && Arrays.asList("/", "-").contains(buttonText)) {
|
if (popupWindow != null) {
|
||||||
popupWindow.setContentView(null);
|
popupWindow.setContentView(null);
|
||||||
popupWindow.dismiss();
|
popupWindow.dismiss();
|
||||||
popupWindow = null;
|
popupWindow = null;
|
||||||
sendKey(root, "-".equals(buttonText) ? "|" : "\\");
|
if (buttonInfo.getPopup() != null) {
|
||||||
|
sendKey(root, buttonInfo.getPopup());
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
v.performClick();
|
v.performClick();
|
||||||
}
|
}
|
||||||
|
|||||||
102
app/src/main/java/com/termux/app/RunCommandService.java
Normal file
102
app/src/main/java/com/termux/app/RunCommandService.java
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
package com.termux.app;
|
||||||
|
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Binder;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When allow-external-apps property is set to "true" in ~/.termux/termux.properties, Termux
|
||||||
|
* is able to process execute intents sent by third-party applications.
|
||||||
|
*
|
||||||
|
* Third-party program must declare com.termux.permission.RUN_COMMAND permission and it should be
|
||||||
|
* granted by user.
|
||||||
|
* Full path of command or script must be given in "RUN_COMMAND_PATH" extra.
|
||||||
|
* The "RUN_COMMAND_ARGUMENTS", "RUN_COMMAND_WORKDIR" and "RUN_COMMAND_BACKGROUND" extras are
|
||||||
|
* optional. The background mode defaults to false.
|
||||||
|
*
|
||||||
|
* Sample code to run command "top" with java:
|
||||||
|
* Intent intent = new Intent();
|
||||||
|
* intent.setClassName("com.termux", "com.termux.app.RunCommandService");
|
||||||
|
* intent.setAction("com.termux.RUN_COMMAND");
|
||||||
|
* intent.putExtra("com.termux.RUN_COMMAND_PATH", "/data/data/com.termux/files/usr/bin/top");
|
||||||
|
* intent.putExtra("com.termux.RUN_COMMAND_ARGUMENTS", new String[]{"-n", "5"});
|
||||||
|
* intent.putExtra("com.termux.RUN_COMMAND_WORKDIR", "/data/data/com.termux/files/home");
|
||||||
|
* intent.putExtra("com.termux.RUN_COMMAND_BACKGROUND", false);
|
||||||
|
* startService(intent);
|
||||||
|
*
|
||||||
|
* Sample code to run command "top" with "am startservice" command:
|
||||||
|
* am startservice --user 0 -n com.termux/com.termux.app.RunCommandService
|
||||||
|
* -a com.termux.RUN_COMMAND
|
||||||
|
* --es com.termux.RUN_COMMAND_PATH '/data/data/com.termux/files/usr/bin/top'
|
||||||
|
* --esa com.termux.RUN_COMMAND_ARGUMENTS '-n,5'
|
||||||
|
* --es com.termux.RUN_COMMAND_WORKDIR '/data/data/com.termux/files/home'
|
||||||
|
* --ez com.termux.RUN_COMMAND_BACKGROUND 'false'
|
||||||
|
*/
|
||||||
|
public class RunCommandService extends Service {
|
||||||
|
|
||||||
|
public static final String RUN_COMMAND_ACTION = "com.termux.RUN_COMMAND";
|
||||||
|
public static final String RUN_COMMAND_PATH = "com.termux.RUN_COMMAND_PATH";
|
||||||
|
public static final String RUN_COMMAND_ARGUMENTS = "com.termux.RUN_COMMAND_ARGUMENTS";
|
||||||
|
public static final String RUN_COMMAND_WORKDIR = "com.termux.RUN_COMMAND_WORKDIR";
|
||||||
|
public static final String RUN_COMMAND_BACKGROUND = "com.termux.RUN_COMMAND_BACKGROUND";
|
||||||
|
|
||||||
|
class LocalBinder extends Binder {
|
||||||
|
public final RunCommandService service = RunCommandService.this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final IBinder mBinder = new RunCommandService.LocalBinder();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IBinder onBind(Intent intent) {
|
||||||
|
return mBinder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||||
|
if (allowExternalApps() && RUN_COMMAND_ACTION.equals(intent.getAction())) {
|
||||||
|
Uri programUri = new Uri.Builder().scheme("com.termux.file").path(intent.getStringExtra(RUN_COMMAND_PATH)).build();
|
||||||
|
|
||||||
|
Intent execIntent = new Intent(TermuxService.ACTION_EXECUTE, programUri);
|
||||||
|
execIntent.setClass(this, TermuxService.class);
|
||||||
|
execIntent.putExtra(TermuxService.EXTRA_ARGUMENTS, intent.getStringArrayExtra(RUN_COMMAND_ARGUMENTS));
|
||||||
|
execIntent.putExtra(TermuxService.EXTRA_CURRENT_WORKING_DIRECTORY, intent.getStringExtra(RUN_COMMAND_WORKDIR));
|
||||||
|
execIntent.putExtra(TermuxService.EXTRA_EXECUTE_IN_BACKGROUND, intent.getBooleanExtra(RUN_COMMAND_BACKGROUND, false));
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
this.startForegroundService(execIntent);
|
||||||
|
} else {
|
||||||
|
this.startService(execIntent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Service.START_NOT_STICKY;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean allowExternalApps() {
|
||||||
|
File propsFile = new File(TermuxService.HOME_PATH + "/.termux/termux.properties");
|
||||||
|
if (!propsFile.exists())
|
||||||
|
propsFile = new File(TermuxService.HOME_PATH + "/.config/termux/termux.properties");
|
||||||
|
|
||||||
|
Properties props = new Properties();
|
||||||
|
try {
|
||||||
|
if (propsFile.isFile() && propsFile.canRead()) {
|
||||||
|
try (FileInputStream in = new FileInputStream(propsFile)) {
|
||||||
|
props.load(new InputStreamReader(in, StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e("termux", "Error loading props", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.getProperty("allow-external-apps", "false").equals("true");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -148,7 +148,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
mSettings.reloadFromProperties(TermuxActivity.this);
|
mSettings.reloadFromProperties(TermuxActivity.this);
|
||||||
|
|
||||||
if (mExtraKeysView != null) {
|
if (mExtraKeysView != null) {
|
||||||
mExtraKeysView.reload(mSettings.mExtraKeys, ExtraKeysView.defaultCharDisplay);
|
mExtraKeysView.reload(mSettings.mExtraKeys);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -229,7 +229,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
|
|
||||||
|
|
||||||
ViewGroup.LayoutParams layoutParams = viewPager.getLayoutParams();
|
ViewGroup.LayoutParams layoutParams = viewPager.getLayoutParams();
|
||||||
layoutParams.height = layoutParams.height * mSettings.mExtraKeys.length;
|
layoutParams.height = layoutParams.height * (mSettings.mExtraKeys == null ? 0 : mSettings.mExtraKeys.getMatrix().length);
|
||||||
viewPager.setLayoutParams(layoutParams);
|
viewPager.setLayoutParams(layoutParams);
|
||||||
|
|
||||||
viewPager.setAdapter(new PagerAdapter() {
|
viewPager.setAdapter(new PagerAdapter() {
|
||||||
@@ -250,7 +250,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
View layout;
|
View layout;
|
||||||
if (position == 0) {
|
if (position == 0) {
|
||||||
layout = mExtraKeysView = (ExtraKeysView) inflater.inflate(R.layout.extra_keys_main, collection, false);
|
layout = mExtraKeysView = (ExtraKeysView) inflater.inflate(R.layout.extra_keys_main, collection, false);
|
||||||
mExtraKeysView.reload(mSettings.mExtraKeys, ExtraKeysView.defaultCharDisplay);
|
mExtraKeysView.reload(mSettings.mExtraKeys);
|
||||||
} else {
|
} else {
|
||||||
layout = inflater.inflate(R.layout.extra_keys_right, collection, false);
|
layout = inflater.inflate(R.layout.extra_keys_right, collection, false);
|
||||||
final EditText editText = layout.findViewById(R.id.text_input);
|
final EditText editText = layout.findViewById(R.id.text_input);
|
||||||
@@ -733,6 +733,9 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
// Resource path with optional query string.
|
// Resource path with optional query string.
|
||||||
regex_sb.append("(?:/[a-zA-Z0-9:@%\\-._~!$&()*+,;=?/]*)?");
|
regex_sb.append("(?:/[a-zA-Z0-9:@%\\-._~!$&()*+,;=?/]*)?");
|
||||||
|
|
||||||
|
// Fragment.
|
||||||
|
regex_sb.append("(?:#[a-zA-Z0-9:@%\\-._~!$&()*+,;=?/]*)?");
|
||||||
|
|
||||||
// End second matching group.
|
// End second matching group.
|
||||||
regex_sb.append(")");
|
regex_sb.append(")");
|
||||||
|
|
||||||
@@ -754,7 +757,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
}
|
}
|
||||||
|
|
||||||
void showUrlSelection() {
|
void showUrlSelection() {
|
||||||
String text = getCurrentTermSession().getEmulator().getScreen().getTranscriptText();
|
String text = getCurrentTermSession().getEmulator().getScreen().getTranscriptTextWithFullLinesJoined();
|
||||||
LinkedHashSet<CharSequence> urlSet = extractUrls(text);
|
LinkedHashSet<CharSequence> urlSet = extractUrls(text);
|
||||||
if (urlSet.isEmpty()) {
|
if (urlSet.isEmpty()) {
|
||||||
new AlertDialog.Builder(this).setMessage(R.string.select_url_no_found).show();
|
new AlertDialog.Builder(this).setMessage(R.string.select_url_no_found).show();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.termux.app;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
import android.content.res.Configuration;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.TypedValue;
|
import android.util.TypedValue;
|
||||||
@@ -9,6 +10,7 @@ import android.widget.Toast;
|
|||||||
import com.termux.terminal.TerminalSession;
|
import com.termux.terminal.TerminalSession;
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
@@ -18,7 +20,10 @@ import java.lang.annotation.Retention;
|
|||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
|
|
||||||
import androidx.annotation.IntDef;
|
import androidx.annotation.IntDef;
|
||||||
@@ -58,7 +63,7 @@ final class TermuxPreferences {
|
|||||||
private static final String CURRENT_SESSION_KEY = "current_session";
|
private static final String CURRENT_SESSION_KEY = "current_session";
|
||||||
private static final String SCREEN_ALWAYS_ON_KEY = "screen_always_on";
|
private static final String SCREEN_ALWAYS_ON_KEY = "screen_always_on";
|
||||||
|
|
||||||
private String mUseDarkUI;
|
private boolean mUseDarkUI;
|
||||||
private boolean mScreenAlwaysOn;
|
private boolean mScreenAlwaysOn;
|
||||||
private int mFontSize;
|
private int mFontSize;
|
||||||
|
|
||||||
@@ -69,7 +74,7 @@ final class TermuxPreferences {
|
|||||||
boolean mDisableVolumeVirtualKeys;
|
boolean mDisableVolumeVirtualKeys;
|
||||||
boolean mShowExtraKeys;
|
boolean mShowExtraKeys;
|
||||||
|
|
||||||
String[][] mExtraKeys;
|
ExtraKeysInfos mExtraKeys;
|
||||||
|
|
||||||
final List<KeyboardShortcut> shortcuts = new ArrayList<>();
|
final List<KeyboardShortcut> shortcuts = new ArrayList<>();
|
||||||
|
|
||||||
@@ -129,7 +134,7 @@ final class TermuxPreferences {
|
|||||||
}
|
}
|
||||||
|
|
||||||
boolean isUsingBlackUI() {
|
boolean isUsingBlackUI() {
|
||||||
return mUseDarkUI.toLowerCase().equals("true");
|
return mUseDarkUI;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setScreenAlwaysOn(Context context, boolean newValue) {
|
void setScreenAlwaysOn(Context context, boolean newValue) {
|
||||||
@@ -149,7 +154,7 @@ final class TermuxPreferences {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
void reloadFromProperties(Context context) {
|
void reloadFromProperties(Context context) {
|
||||||
File propsFile = new File(TermuxService.HOME_PATH + "/.termux/termux.properties");
|
File propsFile = new File(TermuxService.HOME_PATH + "/.termux/termux.properties");
|
||||||
if (!propsFile.exists())
|
if (!propsFile.exists())
|
||||||
@@ -162,8 +167,8 @@ final class TermuxPreferences {
|
|||||||
props.load(new InputStreamReader(in, StandardCharsets.UTF_8));
|
props.load(new InputStreamReader(in, StandardCharsets.UTF_8));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (Exception e) {
|
||||||
Toast.makeText(context, "Could not open properties file termux.properties.", Toast.LENGTH_LONG).show();
|
Toast.makeText(context, "Could not open properties file termux.properties: " + e.getMessage(), Toast.LENGTH_LONG).show();
|
||||||
Log.e("termux", "Error loading props", e);
|
Log.e("termux", "Error loading props", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,23 +184,35 @@ final class TermuxPreferences {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
mUseDarkUI = props.getProperty("use-black-ui", "false");
|
switch (props.getProperty("use-black-ui", "").toLowerCase()) {
|
||||||
|
case "true":
|
||||||
|
mUseDarkUI = true;
|
||||||
|
break;
|
||||||
|
case "false":
|
||||||
|
mUseDarkUI = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
int nightMode = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
||||||
|
mUseDarkUI = nightMode == Configuration.UI_MODE_NIGHT_YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
String defaultExtraKeys = "[[ESC, TAB, CTRL, ALT, {key: '-', popup: '|'}, DOWN, UP]]";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
JSONArray arr = new JSONArray(props.getProperty("extra-keys", "[['ESC', 'TAB', 'CTRL', 'ALT', '-', 'DOWN', 'UP']]"));
|
String extrakeyProp = props.getProperty("extra-keys", defaultExtraKeys);
|
||||||
|
String extraKeysStyle = props.getProperty("extra-keys-style", "default");
|
||||||
mExtraKeys = new String[arr.length()][];
|
mExtraKeys = new ExtraKeysInfos(extrakeyProp, extraKeysStyle);
|
||||||
for (int i = 0; i < arr.length(); i++) {
|
|
||||||
JSONArray line = arr.getJSONArray(i);
|
|
||||||
mExtraKeys[i] = new String[line.length()];
|
|
||||||
for (int j = 0; j < line.length(); j++) {
|
|
||||||
mExtraKeys[i][j] = line.getString(j);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (JSONException e) {
|
} catch (JSONException e) {
|
||||||
Toast.makeText(context, "Could not load the extra-keys property from the config: " + e.toString(), Toast.LENGTH_LONG).show();
|
Toast.makeText(context, "Could not load the extra-keys property from the config: " + e.toString(), Toast.LENGTH_LONG).show();
|
||||||
Log.e("termux", "Error loading props", e);
|
Log.e("termux", "Error loading props", e);
|
||||||
mExtraKeys = new String[0][];
|
|
||||||
|
try {
|
||||||
|
mExtraKeys = new ExtraKeysInfos(defaultExtraKeys, "default");
|
||||||
|
} catch (JSONException e2) {
|
||||||
|
e2.printStackTrace();
|
||||||
|
Toast.makeText(context, "Can't create default extra keys", Toast.LENGTH_LONG).show();
|
||||||
|
mExtraKeys = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mBackIsEscape = "escape".equals(props.getProperty("back-key", "back"));
|
mBackIsEscape = "escape".equals(props.getProperty("back-key", "back"));
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ public final class TermuxService extends Service implements SessionChangedCallba
|
|||||||
public static final String EXTRA_ARGUMENTS = "com.termux.execute.arguments";
|
public static final String EXTRA_ARGUMENTS = "com.termux.execute.arguments";
|
||||||
|
|
||||||
public static final String EXTRA_CURRENT_WORKING_DIRECTORY = "com.termux.execute.cwd";
|
public static final String EXTRA_CURRENT_WORKING_DIRECTORY = "com.termux.execute.cwd";
|
||||||
private static final String EXTRA_EXECUTE_IN_BACKGROUND = "com.termux.execute.background";
|
public static final String EXTRA_EXECUTE_IN_BACKGROUND = "com.termux.execute.background";
|
||||||
|
|
||||||
/** This service is only bound from inside the same process and never uses IPC. */
|
/** This service is only bound from inside the same process and never uses IPC. */
|
||||||
class LocalBinder extends Binder {
|
class LocalBinder extends Binder {
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
|
|||||||
row.add(Root.COLUMN_ROOT_ID, getDocIdForFile(BASE_DIR));
|
row.add(Root.COLUMN_ROOT_ID, getDocIdForFile(BASE_DIR));
|
||||||
row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(BASE_DIR));
|
row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(BASE_DIR));
|
||||||
row.add(Root.COLUMN_SUMMARY, null);
|
row.add(Root.COLUMN_SUMMARY, null);
|
||||||
row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_SEARCH);
|
row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_SEARCH | Root.FLAG_SUPPORTS_IS_CHILD);
|
||||||
row.add(Root.COLUMN_TITLE, applicationName);
|
row.add(Root.COLUMN_TITLE, applicationName);
|
||||||
row.add(Root.COLUMN_MIME_TYPES, ALL_MIME_TYPES);
|
row.add(Root.COLUMN_MIME_TYPES, ALL_MIME_TYPES);
|
||||||
row.add(Root.COLUMN_AVAILABLE_BYTES, BASE_DIR.getFreeSpace());
|
row.add(Root.COLUMN_AVAILABLE_BYTES, BASE_DIR.getFreeSpace());
|
||||||
@@ -117,6 +117,29 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String createDocument(String parentDocumentId, String mimeType, String displayName) throws FileNotFoundException {
|
||||||
|
File newFile = new File(parentDocumentId, displayName);
|
||||||
|
int noConflictId = 2;
|
||||||
|
while (newFile.exists()) {
|
||||||
|
newFile = new File(parentDocumentId, displayName + " (" + noConflictId++ + ")");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
boolean succeeded;
|
||||||
|
if (Document.MIME_TYPE_DIR.equals(mimeType)) {
|
||||||
|
succeeded = newFile.mkdir();
|
||||||
|
} else {
|
||||||
|
succeeded = newFile.createNewFile();
|
||||||
|
}
|
||||||
|
if (!succeeded) {
|
||||||
|
throw new FileNotFoundException("Failed to create document with id " + newFile.getPath());
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new FileNotFoundException("Failed to create document with id " + newFile.getPath());
|
||||||
|
}
|
||||||
|
return newFile.getPath();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void deleteDocument(String documentId) throws FileNotFoundException {
|
public void deleteDocument(String documentId) throws FileNotFoundException {
|
||||||
File file = getFileForDocId(documentId);
|
File file = getFileForDocId(documentId);
|
||||||
@@ -169,6 +192,11 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isChildDocument(String parentDocumentId, String documentId) {
|
||||||
|
return documentId.startsWith(parentDocumentId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
||||||
@@ -220,10 +248,11 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
|
|||||||
|
|
||||||
int flags = 0;
|
int flags = 0;
|
||||||
if (file.isDirectory()) {
|
if (file.isDirectory()) {
|
||||||
if (file.isDirectory() && file.canWrite()) flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
|
if (file.canWrite()) flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
|
||||||
} else if (file.canWrite()) {
|
} else if (file.canWrite()) {
|
||||||
flags |= Document.FLAG_SUPPORTS_WRITE | Document.FLAG_SUPPORTS_DELETE;
|
flags |= Document.FLAG_SUPPORTS_WRITE;
|
||||||
}
|
}
|
||||||
|
if (file.getParentFile().canWrite()) flags |= Document.FLAG_SUPPORTS_DELETE;
|
||||||
|
|
||||||
final String displayName = file.getName();
|
final String displayName = file.getName();
|
||||||
final String mimeType = getMimeType(file);
|
final String mimeType = getMimeType(file);
|
||||||
|
|||||||
@@ -1,33 +1,24 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:height="48dp"
|
android:width="24dp"
|
||||||
android:width="48dp"
|
android:height="24dp"
|
||||||
android:viewportWidth="48"
|
android:viewportWidth="24"
|
||||||
android:viewportHeight="48">
|
android:viewportHeight="24">
|
||||||
<!--
|
<!--
|
||||||
https://material.google.com/style/icons.html
|
Updated notification icon compliant with system icons guidelines
|
||||||
|
https://material.io/design/iconography/system-icons.html
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<!-- Screen border. -->
|
<group>
|
||||||
<path android:fillColor="#00000000"
|
<clip-path
|
||||||
android:strokeColor="#FFF"
|
android:pathData="M0,0h24v24h-24z"/>
|
||||||
android:strokeWidth="3"
|
|
||||||
android:pathData="M7,4
|
|
||||||
l34,0
|
|
||||||
q3 0,3 3
|
|
||||||
l0,34
|
|
||||||
q0 3, -3 3
|
|
||||||
l-34,0
|
|
||||||
q-3 0, -3-3
|
|
||||||
l0 -34
|
|
||||||
q0 -3, 3 -3"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Block cursor. -->
|
<path
|
||||||
<path android:fillColor="#FFF"
|
android:pathData="M5,4H2L8,12L2,20H5L11,12L5,4Z"
|
||||||
android:pathData="M14,14
|
android:fillColor="#ffffff"/>
|
||||||
l5,0
|
|
||||||
l0,10
|
|
||||||
l-5,0"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:pathData="M13,18H22V20H13V18Z"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
|
||||||
|
</group>
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="application_name">Termux</string>
|
<string name="application_name">Termux</string>
|
||||||
<string name="shared_user_label">Termux user</string>
|
<string name="shared_user_label">Termux user</string>
|
||||||
|
<string name="run_command_permission_label">Run commands in Termux environment</string>
|
||||||
|
<string name="run_command_permission_description">Allow third-party applications to execute arbitrary commands within Termux environment.</string>
|
||||||
<string name="new_session">New session</string>
|
<string name="new_session">New session</string>
|
||||||
<string name="new_session_failsafe">Failsafe</string>
|
<string name="new_session_failsafe">Failsafe</string>
|
||||||
<string name="toggle_soft_keyboard">Keyboard</string>
|
<string name="toggle_soft_keyboard">Keyboard</string>
|
||||||
@@ -13,7 +15,7 @@
|
|||||||
|
|
||||||
<string name="bootstrap_installer_body">Installing…</string>
|
<string name="bootstrap_installer_body">Installing…</string>
|
||||||
<string name="bootstrap_error_title">Unable to install</string>
|
<string name="bootstrap_error_title">Unable to install</string>
|
||||||
<string name="bootstrap_error_body">Termux was unable to install the bootstrap packages.\n\nCheck your network connection and try again.</string>
|
<string name="bootstrap_error_body">Termux was unable to install the bootstrap packages.</string>
|
||||||
<string name="bootstrap_error_abort">Abort</string>
|
<string name="bootstrap_error_abort">Abort</string>
|
||||||
<string name="bootstrap_error_try_again">Try again</string>
|
<string name="bootstrap_error_try_again">Try again</string>
|
||||||
<string name="bootstrap_error_not_primary_user_message">Termux can only be installed on the primary user account.</string>
|
<string name="bootstrap_error_not_primary_user_message">Termux can only be installed on the primary user account.</string>
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ public class TermuxActivityTest {
|
|||||||
|
|
||||||
assertUrlsAre("hello http://example.com world and http://more.example.com with secure https://more.example.com",
|
assertUrlsAre("hello http://example.com world and http://more.example.com with secure https://more.example.com",
|
||||||
"http://example.com", "http://more.example.com", "https://more.example.com");
|
"http://example.com", "http://more.example.com", "https://more.example.com");
|
||||||
|
|
||||||
|
assertUrlsAre("hello https://example.com/#bar https://example.com/foo#bar",
|
||||||
|
"https://example.com/#bar", "https://example.com/foo#bar");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ buildscript {
|
|||||||
google()
|
google()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:3.5.3'
|
classpath 'com.android.tools.build:gradle:3.6.3'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,3 +13,4 @@
|
|||||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
# org.gradle.parallel=true
|
# org.gradle.parallel=true
|
||||||
org.gradle.jvmargs=-Xmx2048M
|
org.gradle.jvmargs=-Xmx2048M
|
||||||
|
android.useAndroidX=true
|
||||||
|
|||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-6.2.2-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
3
gradlew.bat
vendored
3
gradlew.bat
vendored
@@ -29,6 +29,9 @@ if "%DIRNAME%" == "" set DIRNAME=.
|
|||||||
set APP_BASE_NAME=%~n0
|
set APP_BASE_NAME=%~n0
|
||||||
set APP_HOME=%DIRNAME%
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ ext {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 28
|
compileSdkVersion 28
|
||||||
|
ndkVersion '21.3.6528147'
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
|
|||||||
@@ -45,11 +45,19 @@ public final class TerminalBuffer {
|
|||||||
return getSelectedText(0, -getActiveTranscriptRows(), mColumns, mScreenRows, false).trim();
|
return getSelectedText(0, -getActiveTranscriptRows(), mColumns, mScreenRows, false).trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getTranscriptTextWithFullLinesJoined() {
|
||||||
|
return getSelectedText(0, -getActiveTranscriptRows(), mColumns, mScreenRows, true, true).trim();
|
||||||
|
}
|
||||||
|
|
||||||
public String getSelectedText(int selX1, int selY1, int selX2, int selY2) {
|
public String getSelectedText(int selX1, int selY1, int selX2, int selY2) {
|
||||||
return getSelectedText(selX1, selY1, selX2, selY2, true);
|
return getSelectedText(selX1, selY1, selX2, selY2, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getSelectedText(int selX1, int selY1, int selX2, int selY2, boolean joinBackLines) {
|
public String getSelectedText(int selX1, int selY1, int selX2, int selY2, boolean joinBackLines) {
|
||||||
|
return getSelectedText(selX1, selY1, selX2, selY2, true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSelectedText(int selX1, int selY1, int selX2, int selY2, boolean joinBackLines, boolean joinFullLines) {
|
||||||
final StringBuilder builder = new StringBuilder();
|
final StringBuilder builder = new StringBuilder();
|
||||||
final int columns = mColumns;
|
final int columns = mColumns;
|
||||||
|
|
||||||
@@ -87,7 +95,8 @@ public final class TerminalBuffer {
|
|||||||
}
|
}
|
||||||
if (lastPrintingCharIndex != -1)
|
if (lastPrintingCharIndex != -1)
|
||||||
builder.append(line, x1Index, lastPrintingCharIndex - x1Index + 1);
|
builder.append(line, x1Index, lastPrintingCharIndex - x1Index + 1);
|
||||||
if ((!joinBackLines || !rowLineWrap)
|
boolean lineFillsWidth = lastPrintingCharIndex == x2Index - 1;
|
||||||
|
if ((!joinBackLines || !rowLineWrap) && (!joinFullLines || !lineFillsWidth)
|
||||||
&& row < selY2 && row < mScreenRows - 1) builder.append('\n');
|
&& row < selY2 && row < mScreenRows - 1) builder.append('\n');
|
||||||
}
|
}
|
||||||
return builder.toString();
|
return builder.toString();
|
||||||
|
|||||||
@@ -37,4 +37,12 @@ public class ScreenBufferTest extends TerminalTestCase {
|
|||||||
withTerminalSized(5, 3).enterString("ABCDE\r\nFGHIJ").assertLinesAre("ABCDE", "FGHIJ", " ");
|
withTerminalSized(5, 3).enterString("ABCDE\r\nFGHIJ").assertLinesAre("ABCDE", "FGHIJ", " ");
|
||||||
assertEquals("ABCDE\nFG", mTerminal.getSelectedText(0, 0, 1, 1));
|
assertEquals("ABCDE\nFG", mTerminal.getSelectedText(0, 0, 1, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void testGetSelectedTextJoinFullLines() {
|
||||||
|
withTerminalSized(5, 3).enterString("ABCDE\r\nFG");
|
||||||
|
assertEquals("ABCDEFG", mTerminal.getScreen().getSelectedText(0, 0, 1, 1, true, true));
|
||||||
|
|
||||||
|
withTerminalSized(5, 3).enterString("ABC\r\nFG");
|
||||||
|
assertEquals("ABC\nFG", mTerminal.getScreen().getSelectedText(0, 0, 1, 1, true, true));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ public final class TerminalRenderer {
|
|||||||
|
|
||||||
long lastRunStyle = 0;
|
long lastRunStyle = 0;
|
||||||
boolean lastRunInsideCursor = false;
|
boolean lastRunInsideCursor = false;
|
||||||
|
boolean lastRunInsideSelection = false;
|
||||||
int lastRunStartColumn = -1;
|
int lastRunStartColumn = -1;
|
||||||
int lastRunStartIndex = 0;
|
int lastRunStartIndex = 0;
|
||||||
boolean lastRunFontWidthMismatch = false;
|
boolean lastRunFontWidthMismatch = false;
|
||||||
@@ -98,7 +99,8 @@ public final class TerminalRenderer {
|
|||||||
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 = (cursorX == column || (codePointWcWidth == 2 && cursorX == column + 1));
|
||||||
|
final boolean insideSelection = column >= selx1 && column <= selx2;
|
||||||
final long 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().
|
||||||
@@ -109,7 +111,7 @@ public final class TerminalRenderer {
|
|||||||
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 || insideSelection != lastRunInsideSelection || 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 {
|
||||||
@@ -118,11 +120,12 @@ public final class TerminalRenderer {
|
|||||||
int cursorColor = lastRunInsideCursor ? mEmulator.mColors.mCurrentColors[TextStyle.COLOR_INDEX_CURSOR] : 0;
|
int cursorColor = lastRunInsideCursor ? mEmulator.mColors.mCurrentColors[TextStyle.COLOR_INDEX_CURSOR] : 0;
|
||||||
drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun,
|
drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun,
|
||||||
lastRunStartIndex, charsSinceLastRun, measuredWidthForRun,
|
lastRunStartIndex, charsSinceLastRun, measuredWidthForRun,
|
||||||
cursorColor, cursorShape, lastRunStyle, reverseVideo);
|
cursorColor, cursorShape, lastRunStyle, reverseVideo || lastRunInsideSelection);
|
||||||
}
|
}
|
||||||
measuredWidthForRun = 0.f;
|
measuredWidthForRun = 0.f;
|
||||||
lastRunStyle = style;
|
lastRunStyle = style;
|
||||||
lastRunInsideCursor = insideCursor;
|
lastRunInsideCursor = insideCursor;
|
||||||
|
lastRunInsideSelection = insideSelection;
|
||||||
lastRunStartColumn = column;
|
lastRunStartColumn = column;
|
||||||
lastRunStartIndex = currentCharIndex;
|
lastRunStartIndex = currentCharIndex;
|
||||||
lastRunFontWidthMismatch = fontWidthMismatch;
|
lastRunFontWidthMismatch = fontWidthMismatch;
|
||||||
@@ -141,7 +144,7 @@ public final class TerminalRenderer {
|
|||||||
final int charsSinceLastRun = currentCharIndex - lastRunStartIndex;
|
final int charsSinceLastRun = currentCharIndex - lastRunStartIndex;
|
||||||
int cursorColor = lastRunInsideCursor ? mEmulator.mColors.mCurrentColors[TextStyle.COLOR_INDEX_CURSOR] : 0;
|
int cursorColor = lastRunInsideCursor ? mEmulator.mColors.mCurrentColors[TextStyle.COLOR_INDEX_CURSOR] : 0;
|
||||||
drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun, lastRunStartIndex, charsSinceLastRun,
|
drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun, lastRunStartIndex, charsSinceLastRun,
|
||||||
measuredWidthForRun, cursorColor, cursorShape, lastRunStyle, reverseVideo);
|
measuredWidthForRun, cursorColor, cursorShape, lastRunStyle, reverseVideo || lastRunInsideSelection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -558,13 +558,13 @@ public final class TerminalView extends View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final int metaState = event.getMetaState();
|
final int metaState = event.getMetaState();
|
||||||
final boolean controlDownFromEvent = event.isCtrlPressed();
|
final boolean controlDown = event.isCtrlPressed() || mClient.readControlKey();
|
||||||
final boolean leftAltDownFromEvent = (metaState & KeyEvent.META_ALT_LEFT_ON) != 0;
|
final boolean leftAltDown = (metaState & KeyEvent.META_ALT_LEFT_ON) != 0 || mClient.readAltKey();
|
||||||
final boolean rightAltDownFromEvent = (metaState & KeyEvent.META_ALT_RIGHT_ON) != 0;
|
final boolean rightAltDownFromEvent = (metaState & KeyEvent.META_ALT_RIGHT_ON) != 0;
|
||||||
|
|
||||||
int keyMod = 0;
|
int keyMod = 0;
|
||||||
if (controlDownFromEvent) keyMod |= KeyHandler.KEYMOD_CTRL;
|
if (controlDown) keyMod |= KeyHandler.KEYMOD_CTRL;
|
||||||
if (event.isAltPressed()) keyMod |= KeyHandler.KEYMOD_ALT;
|
if (event.isAltPressed() || leftAltDown) keyMod |= KeyHandler.KEYMOD_ALT;
|
||||||
if (event.isShiftPressed()) keyMod |= KeyHandler.KEYMOD_SHIFT;
|
if (event.isShiftPressed()) keyMod |= KeyHandler.KEYMOD_SHIFT;
|
||||||
if (!event.isFunctionPressed() && handleKeyCode(keyCode, keyMod)) {
|
if (!event.isFunctionPressed() && handleKeyCode(keyCode, keyMod)) {
|
||||||
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "handleKeyCode() took key event");
|
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "handleKeyCode() took key event");
|
||||||
@@ -592,7 +592,7 @@ public final class TerminalView extends View {
|
|||||||
if ((result & KeyCharacterMap.COMBINING_ACCENT) != 0) {
|
if ((result & KeyCharacterMap.COMBINING_ACCENT) != 0) {
|
||||||
// If entered combining accent previously, write it out:
|
// If entered combining accent previously, write it out:
|
||||||
if (mCombiningAccent != 0)
|
if (mCombiningAccent != 0)
|
||||||
inputCodePoint(mCombiningAccent, controlDownFromEvent, leftAltDownFromEvent);
|
inputCodePoint(mCombiningAccent, controlDown, leftAltDown);
|
||||||
mCombiningAccent = result & KeyCharacterMap.COMBINING_ACCENT_MASK;
|
mCombiningAccent = result & KeyCharacterMap.COMBINING_ACCENT_MASK;
|
||||||
} else {
|
} else {
|
||||||
if (mCombiningAccent != 0) {
|
if (mCombiningAccent != 0) {
|
||||||
@@ -600,7 +600,7 @@ public final class TerminalView extends View {
|
|||||||
if (combinedChar > 0) result = combinedChar;
|
if (combinedChar > 0) result = combinedChar;
|
||||||
mCombiningAccent = 0;
|
mCombiningAccent = 0;
|
||||||
}
|
}
|
||||||
inputCodePoint(result, controlDownFromEvent, leftAltDownFromEvent);
|
inputCodePoint(result, controlDown, leftAltDown);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mCombiningAccent != oldCombiningAccent) invalidate();
|
if (mCombiningAccent != oldCombiningAccent) invalidate();
|
||||||
@@ -608,7 +608,7 @@ public final class TerminalView extends View {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void inputCodePoint(int codePoint, boolean controlDownFromEvent, boolean leftAltDownFromEvent) {
|
public void inputCodePoint(int codePoint, boolean controlDownFromEvent, boolean leftAltDownFromEvent) {
|
||||||
if (LOG_KEY_EVENTS) {
|
if (LOG_KEY_EVENTS) {
|
||||||
Log.i(EmulatorDebug.LOG_TAG, "inputCodePoint(codePoint=" + codePoint + ", controlDownFromEvent=" + controlDownFromEvent + ", leftAltDownFromEvent="
|
Log.i(EmulatorDebug.LOG_TAG, "inputCodePoint(codePoint=" + codePoint + ", controlDownFromEvent=" + controlDownFromEvent + ", leftAltDownFromEvent="
|
||||||
+ leftAltDownFromEvent + ")");
|
+ leftAltDownFromEvent + ")");
|
||||||
@@ -973,12 +973,12 @@ public final class TerminalView extends View {
|
|||||||
return mContainer.isShowing();
|
return mContainer.isShowing();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkChangedOrientation() {
|
private void checkChangedOrientation(int posX, boolean force) {
|
||||||
if (!mIsDragging) {
|
if (!mIsDragging && !force) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
long millis = SystemClock.currentThreadTimeMillis();
|
long millis = SystemClock.currentThreadTimeMillis();
|
||||||
if (millis - mLastTime < 50) {
|
if (millis - mLastTime < 50 && !force) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
mLastTime = millis;
|
mLastTime = millis;
|
||||||
@@ -1003,10 +1003,7 @@ public final class TerminalView extends View {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final int[] coords = mTempCoords;
|
if (posX - mHandleWidth < clip.left) {
|
||||||
hostView.getLocationInWindow(coords);
|
|
||||||
final int posX = coords[0] + mPointX;
|
|
||||||
if (posX < clip.left) {
|
|
||||||
changeOrientation(RIGHT);
|
changeOrientation(RIGHT);
|
||||||
} else if (posX + mHandleWidth > clip.right) {
|
} else if (posX + mHandleWidth > clip.right) {
|
||||||
changeOrientation(LEFT);
|
changeOrientation(LEFT);
|
||||||
@@ -1050,13 +1047,14 @@ public final class TerminalView extends View {
|
|||||||
posY >= clip.top && posY <= clip.bottom;
|
posY >= clip.top && posY <= clip.bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void moveTo(int x, int y) {
|
private void moveTo(int x, int y, boolean forceOrientationCheck) {
|
||||||
mPointX = x;
|
float oldHotspotX = mHotspotX;
|
||||||
|
checkChangedOrientation(x, forceOrientationCheck);
|
||||||
|
mPointX = (int) (x - (isShowing() ? oldHotspotX : mHotspotX));
|
||||||
mPointY = y;
|
mPointY = y;
|
||||||
checkChangedOrientation();
|
|
||||||
if (isPositionVisible()) {
|
if (isPositionVisible()) {
|
||||||
int[] coords = null;
|
int[] coords = null;
|
||||||
if (mContainer.isShowing()) {
|
if (isShowing()) {
|
||||||
coords = mTempCoords;
|
coords = mTempCoords;
|
||||||
TerminalView.this.getLocationInWindow(coords);
|
TerminalView.this.getLocationInWindow(coords);
|
||||||
int x1 = coords[0] + mPointX;
|
int x1 = coords[0] + mPointX;
|
||||||
@@ -1138,10 +1136,10 @@ public final class TerminalView extends View {
|
|||||||
return mIsDragging;
|
return mIsDragging;
|
||||||
}
|
}
|
||||||
|
|
||||||
void positionAtCursor(final int cx, final int cy) {
|
void positionAtCursor(final int cx, final int cy, boolean forceOrientationCheck) {
|
||||||
int left = (int) (getPointX(cx) - mHotspotX);
|
int left = getPointX(cx);
|
||||||
int bottom = getPointY(cy + 1);
|
int bottom = getPointY(cy + 1);
|
||||||
moveTo(left, bottom);
|
moveTo(left, bottom, forceOrientationCheck);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1162,9 +1160,8 @@ public final class TerminalView extends View {
|
|||||||
|
|
||||||
public void show() {
|
public void show() {
|
||||||
mIsShowing = true;
|
mIsShowing = true;
|
||||||
updatePosition();
|
mStartHandle.positionAtCursor(mSelX1, mSelY1, true);
|
||||||
mStartHandle.show();
|
mEndHandle.positionAtCursor(mSelX2 + 1, mSelY2, true);
|
||||||
mEndHandle.show();
|
|
||||||
|
|
||||||
final ActionMode.Callback callback = new ActionMode.Callback() {
|
final ActionMode.Callback callback = new ActionMode.Callback() {
|
||||||
@Override
|
@Override
|
||||||
@@ -1240,7 +1237,7 @@ public final class TerminalView extends View {
|
|||||||
public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
|
public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
|
||||||
int x1 = Math.round(mSelX1 * mRenderer.mFontWidth);
|
int x1 = Math.round(mSelX1 * mRenderer.mFontWidth);
|
||||||
int x2 = Math.round(mSelX2 * mRenderer.mFontWidth);
|
int x2 = Math.round(mSelX2 * mRenderer.mFontWidth);
|
||||||
int y1 = Math.round((mSelY1 - mTopRow) * mRenderer.mFontLineSpacing);
|
int y1 = Math.round((mSelY1 - 1 - mTopRow) * mRenderer.mFontLineSpacing);
|
||||||
int y2 = Math.round((mSelY2 + 1 - mTopRow) * mRenderer.mFontLineSpacing);
|
int y2 = Math.round((mSelY2 + 1 - mTopRow) * mRenderer.mFontLineSpacing);
|
||||||
|
|
||||||
|
|
||||||
@@ -1395,9 +1392,9 @@ public final class TerminalView extends View {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
mStartHandle.positionAtCursor(mSelX1, mSelY1);
|
mStartHandle.positionAtCursor(mSelX1, mSelY1, false);
|
||||||
|
|
||||||
mEndHandle.positionAtCursor(mSelX2 + 1, mSelY2); //bug
|
mEndHandle.positionAtCursor(mSelX2 + 1, mSelY2, false);
|
||||||
|
|
||||||
if (mActionMode != null) {
|
if (mActionMode != null) {
|
||||||
mActionMode.invalidate();
|
mActionMode.invalidate();
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.0 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
@@ -1,4 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
|
android:width="48dp"
|
||||||
android:src="@drawable/text_select_handle_left_mtrl_alpha"
|
android:height="24dp"
|
||||||
android:tint="#2196F3" />
|
android:viewportWidth="132"
|
||||||
|
android:viewportHeight="66">
|
||||||
|
<path
|
||||||
|
android:pathData="M52.3,1.6c-5.7,2.1 -12.9,8.6 -16,14.8 -2.2,4.1 -2.8,6.9 -3.1,14.3 -0.6,12.6 1.3,17.8 9.3,25.8 8,8 13.2,9.9 25.8,9.3 11.1,-0.5 17.3,-3.2 23.5,-10.3 6.5,-7.4 7.2,-10.8 7.2,-34.7l0,-20.8 -21.2,0.1c-16.1,-0 -22.3,0.4 -25.5,1.5z"
|
||||||
|
android:fillColor="#2196F3"
|
||||||
|
android:strokeColor="#00000000"/>
|
||||||
|
</vector>
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
|
android:width="48dp"
|
||||||
android:src="@drawable/text_select_handle_right_mtrl_alpha"
|
android:height="24dp"
|
||||||
android:tint="#2196F3" />
|
android:viewportWidth="132"
|
||||||
|
android:viewportHeight="66">
|
||||||
|
<path
|
||||||
|
android:pathData="M33,20.8c0,23.9 0.7,27.3 7.2,34.7 6.2,7.1 12.4,9.8 23.5,10.3 12.6,0.6 17.8,-1.3 25.8,-9.3 8,-8 9.9,-13.2 9.3,-25.8 -0.5,-11.1 -3.2,-17.3 -10.3,-23.5 -7.4,-6.5 -10.8,-7.2 -34.7,-7.2l-20.8,-0 0,20.8z"
|
||||||
|
android:fillColor="#2196F3"
|
||||||
|
android:strokeColor="#00000000"/>
|
||||||
|
</vector>
|
||||||
|
|||||||
Reference in New Issue
Block a user