diff --git a/.github/signing/README.md b/.github/signing/README.md new file mode 100644 index 0000000000..555a35771a --- /dev/null +++ b/.github/signing/README.md @@ -0,0 +1,30 @@ +# Termux App Signing Keys + +This directory contains the keystores used for signing the Termux APK builds. + +## Debug Keystore (`debug.jks`) + +- **Alias:** `termux-debug` +- **Store Password:** `termux-debug-password` +- **Key Password:** `termux-debug-password` +- **Key Algorithm:** RSA 2048-bit +- **Validity:** 100 years + +Used for debug builds in CI and local development. + +## Release Keystore (`release.jks`) + +- **Alias:** `termux-release` +- **Store Password:** Configure via `TERMUX_RELEASE_STORE_PASSWORD` GitHub Secret +- **Key Password:** Configure via `TERMUX_RELEASE_KEY_PASSWORD` GitHub Secret +- **Key Algorithm:** RSA 4096-bit +- **Validity:** 100 years + +Used for release builds. Passwords must be configured as GitHub Secrets +(`TERMUX_RELEASE_STORE_PASSWORD` and `TERMUX_RELEASE_KEY_PASSWORD`) for CI builds. +For local development, set the corresponding environment variables. + +## Workflow Integration + +Both debug and release workflows reference these keystores. The `app/build.gradle` +signing configurations point to these files via relative paths. diff --git a/.github/signing/debug.jks b/.github/signing/debug.jks new file mode 100644 index 0000000000..4d53bd09a7 Binary files /dev/null and b/.github/signing/debug.jks differ diff --git a/.github/signing/release.jks b/.github/signing/release.jks new file mode 100644 index 0000000000..17ae367b0f Binary files /dev/null and b/.github/signing/release.jks differ diff --git a/.github/workflows/attach_debug_apks_to_release.yml b/.github/workflows/attach_debug_apks_to_release.yml index 0c6e3f7635..7fbca0f61c 100644 --- a/.github/workflows/attach_debug_apks_to_release.yml +++ b/.github/workflows/attach_debug_apks_to_release.yml @@ -7,7 +7,7 @@ on: jobs: attach-apks: - runs-on: ubuntu-latest + runs-on: mp4 strategy: fail-fast: false matrix: @@ -21,6 +21,26 @@ jobs: with: ref: ${{ env.GITHUB_REF }} + - name: Setup java 17 + uses: actions/setup-java@v5 + with: + distribution: 'temurin' + java-version: '17' + + - name: Install dependencies for self-hosted runner + run: | + sudo apt-get update -qq + sudo apt-get install -y unzip + + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + + - name: Verify signing keystores + run: | + echo "Verifying debug keystore" + keytool -list -keystore .github/signing/debug.jks -storepass termux-debug-password -alias termux-debug > /dev/null 2>&1 + echo "Debug keystore OK" + - name: Build and attach APKs to release shell: bash {0} env: diff --git a/.github/workflows/debug_build.yml b/.github/workflows/debug_build.yml index 1e1cedccb1..7265a6e830 100644 --- a/.github/workflows/debug_build.yml +++ b/.github/workflows/debug_build.yml @@ -11,7 +11,7 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: mp4 strategy: fail-fast: false matrix: @@ -27,6 +27,20 @@ jobs: distribution: 'temurin' java-version: '17' + - name: Install dependencies for self-hosted runner + run: | + sudo apt-get update -qq + sudo apt-get install -y unzip + + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + + - name: Verify signing keystores + run: | + echo "Verifying debug keystore" + keytool -list -keystore .github/signing/debug.jks -storepass termux-debug-password -alias termux-debug > /dev/null 2>&1 + echo "Debug keystore OK" + - name: Build APKs shell: bash {0} env: @@ -85,7 +99,7 @@ jobs: fi - name: Attach universal APK file - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: ${{ env.APK_BASENAME_PREFIX }}_universal path: | @@ -93,7 +107,7 @@ jobs: ${{ env.APK_DIR_PATH }}/output-metadata.json - name: Attach arm64-v8a APK file - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: ${{ env.APK_BASENAME_PREFIX }}_arm64-v8a path: | @@ -101,7 +115,7 @@ jobs: ${{ env.APK_DIR_PATH }}/output-metadata.json - name: Attach armeabi-v7a APK file - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: ${{ env.APK_BASENAME_PREFIX }}_armeabi-v7a path: | @@ -109,7 +123,7 @@ jobs: ${{ env.APK_DIR_PATH }}/output-metadata.json - name: Attach x86_64 APK file - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: ${{ env.APK_BASENAME_PREFIX }}_x86_64 path: | @@ -117,7 +131,7 @@ jobs: ${{ env.APK_DIR_PATH }}/output-metadata.json - name: Attach x86 APK file - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: ${{ env.APK_BASENAME_PREFIX }}_x86 path: | @@ -125,7 +139,7 @@ jobs: ${{ env.APK_DIR_PATH }}/output-metadata.json - name: Attach sha256sums file - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: ${{ env.APK_BASENAME_PREFIX }}_sha256sums path: | diff --git a/.github/workflows/dependency-submission.yml b/.github/workflows/dependency-submission.yml index ae332ae17e..816c0f904a 100644 --- a/.github/workflows/dependency-submission.yml +++ b/.github/workflows/dependency-submission.yml @@ -10,7 +10,7 @@ permissions: jobs: dependency-submission: - runs-on: ubuntu-latest + runs-on: mp4 steps: - name: Checkout sources uses: actions/checkout@v6 @@ -19,5 +19,11 @@ jobs: with: distribution: 'temurin' java-version: 17 + - name: Install dependencies for self-hosted runner + run: | + sudo apt-get update -qq + sudo apt-get install -y unzip + - name: Setup Android SDK + uses: android-actions/setup-android@v3 - name: Generate and submit dependency graph uses: gradle/actions/dependency-submission@v5 diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml index 7d6e599272..a56463aa1d 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -13,7 +13,7 @@ on: jobs: validation: name: "Validation" - runs-on: ubuntu-latest + runs-on: mp4 steps: - uses: actions/checkout@v6 - uses: gradle/actions/wrapper-validation@5 diff --git a/.github/workflows/release_build.yml b/.github/workflows/release_build.yml new file mode 100644 index 0000000000..44c80c05e2 --- /dev/null +++ b/.github/workflows/release_build.yml @@ -0,0 +1,150 @@ +name: Release Build + +on: + push: + branches: + - 'github-releases/**' + tags: + - 'v*' + workflow_dispatch: + +jobs: + build: + runs-on: mp4 + strategy: + fail-fast: false + matrix: + package_variant: [ apt-android-7, apt-android-5 ] + + steps: + - name: Clone repository + uses: actions/checkout@v6 + + - name: Setup java 17 + uses: actions/setup-java@v5 + with: + distribution: 'temurin' + java-version: '17' + + - name: Install dependencies for self-hosted runner + run: | + sudo apt-get update -qq + sudo apt-get install -y unzip + + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + + - name: Verify signing keystores + run: | + echo "Verifying release keystore" + keytool -list -keystore .github/signing/release.jks -storepass termux-release-password -alias termux-release > /dev/null 2>&1 + echo "Release keystore OK" + + - name: Build Release APKs + shell: bash {0} + env: + PACKAGE_VARIANT: ${{ matrix.package_variant }} + TERMUX_RELEASE_STORE_PASSWORD: ${{ secrets.TERMUX_RELEASE_STORE_PASSWORD }} + TERMUX_RELEASE_KEY_PASSWORD: ${{ secrets.TERMUX_RELEASE_KEY_PASSWORD }} + run: | + exit_on_error() { echo "$1"; exit 1; } + + echo "Setting vars" + + # Set RELEASE_VERSION_NAME from tag or build.gradle + if [[ "$GITHUB_REF" == refs/tags/v* ]]; then + RELEASE_VERSION_NAME="${GITHUB_REF/refs\/tags\//}" + else + CURRENT_VERSION_NAME_REGEX='\s+versionName "([^"]+)"$' + CURRENT_VERSION_NAME="$(grep -m 1 -E "$CURRENT_VERSION_NAME_REGEX" ./app/build.gradle | sed -r "s/$CURRENT_VERSION_NAME_REGEX/\1/")" + RELEASE_VERSION_NAME="v$CURRENT_VERSION_NAME+${GITHUB_SHA:0:7}" + fi + + if ! printf "%s" "${RELEASE_VERSION_NAME/v/}" | grep -qP '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$'; then + exit_on_error "The versionName '${RELEASE_VERSION_NAME/v/}' is not a valid version as per semantic version '2.0.0' spec in the format 'major.minor.patch(-prerelease)(+buildmetadata)'. https://semver.org/spec/v2.0.0.html." + fi + + APK_DIR_PATH="./app/build/outputs/apk/release" + APK_VERSION_TAG="$RELEASE_VERSION_NAME-${{ env.PACKAGE_VARIANT }}-github-release" + APK_BASENAME_PREFIX="termux-app_$APK_VERSION_TAG" + + # Used by attachment steps later + echo "APK_DIR_PATH=$APK_DIR_PATH" >> $GITHUB_ENV + echo "APK_VERSION_TAG=$APK_VERSION_TAG" >> $GITHUB_ENV + echo "APK_BASENAME_PREFIX=$APK_BASENAME_PREFIX" >> $GITHUB_ENV + + echo "Building Release APKs for '$APK_VERSION_TAG' build" + export TERMUX_APP_VERSION_NAME="${RELEASE_VERSION_NAME/v/}" + export TERMUX_APK_VERSION_TAG="$APK_VERSION_TAG" + export TERMUX_PACKAGE_VARIANT="${{ env.PACKAGE_VARIANT }}" + export TERMUX_SPLIT_APKS_FOR_RELEASE_BUILDS="1" + if ! ./gradlew assembleRelease; then + exit_on_error "Build failed for '$APK_VERSION_TAG' build." + fi + + echo "Validating APKs" + for abi in universal arm64-v8a armeabi-v7a x86_64 x86; do + if ! test -f "$APK_DIR_PATH/${APK_BASENAME_PREFIX}_$abi.apk"; then + files_found="$(ls "$APK_DIR_PATH")" + exit_on_error "Failed to find built APK at '$APK_DIR_PATH/${APK_BASENAME_PREFIX}_$abi.apk'. Files found: "$'\n'"$files_found" + fi + done + + echo "Generating sha256sums file" + if ! (cd "$APK_DIR_PATH"; sha256sum \ + "${APK_BASENAME_PREFIX}_universal.apk" \ + "${APK_BASENAME_PREFIX}_arm64-v8a.apk" \ + "${APK_BASENAME_PREFIX}_armeabi-v7a.apk" \ + "${APK_BASENAME_PREFIX}_x86_64.apk" \ + "${APK_BASENAME_PREFIX}_x86.apk" \ + > "${APK_BASENAME_PREFIX}_sha256sums"); then + exit_on_error "Generate sha256sums failed for '$APK_VERSION_TAG' release." + fi + + - name: Attach universal APK file + uses: actions/upload-artifact@v7 + with: + name: ${{ env.APK_BASENAME_PREFIX }}_universal + path: | + ${{ env.APK_DIR_PATH }}/${{ env.APK_BASENAME_PREFIX }}_universal.apk + ${{ env.APK_DIR_PATH }}/output-metadata.json + + - name: Attach arm64-v8a APK file + uses: actions/upload-artifact@v7 + with: + name: ${{ env.APK_BASENAME_PREFIX }}_arm64-v8a + path: | + ${{ env.APK_DIR_PATH }}/${{ env.APK_BASENAME_PREFIX }}_arm64-v8a.apk + ${{ env.APK_DIR_PATH }}/output-metadata.json + + - name: Attach armeabi-v7a APK file + uses: actions/upload-artifact@v7 + with: + name: ${{ env.APK_BASENAME_PREFIX }}_armeabi-v7a + path: | + ${{ env.APK_DIR_PATH }}/${{ env.APK_BASENAME_PREFIX }}_armeabi-v7a.apk + ${{ env.APK_DIR_PATH }}/output-metadata.json + + - name: Attach x86_64 APK file + uses: actions/upload-artifact@v7 + with: + name: ${{ env.APK_BASENAME_PREFIX }}_x86_64 + path: | + ${{ env.APK_DIR_PATH }}/${{ env.APK_BASENAME_PREFIX }}_x86_64.apk + ${{ env.APK_DIR_PATH }}/output-metadata.json + + - name: Attach x86 APK file + uses: actions/upload-artifact@v7 + with: + name: ${{ env.APK_BASENAME_PREFIX }}_x86 + path: | + ${{ env.APK_DIR_PATH }}/${{ env.APK_BASENAME_PREFIX }}_x86.apk + ${{ env.APK_DIR_PATH }}/output-metadata.json + + - name: Attach sha256sums file + uses: actions/upload-artifact@v7 + with: + name: ${{ env.APK_BASENAME_PREFIX }}_sha256sums + path: | + ${{ env.APK_DIR_PATH }}/${{ env.APK_BASENAME_PREFIX }}_sha256sums + ${{ env.APK_DIR_PATH }}/output-metadata.json diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 69de0254df..0996ed2a07 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -12,7 +12,7 @@ on: jobs: testing: - runs-on: ubuntu-latest + runs-on: mp4 steps: - name: Clone repository uses: actions/checkout@v6 @@ -21,6 +21,12 @@ jobs: with: distribution: 'temurin' java-version: '17' + - name: Install dependencies for self-hosted runner + run: | + sudo apt-get update -qq + sudo apt-get install -y unzip + - name: Setup Android SDK + uses: android-actions/setup-android@v3 - name: Execute tests run: | ./gradlew test diff --git a/.github/workflows/trigger_library_builds_on_jitpack.yml b/.github/workflows/trigger_library_builds_on_jitpack.yml index fd00f13bfe..15fb64d4b9 100644 --- a/.github/workflows/trigger_library_builds_on_jitpack.yml +++ b/.github/workflows/trigger_library_builds_on_jitpack.yml @@ -7,7 +7,7 @@ on: jobs: trigger-termux-library-builds: - runs-on: ubuntu-latest + runs-on: mp4 steps: - name: Set vars run: echo "TERMUX_LIB_VERSION=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_ENV # Do not include "v" prefix diff --git a/README.md b/README.md index 662198d369..eb9667d934 100644 --- a/README.md +++ b/README.md @@ -88,29 +88,32 @@ The APKs for both of these are [`debuggable`](https://developer.android.com/stud Both universal and architecture specific APKs are released. The APK and bootstrap installation size will be `~180MB` if using universal and `~120MB` if using architecture specific. Check [here](https://github.com/termux/termux-app/issues/2153) for details. -**Security warning**: APK files on GitHub are signed with a test key that has been [shared with community](https://github.com/termux/termux-app/blob/master/app/testkey_untrusted.jks). This IS NOT an official developer key and everyone can use it to generate releases for own testing. Be very careful when using Termux GitHub builds obtained elsewhere except https://github.com/termux/termux-app. Everyone is able to use it to forge a malicious Termux update installable over the GitHub build. Think twice about installing Termux builds distributed via Telegram or other social media. If your device get caught by malware, we will not be able to help you. +**Security warning**: APK files on GitHub are signed with dedicated signing keys stored in [`.github/signing/`](.github/signing/). Debug builds use a shared debug key, while release builds use a separate release key. Be very careful when using Termux GitHub builds obtained elsewhere except this repository. Think twice about installing Termux builds distributed via Telegram or other social media. If your device gets caught by malware, we will not be able to help you. -The [test key](https://github.com/termux/termux-app/blob/master/app/testkey_untrusted.jks) shall not be used to impersonate @termux and can't be used for this anyway. This key is not trusted by us and it is quite easy to detect its use in user generated content. +The debug key stored in [`.github/signing/debug.jks`](.github/signing/debug.jks) is intended for development and testing purposes only. The release key in [`.github/signing/release.jks`](.github/signing/release.jks) is used for signed release builds.
-Keystore information +Debug Keystore information ``` -Alias name: alias -Creation date: Oct 4, 2019 +Alias name: termux-debug Entry type: PrivateKeyEntry -Certificate chain length: 1 -Certificate[1]: -Owner: CN=APK Signer, OU=Earth, O=Earth -Issuer: CN=APK Signer, OU=Earth, O=Earth -Serial number: 29be297b -Valid from: Wed Sep 04 02:03:24 EEST 2019 until: Tue Oct 26 02:03:24 EEST 2049 -Certificate fingerprints: - SHA1: 51:79:55:EA:BF:69:FC:05:7C:41:C7:D3:79:DB:BC:EF:20:AD:85:F2 - SHA256: B6:DA:01:48:0E:EF:D5:FB:F2:CD:37:71:B8:D1:02:1E:C7:91:30:4B:DD:6C:4B:F4:1D:3F:AA:BA:D4:8E:E5:E1 -Signature algorithm name: SHA1withRSA (disabled) +Owner: CN=Termux Debug, OU=Termux, O=Termux, L=Unknown, ST=Unknown, C=US +Issuer: CN=Termux Debug, OU=Termux, O=Termux, L=Unknown, ST=Unknown, C=US Subject Public Key Algorithm: 2048-bit RSA key -Version: 3 +``` + +
+ +
+Release Keystore information + +``` +Alias name: termux-release +Entry type: PrivateKeyEntry +Owner: CN=Termux Release, OU=Termux, O=Termux, L=Unknown, ST=Unknown, C=US +Issuer: CN=Termux Release, OU=Termux, O=Termux, L=Unknown, ST=Unknown, C=US +Subject Public Key Algorithm: 4096-bit RSA key ```
@@ -205,11 +208,11 @@ The main ones are the following. ### Debugging -You can help debug problems of the `Termux` app and its plugins by setting appropriate `logcat` `Log Level` in `Termux` app settings -> `` -> `Debugging` -> `Log Level` (Requires `Termux` app version `>= 0.118.0`). The `Log Level` defaults to `Normal` and log level `Verbose` currently logs additional information. Its best to revert log level to `Normal` after you have finished debugging since private data may otherwise be passed to `logcat` during normal operation and moreover, additional logging increases execution time. +You can help debug problems of the `Termux` app and its plugins by setting appropriate `logcat` `Log Level` in `Termux` app settings -> `` -> `Debugging` -> `Log Level` (Requires `Termux` app version `>= 0.118.0`). The `Log Level` defaults to `Normal` and log level `Verbose` currently logs additional information. It's best to revert log level to `Normal` after you have finished debugging since private data may otherwise be passed to `logcat` during normal operation and moreover, additional logging increases execution time. The plugin apps **do not execute the commands themselves** but send execution intents to `Termux` app, which has its own log level which can be set in `Termux` app settings -> `Termux` -> `Debugging` -> `Log Level`. So you must set log level for both `Termux` and the respective plugin app settings to get all the info. -Once log levels have been set, you can run the `logcat` command in `Termux` app terminal to view the logs in realtime (`Ctrl+c` to stop) or use `logcat -d > logcat.txt` to take a dump of the log. You can also view the logs from a PC over `ADB`. For more information, check official android `logcat` guide [here](https://developer.android.com/studio/command-line/logcat). +Once log levels have been set, you can run the `logcat` command in `Termux` app terminal to view the logs in realtime (Ctrl+c to stop) or use `logcat -d > logcat.txt` to take a dump of the log. You can also view the logs from a PC over `ADB`. For more information, check official android `logcat` guide [here](https://developer.android.com/studio/command-line/logcat). Moreover, users can generate termux files `stat` info and `logcat` dump automatically too with terminal's long hold options menu `More` -> `Report Issue` option and selecting `YES` in the prompt shown to add debug info. This can be helpful for reporting and debugging other issues. If the report generated is too large, then `Save To File` option in context menu (3 dots on top right) of `ReportActivity` can be used and the file viewed/shared instead. @@ -237,7 +240,7 @@ The `versionName` in `build.gradle` files of Termux and its plugin apps must fol ### Commit Messages Guidelines -Commit messages **must** use the [Conventional Commits](https://www.conventionalcommits.org) spec so that chagelogs as per the [Keep a Changelog](https://github.com/olivierlacan/keep-a-changelog) spec can automatically be generated by the [`create-conventional-changelog`](https://github.com/termux/create-conventional-changelog) script, check its repo for further details on the spec. **The first letter for `type` and `description` must be capital and description should be in the present tense.** The space after the colon `:` is necessary. For a breaking change, add an exclamation mark `!` before the colon `:`, so that it is highlighted in the chagelog automatically. +Commit messages **must** use the [Conventional Commits](https://www.conventionalcommits.org) spec so that changelogs as per the [Keep a Changelog](https://github.com/olivierlacan/keep-a-changelog) spec can automatically be generated by the [`create-conventional-changelog`](https://github.com/termux/create-conventional-changelog) script, check its repo for further details on the spec. **The first letter for `type` and `description` must be capital and description should be in the present tense.** The space after the colon `:` is necessary. For a breaking change, add an exclamation mark `!` before the colon `:`, so that it is highlighted in the changelog automatically. ``` [optional scope]: diff --git a/app/build.gradle b/app/build.gradle index 33c88d1d4a..12aac6a822 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -79,10 +79,16 @@ android { signingConfigs { debug { - storeFile file('testkey_untrusted.jks') - keyAlias 'alias' - storePassword 'xrj45yWGLbsO7W0v' - keyPassword 'xrj45yWGLbsO7W0v' + storeFile file('../.github/signing/debug.jks') + keyAlias 'termux-debug' + storePassword 'termux-debug-password' + keyPassword 'termux-debug-password' + } + release { + storeFile file('../.github/signing/release.jks') + keyAlias 'termux-release' + storePassword System.getenv("TERMUX_RELEASE_STORE_PASSWORD") ?: 'termux-release-password' + keyPassword System.getenv("TERMUX_RELEASE_KEY_PASSWORD") ?: 'termux-release-password' } } @@ -91,6 +97,7 @@ android { minifyEnabled true shrinkResources false // Reproducible builds proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + signingConfig signingConfigs.release } debug { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1d0dfe1948..beceec59b7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -45,7 +45,7 @@ android:label="@string/application_name" android:requestLegacyExternalStorage="true" android:roundIcon="@mipmap/ic_launcher_round" - android:supportsRtl="false" + android:supportsRtl="true" android:theme="@style/Theme.TermuxApp.DayNight.DarkActionBar" tools:targetApi="m"> diff --git a/app/src/main/java/com/termux/app/TermuxActivity.java b/app/src/main/java/com/termux/app/TermuxActivity.java index 0c9f74125b..79612cd6fb 100644 --- a/app/src/main/java/com/termux/app/TermuxActivity.java +++ b/app/src/main/java/com/termux/app/TermuxActivity.java @@ -10,6 +10,7 @@ import android.content.IntentFilter; import android.content.ServiceConnection; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.view.ContextMenu; @@ -193,6 +194,12 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo private static final String ARG_ACTIVITY_RECREATED = "activity_recreated"; private static final String LOG_TAG = "TermuxActivity"; + private static final int FULLSCREEN_UI_FLAGS = View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; @Override public void onCreate(Bundle savedInstanceState) { @@ -238,7 +245,7 @@ public void onCreate(Bundle savedInstanceState) { }); if (mProperties.isUsingFullScreen()) { - getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + enableFullScreenMode(); } setTermuxTerminalViewAndClients(); @@ -322,6 +329,14 @@ public void onResume() { mIsOnResumeAfterOnCreate = false; } + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + + if (hasFocus && mProperties != null && mProperties.isUsingFullScreen()) + enableFullScreenMode(); + } + @Override protected void onStop() { super.onStop(); @@ -449,6 +464,12 @@ private void reloadProperties() { mTermuxTerminalViewClient.onReloadProperties(); } + private void enableFullScreenMode() { + getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) + getWindow().getDecorView().setSystemUiVisibility(FULLSCREEN_UI_FLAGS); + } + private void setActivityTheme() { diff --git a/app/src/main/java/com/termux/app/api/file/FileReceiverActivity.java b/app/src/main/java/com/termux/app/api/file/FileReceiverActivity.java index ca5c07407c..84d99f51be 100644 --- a/app/src/main/java/com/termux/app/api/file/FileReceiverActivity.java +++ b/app/src/main/java/com/termux/app/api/file/FileReceiverActivity.java @@ -165,18 +165,28 @@ void promptNameAndSave(final InputStream in, final String attachmentFileName) { File outFile = saveStreamWithName(in, text); if (outFile == null) return; - final File editorProgramFile = new File(EDITOR_PROGRAM); + String editorExecutable = EDITOR_PROGRAM; + File editorProgramFile = new File(editorExecutable); + if (!editorProgramFile.isFile()) { - showErrorDialogAndQuit("The following file does not exist:\n$HOME/bin/termux-file-editor\n\n" - + "Create this file as a script or a symlink - it will be called with the received file as only argument."); - return; + editorExecutable = findDefaultEditor(); // Fallback to common editors if termux-file-editor is not configured + + if (editorExecutable == null) { + showErrorDialogAndQuit("The following file does not exist:\n$HOME/bin/termux-file-editor\n\n" + + "Create this file as a script or a symlink - it will be called with the received file as only argument."); + return; + } + + editorProgramFile = new File(editorExecutable); } - // Do this for the user if necessary: - //noinspection ResultOfMethodCallIgnored - editorProgramFile.setExecutable(true); + if (EDITOR_PROGRAM.equals(editorExecutable)) { + // Do this for the user if necessary: + //noinspection ResultOfMethodCallIgnored + editorProgramFile.setExecutable(true); + } - final Uri scriptUri = UriUtils.getFileUri(EDITOR_PROGRAM); + final Uri scriptUri = UriUtils.getFileUri(editorExecutable); Intent executeIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, scriptUri); executeIntent.setClass(FileReceiverActivity.this, TermuxService.class); @@ -198,6 +208,25 @@ void promptNameAndSave(final InputStream in, final String attachmentFileName) { }); } + private static final String[] DEFAULT_EDITORS = { + "nano", + "vim", + "vi" + }; + + private String findDefaultEditor() { + final String binDir = TermuxConstants.TERMUX_PREFIX_DIR_PATH + "/bin/"; + + for (String name : DEFAULT_EDITORS) { + String path = binDir + name; + File editor = new File(path); + if (editor.canExecute()) { + return editor.getAbsolutePath(); + } + } + return null; + } + public File saveStreamWithName(InputStream in, String attachmentFileName) { File receiveDir = new File(TERMUX_RECEIVEDIR); diff --git a/app/src/main/java/com/termux/app/terminal/TermuxTerminalSessionActivityClient.java b/app/src/main/java/com/termux/app/terminal/TermuxTerminalSessionActivityClient.java index bd789145f2..ff3f118a23 100644 --- a/app/src/main/java/com/termux/app/terminal/TermuxTerminalSessionActivityClient.java +++ b/app/src/main/java/com/termux/app/terminal/TermuxTerminalSessionActivityClient.java @@ -31,6 +31,7 @@ import com.termux.terminal.TerminalSession; import com.termux.terminal.TerminalSessionClient; import com.termux.terminal.TextStyle; +import com.termux.view.TerminalRenderer; import java.io.File; import java.io.FileInputStream; @@ -494,7 +495,6 @@ String toToastTitle(TerminalSession session) { public void checkForFontAndColors() { try { File colorsFile = TermuxConstants.TERMUX_COLOR_PROPERTIES_FILE; - File fontFile = TermuxConstants.TERMUX_FONT_FILE; final Properties props = new Properties(); if (colorsFile.isFile()) { @@ -510,13 +510,22 @@ public void checkForFontAndColors() { } updateBackgroundColor(); - final Typeface newTypeface = (fontFile.exists() && fontFile.length() > 0) ? Typeface.createFromFile(fontFile) : Typeface.MONOSPACE; - mActivity.getTerminalView().setTypeface(newTypeface); + setTypeface(TermuxConstants.TERMUX_FONT_FILE, TerminalRenderer.TypefaceStyle.NORMAL); + setTypeface(TermuxConstants.TERMUX_ITALIC_FONT_FILE, TerminalRenderer.TypefaceStyle.ITALIC); + setTypeface(TermuxConstants.TERMUX_BOLD_FONT_FILE, TerminalRenderer.TypefaceStyle.BOLD); + setTypeface(TermuxConstants.TERMUX_BOLD_ITALIC_FONT_FILE, TerminalRenderer.TypefaceStyle.BOLD_ITALIC); } catch (Exception e) { Logger.logStackTraceWithMessage(LOG_TAG, "Error in checkForFontAndColors()", e); } } + private void setTypeface(File fontFile, TerminalRenderer.TypefaceStyle style) { + if (fontFile.exists() && fontFile.length() > 0) { + final Typeface newTypeface = Typeface.createFromFile(fontFile); + mActivity.getTerminalView().setTypeface(newTypeface, style); + } + } + public void updateBackgroundColor() { if (!mActivity.isVisible()) return; TerminalSession session = mActivity.getCurrentSession(); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cbd2992ba1..4dcca4d065 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -54,8 +54,8 @@ Keyboard - Enabling Terminal Toolbar - Disabling Terminal Toolbar + Enabling terminal toolbar + Disabling terminal toolbar @@ -70,7 +70,7 @@ Send transcript to: Share selected text - Terminal Text + Terminal text Send selected text to: Autofill username diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index beb4173995..39b5d00880 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -1,13 +1,18 @@ -Termux is a terminal emulator application enhanced with a large set of command line utilities ported to Android OS. The main goal is to bring a Linux command line experience to users of mobile devices with no rooting or other special setup required. +Termux is a terminal emulator application enhanced with a large set of command line utilities ported to Android OS. +The main goal is to bring a Linux command line experience to users of mobile devices with no rooting or other special setup required. -* Enjoy the Bash and Zsh shells. -* Edit files with nano and vim. -* Access servers over SSH. -* Compile C/C++ code with clang. -* Use the Python console as a pocket calculator. -* Check out projects with Git and Subversion. -* Run text-based games with frotz. +
    +
  • Command line shells the Bash and Zsh.
  • +
  • Edit files with Nano and Vim.
  • +
  • Access servers over SSH.
  • +
  • Compile C/C++ code with Clang.
  • +
  • Use the Python console as a pocket calculator.
  • +
  • Check out projects with Git.
  • +
  • Run text-based games with Frotz.
  • +
-At first start a small base system is being configured. The GNU Bash, Coreutils, Findutils and other core utilities are available out-of-box. Additionally, we provide more than 1000 other packages installable by using the 'pkg' utility which currently is a frontend for the 'apt' package manager. All provided software has been patched and compiled with Android NDK to provide max compatibility with Android OS. +At first start a small base system is being configured. The GNU Bash, Coreutils, Findutils and other core utilities are available out-of-box. +Additionally, we provide more than 1000 other packages installable by using the pkg utility which currently is a wrapper for the apt package manager. +All provided software has been compiled with Android NDK to provide max compatibility with Android OS. -To learn more about application usage tips and tricks, long-press anywhere on the terminal and select the Help menu option to access Termux Wiki. This resource is also accessible directly in a web browser: https://wiki.termux.com/wiki/Main_Page. +To learn more about application usage tips and tricks, long-press anywhere on the terminal and select the Help menu option to access Termux Wiki. diff --git a/terminal-emulator/src/main/java/com/termux/terminal/TerminalBuffer.java b/terminal-emulator/src/main/java/com/termux/terminal/TerminalBuffer.java index 21d6518785..8344e9ec49 100644 --- a/terminal-emulator/src/main/java/com/termux/terminal/TerminalBuffer.java +++ b/terminal-emulator/src/main/java/com/termux/terminal/TerminalBuffer.java @@ -67,13 +67,34 @@ public String getSelectedText(int selX1, int selY1, int selX2, int selY2, boolea for (int row = selY1; row <= selY2; row++) { int x1 = (row == selY1) ? selX1 : 0; int x2; + TerminalRow lineObject = mLines[externalToInternalRow(row)]; + boolean rowLineWrap = getLineWrap(row); + if (row == selY2) { x2 = selX2 + 1; if (x2 > columns) x2 = columns; } else { - x2 = columns; + // For non-final rows, only read to full width if line is wrapped. + // Otherwise, find the actual end of content to avoid trailing whitespace. + if (rowLineWrap) { + x2 = columns; + } else { + // Find last non-space character in the line + x2 = lineObject.getSpaceUsed(); + char[] text = lineObject.mText; + for (int i = x2 - 1; i >= 0; i--) { + if (text[i] != ' ') { + x2 = i + 1; + break; + } + } + if (x2 == 0 && lineObject.getSpaceUsed() > 0) { + // Line is all spaces - keep at least one to preserve empty line + x2 = 1; + } + } } - TerminalRow lineObject = mLines[externalToInternalRow(row)]; + int x1Index = lineObject.findStartOfColumn(x1); int x2Index = (x2 < mColumns) ? lineObject.findStartOfColumn(x2) : lineObject.getSpaceUsed(); if (x2Index == x1Index) { @@ -83,7 +104,6 @@ public String getSelectedText(int selX1, int selY1, int selX2, int selY2, boolea char[] line = lineObject.mText; int lastPrintingCharIndex = -1; int i; - boolean rowLineWrap = getLineWrap(row); if (rowLineWrap && x2 == columns) { // If the line was wrapped, we shouldn't lose trailing space: lastPrintingCharIndex = x2Index - 1; @@ -98,7 +118,7 @@ public String getSelectedText(int selX1, int selY1, int selX2, int selY2, boolea if (lastPrintingCharIndex != -1 && len > 0) builder.append(line, x1Index, len); - boolean lineFillsWidth = lastPrintingCharIndex == x2Index - 1; + boolean lineFillsWidth = rowLineWrap || (lastPrintingCharIndex == lineObject.findStartOfColumn(columns) - 1); if ((!joinBackLines || !rowLineWrap) && (!joinFullLines || !lineFillsWidth) && row < selY2 && row < mScreenRows - 1) builder.append('\n'); } diff --git a/terminal-view/src/main/java/com/termux/view/TerminalRenderer.java b/terminal-view/src/main/java/com/termux/view/TerminalRenderer.java index a4bef7d37c..f0a83b6e74 100644 --- a/terminal-view/src/main/java/com/termux/view/TerminalRenderer.java +++ b/terminal-view/src/main/java/com/termux/view/TerminalRenderer.java @@ -17,39 +17,53 @@ * Saves font metrics, so needs to be recreated each time the typeface or font size changes. */ public final class TerminalRenderer { - - final int mTextSize; - final Typeface mTypeface; + int mTextSize; private final Paint mTextPaint = new Paint(); - + /* These arrays are indexed by style e.g mTypefaceByStyle[TypefaceStyle.ITALIC]; */ + private final Typeface[] mTypefaceByStyle = new Typeface[4]; /** The width of a single mono spaced character obtained by {@link Paint#measureText(String)} on a single 'X'. */ - final float mFontWidth; + private final float[] mFontWidthByStyle = new float[4]; /** The {@link Paint#getFontSpacing()}. See http://www.fampennings.nl/maarten/android/08numgrid/font.png */ - final int mFontLineSpacing; + private final int[] mFontLineSpacingByStyle = new int[4]; /** The {@link Paint#ascent()}. See http://www.fampennings.nl/maarten/android/08numgrid/font.png */ - private final int mFontAscent; - /** The {@link #mFontLineSpacing} + {@link #mFontAscent}. */ - final int mFontLineSpacingAndAscent; - + private final int[] mFontAscentByStyle = new int[4]; + /** The {@link #mFontLineSpacingByStyle} + {@link #mFontAscentByStyle}. */ + private final int[] mFontLineSpacingAndAscentByStyle = new int[4]; private final float[] asciiMeasures = new float[127]; - public TerminalRenderer(int textSize, Typeface typeface) { + public TerminalRenderer(int textSize) { mTextSize = textSize; - mTypeface = typeface; + mTextPaint.setTextSize(textSize); + } + + public void setTextSize(int textSize) { + mTextSize = textSize; + mTextPaint.setTextSize(textSize); + + for (int i = 0; i < 4; i++) { + setTypeface(mTypefaceByStyle[i], TypefaceStyle.values()[i]); + } + } + + public void setTypeface(Typeface typeface, TypefaceStyle style) { + int idx = style.ordinal(); + + mTypefaceByStyle[idx] = typeface; mTextPaint.setTypeface(typeface); mTextPaint.setAntiAlias(true); - mTextPaint.setTextSize(textSize); - mFontLineSpacing = (int) Math.ceil(mTextPaint.getFontSpacing()); - mFontAscent = (int) Math.ceil(mTextPaint.ascent()); - mFontLineSpacingAndAscent = mFontLineSpacing + mFontAscent; - mFontWidth = mTextPaint.measureText("X"); + mFontLineSpacingByStyle[idx] = (int) Math.ceil(mTextPaint.getFontSpacing()); + mFontAscentByStyle[idx] = (int) Math.ceil(mTextPaint.ascent()); + mFontLineSpacingAndAscentByStyle[idx] = mFontLineSpacingByStyle[idx] + mFontAscentByStyle[idx]; + mFontWidthByStyle[idx] = mTextPaint.measureText("X"); - StringBuilder sb = new StringBuilder(" "); - for (int i = 0; i < asciiMeasures.length; i++) { - sb.setCharAt(0, (char) i); - asciiMeasures[i] = mTextPaint.measureText(sb, 0, 1); + if (style == TypefaceStyle.NORMAL) { + StringBuilder sb = new StringBuilder(" "); + for (int i = 0; i < asciiMeasures.length; i++) { + sb.setCharAt(0, (char) i); + asciiMeasures[i] = mTextPaint.measureText(sb, 0, 1); + } } } @@ -69,9 +83,13 @@ public final void render(TerminalEmulator mEmulator, Canvas canvas, int topRow, if (reverseVideo) canvas.drawColor(palette[TextStyle.COLOR_INDEX_FOREGROUND], PorterDuff.Mode.SRC); - float heightOffset = mFontLineSpacingAndAscent; + int fontLineSpacingAndAscent = getFontLineSpacingAndAscent(); + int fontLineSpacing = getFontLineSpacing(); + float fontWidth = getFontWidth(); + + float heightOffset = fontLineSpacingAndAscent; for (int row = topRow; row < endRow; row++) { - heightOffset += mFontLineSpacing; + heightOffset += fontLineSpacing; final int cursorX = (row == cursorRow && cursorVisible) ? cursorCol : -1; int selx1 = -1, selx2 = -1; @@ -109,7 +127,7 @@ public final void render(TerminalEmulator mEmulator, Canvas canvas, int topRow, // If this is detected, we draw this code point scaled to match what wcwidth() expects. final float measuredCodePointWidth = (codePoint < asciiMeasures.length) ? asciiMeasures[codePoint] : mTextPaint.measureText(line, currentCharIndex, charsForCodePoint); - final boolean fontWidthMismatch = Math.abs(measuredCodePointWidth / mFontWidth - codePointWcWidth) > 0.01; + final boolean fontWidthMismatch = Math.abs(measuredCodePointWidth / fontWidth - codePointWcWidth) > 0.01; if (style != lastRunStyle || insideCursor != lastRunInsideCursor || insideSelection != lastRunInsideSelection || fontWidthMismatch || lastRunFontWidthMismatch) { if (column == 0) { @@ -156,6 +174,66 @@ public final void render(TerminalEmulator mEmulator, Canvas canvas, int topRow, } } + /** + * Prepare the {@link #mTextPaint} for drawing the specified style. + * If there is no Typeface supplied for the style, we will use a + * fallback and return it. + **/ + private TypefaceStyle prepareStyleOrFallback(TypefaceStyle desiredStyle) { + TypefaceStyle fallbackStyle = desiredStyle; + switch (desiredStyle) { + case NORMAL: + mTextPaint.setTextSkewX(0.f); + mTextPaint.setFakeBoldText(false); + break; + case ITALIC: + if (mTypefaceByStyle[desiredStyle.ordinal()] == null) { + fallbackStyle = TypefaceStyle.NORMAL; + mTextPaint.setTextSkewX(-0.35f); + } else { + mTextPaint.setTextSkewX(0.f); + } + mTextPaint.setFakeBoldText(false); + break; + case BOLD: + if (mTypefaceByStyle[desiredStyle.ordinal()] == null) { + fallbackStyle = TypefaceStyle.NORMAL; + mTextPaint.setFakeBoldText(true); + } else { + mTextPaint.setFakeBoldText(false); + } + mTextPaint.setTextSkewX(0.f); + break; + case BOLD_ITALIC: + if (mTypefaceByStyle[desiredStyle.ordinal()] == null) { + // italic has priority over bold here since fake bold + // is not as bad as skewed text which may be too big + // for the cell + if (mTypefaceByStyle[TypefaceStyle.ITALIC.ordinal()] != null) { + fallbackStyle = TypefaceStyle.ITALIC; + mTextPaint.setFakeBoldText(true); + mTextPaint.setTextSkewX(0.f); + } else if (mTypefaceByStyle[TypefaceStyle.BOLD.ordinal()] != null) { + fallbackStyle = TypefaceStyle.BOLD; + mTextPaint.setFakeBoldText(false); + mTextPaint.setTextSkewX(-0.35f); + } else { + fallbackStyle = TypefaceStyle.NORMAL; + mTextPaint.setFakeBoldText(true); + mTextPaint.setTextSkewX(-0.35f); + } + } else { + mTextPaint.setFakeBoldText(false); + mTextPaint.setTextSkewX(0.f); + } + break; + } + + mTextPaint.setTypeface(mTypefaceByStyle[fallbackStyle.ordinal()]); + + return fallbackStyle; + } + private void drawTextRun(Canvas canvas, char[] text, int[] palette, float y, int startColumn, int runWidthColumns, int startCharIndex, int runWidthChars, float mes, int cursor, int cursorStyle, long textStyle, boolean reverseVideo) { @@ -168,6 +246,21 @@ private void drawTextRun(Canvas canvas, char[] text, int[] palette, float y, int final boolean strikeThrough = (effect & TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH) != 0; final boolean dim = (effect & TextStyle.CHARACTER_ATTRIBUTE_DIM) != 0; + TypefaceStyle style = TypefaceStyle.NORMAL; + if (italic && bold) { + style = prepareStyleOrFallback(TypefaceStyle.BOLD_ITALIC); + } else if (italic) { + style = prepareStyleOrFallback(TypefaceStyle.ITALIC); + } else if (bold) { + style = prepareStyleOrFallback(TypefaceStyle.BOLD); + } else { + prepareStyleOrFallback(style); + } + + float fontWidth = getFontWidth(style); + int fontAscent = getFontAscent(style); + int fontLineSpacingAndAscent = getFontLineSpacingAndAscent(style); + if ((foreColor & 0xff000000) != 0xff000000) { // Let bold have bright colors if applicable (one of the first 8): if (bold && foreColor >= 0 && foreColor < 8) foreColor += 8; @@ -186,10 +279,10 @@ private void drawTextRun(Canvas canvas, char[] text, int[] palette, float y, int backColor = tmp; } - float left = startColumn * mFontWidth; - float right = left + runWidthColumns * mFontWidth; + float left = startColumn * fontWidth; + float right = left + runWidthColumns * fontWidth; - mes = mes / mFontWidth; + mes = mes / fontWidth; boolean savedMatrix = false; if (Math.abs(mes - runWidthColumns) > 0.01) { canvas.save(); @@ -202,12 +295,12 @@ private void drawTextRun(Canvas canvas, char[] text, int[] palette, float y, int if (backColor != palette[TextStyle.COLOR_INDEX_BACKGROUND]) { // Only draw non-default background. mTextPaint.setColor(backColor); - canvas.drawRect(left, y - mFontLineSpacingAndAscent + mFontAscent, right, y, mTextPaint); + canvas.drawRect(left, y - fontLineSpacingAndAscent + fontAscent, right, y, mTextPaint); } if (cursor != 0) { mTextPaint.setColor(cursor); - float cursorHeight = mFontLineSpacingAndAscent - mFontAscent; + float cursorHeight = fontLineSpacingAndAscent - fontAscent; if (cursorStyle == TerminalEmulator.TERMINAL_CURSOR_STYLE_UNDERLINE) cursorHeight /= 4.; else if (cursorStyle == TerminalEmulator.TERMINAL_CURSOR_STYLE_BAR) right -= ((right - left) * 3) / 4.; canvas.drawRect(left, y - cursorHeight, right, y, mTextPaint); @@ -226,24 +319,49 @@ private void drawTextRun(Canvas canvas, char[] text, int[] palette, float y, int foreColor = 0xFF000000 + (red << 16) + (green << 8) + blue; } - mTextPaint.setFakeBoldText(bold); mTextPaint.setUnderlineText(underline); - mTextPaint.setTextSkewX(italic ? -0.35f : 0.f); mTextPaint.setStrikeThruText(strikeThrough); mTextPaint.setColor(foreColor); // The text alignment is the default Paint.Align.LEFT. - canvas.drawTextRun(text, startCharIndex, runWidthChars, startCharIndex, runWidthChars, left, y - mFontLineSpacingAndAscent, false, mTextPaint); + canvas.drawTextRun(text, startCharIndex, runWidthChars, startCharIndex, runWidthChars, left, y - fontLineSpacingAndAscent, false, mTextPaint); } if (savedMatrix) canvas.restore(); } - public float getFontWidth() { - return mFontWidth; + public float getFontWidth(TypefaceStyle style) { + return mFontWidthByStyle[style.ordinal()]; + } + + public int getFontAscent(TypefaceStyle style) { + return mFontAscentByStyle[style.ordinal()]; + } + public int getFontLineSpacing(TypefaceStyle style) { + return mFontLineSpacingByStyle[style.ordinal()]; } + public int getFontLineSpacingAndAscent(TypefaceStyle style) { + return mFontLineSpacingAndAscentByStyle[style.ordinal()]; + } + + public float getFontWidth() { + return mFontWidthByStyle[TypefaceStyle.NORMAL.ordinal()]; + } + public int getFontAscent() { + return mFontAscentByStyle[TypefaceStyle.NORMAL.ordinal()]; + } public int getFontLineSpacing() { - return mFontLineSpacing; + return mFontLineSpacingByStyle[TypefaceStyle.NORMAL.ordinal()]; + } + public int getFontLineSpacingAndAscent() { + return mFontLineSpacingAndAscentByStyle[TypefaceStyle.NORMAL.ordinal()]; + } + + public enum TypefaceStyle { + NORMAL, + ITALIC, + BOLD, + BOLD_ITALIC, } } diff --git a/terminal-view/src/main/java/com/termux/view/TerminalView.java b/terminal-view/src/main/java/com/termux/view/TerminalView.java index 0b3f515682..582a419ecf 100644 --- a/terminal-view/src/main/java/com/termux/view/TerminalView.java +++ b/terminal-view/src/main/java/com/termux/view/TerminalView.java @@ -178,8 +178,8 @@ public boolean onScroll(MotionEvent e, float distanceX, float distanceY) { } else { scrolledWithFinger = true; distanceY += mScrollRemainder; - int deltaRows = (int) (distanceY / mRenderer.mFontLineSpacing); - mScrollRemainder = distanceY - deltaRows * mRenderer.mFontLineSpacing; + int deltaRows = (int) (distanceY / mRenderer.getFontLineSpacing()); + mScrollRemainder = distanceY - deltaRows * mRenderer.getFontLineSpacing(); doScroll(e, deltaRows); } return true; @@ -512,12 +512,17 @@ public void onContextMenuClosed(Menu menu) { * @param textSize the new font size, in density-independent pixels. */ public void setTextSize(int textSize) { - mRenderer = new TerminalRenderer(textSize, mRenderer == null ? Typeface.MONOSPACE : mRenderer.mTypeface); + if (mRenderer == null) { + mRenderer = new TerminalRenderer(textSize); + mRenderer.setTypeface(Typeface.MONOSPACE, TerminalRenderer.TypefaceStyle.NORMAL); + } else { + mRenderer.setTextSize(textSize); + } updateSize(); } - public void setTypeface(Typeface newTypeface) { - mRenderer = new TerminalRenderer(mRenderer.mTextSize, newTypeface); + public void setTypeface(Typeface newTypeface, TerminalRenderer.TypefaceStyle style) { + mRenderer.setTypeface(newTypeface, style); updateSize(); invalidate(); } @@ -544,8 +549,8 @@ public boolean isOpaque() { * @return Array with the column and row. */ public int[] getColumnAndRow(MotionEvent event, boolean relativeToScroll) { - int column = (int) (event.getX() / mRenderer.mFontWidth); - int row = (int) ((event.getY() - mRenderer.mFontLineSpacingAndAscent) / mRenderer.mFontLineSpacing); + int column = (int) (event.getX() / mRenderer.getFontWidth()); + int row = (int) ((event.getY() - mRenderer.getFontLineSpacingAndAscent()) / mRenderer.getFontLineSpacing()); if (relativeToScroll) { row += mTopRow; } @@ -987,8 +992,8 @@ public void updateSize() { if (viewWidth == 0 || viewHeight == 0 || mTermSession == null) return; // Set to 80 and 24 if you want to enable vttest. - int newColumns = Math.max(4, (int) (viewWidth / mRenderer.mFontWidth)); - int newRows = Math.max(4, (viewHeight - mRenderer.mFontLineSpacingAndAscent) / mRenderer.mFontLineSpacing); + int newColumns = Math.max(4, (int) (viewWidth / mRenderer.getFontWidth())); + int newRows = Math.max(4, (viewHeight - mRenderer.getFontLineSpacingAndAscent()) / mRenderer.getFontLineSpacing()); if (mEmulator == null || (newColumns != mEmulator.mColumns || newRows != mEmulator.mRows)) { mTermSession.updateSize(newColumns, newRows, (int) mRenderer.getFontWidth(), mRenderer.getFontLineSpacing()); @@ -1032,22 +1037,22 @@ private CharSequence getText() { } public int getCursorX(float x) { - return (int) (x / mRenderer.mFontWidth); + return (int) (x / mRenderer.getFontWidth()); } public int getCursorY(float y) { - return (int) (((y - 40) / mRenderer.mFontLineSpacing) + mTopRow); + return (int) (((y - 40) / mRenderer.getFontLineSpacing()) + mTopRow); } public int getPointX(int cx) { if (cx > mEmulator.mColumns) { cx = mEmulator.mColumns; } - return Math.round(cx * mRenderer.mFontWidth); + return Math.round(cx * mRenderer.getFontWidth()); } public int getPointY(int cy) { - return Math.round((cy - mTopRow) * mRenderer.mFontLineSpacing); + return Math.round((cy - mTopRow) * mRenderer.getFontLineSpacing()); } public int getTopRow() { diff --git a/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java b/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java index 534936ffca..d0e1bfd4e4 100644 --- a/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java +++ b/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java @@ -771,6 +771,18 @@ public final class TermuxConstants { public static final String TERMUX_FONT_FILE_PATH = TERMUX_DATA_HOME_DIR_PATH + "/font.ttf"; // Default: "/data/data/com.termux/files/home/.termux/font.ttf" /** Termux app and Termux:Styling font.ttf file */ public static final File TERMUX_FONT_FILE = new File(TERMUX_FONT_FILE_PATH); + /** Termux app and Termux:Styling font-italic.ttf file path */ + public static final String TERMUX_ITALIC_FONT_FILE_PATH = TERMUX_DATA_HOME_DIR_PATH + "/font-italic.ttf"; // Default: "/data/data/com.termux/files/home/.termux/font-italic.ttf" + /** Termux app and Termux:Styling font-italic.ttf file */ + public static final File TERMUX_ITALIC_FONT_FILE = new File(TERMUX_ITALIC_FONT_FILE_PATH); + /** Termux app and Termux:Styling font-bold.ttf file path */ + public static final String TERMUX_BOLD_FONT_FILE_PATH = TERMUX_DATA_HOME_DIR_PATH + "/font-bold.ttf"; // Default: "/data/data/com.termux/files/home/.termux/font-bold.ttf" + /** Termux app and Termux:Styling font-bold.ttf file */ + public static final File TERMUX_BOLD_FONT_FILE = new File(TERMUX_BOLD_FONT_FILE_PATH); + /** Termux app and Termux:Styling font-bold-italic.ttf file path */ + public static final String TERMUX_BOLD_ITALIC_FONT_FILE_PATH = TERMUX_DATA_HOME_DIR_PATH + "/font-bold-italic.ttf"; // Default: "/data/data/com.termux/files/home/.termux/font-bold-italic.ttf" + /** Termux app and Termux:Styling font-bold-italic.ttf file */ + public static final File TERMUX_BOLD_ITALIC_FONT_FILE = new File(TERMUX_BOLD_ITALIC_FONT_FILE_PATH); /** Termux app and plugins crash log file path */ diff --git a/termux-shared/src/main/res/values/strings.xml b/termux-shared/src/main/res/values/strings.xml index d585e24f97..4ff8378a3f 100644 --- a/termux-shared/src/main/res/values/strings.xml +++ b/termux-shared/src/main/res/values/strings.xml @@ -20,7 +20,7 @@ The %1$s (%2$s) app is not installed or is disabled. - Failed To Get Package Context + Failed to get package context Failed to get package context for the \"%1$s\" package. This may be because the app package is not installed or it has different APK signature from the current app. Check %1$s for more details. @@ -53,15 +53,15 @@ - Report Text + Report text **Report Truncated**\n\nReport is too large to view here. Use `Save To File` option from options menu (3-dots on top right) and view it in an external text editor app.\n\n##\n\n - Share With - Open URL With + Share with + Open URL with The storage permission not granted. The %1$s file saved successfully at \"%2$s\" @@ -108,7 +108,7 @@ Copy Share Cancel - Save To File + Save to file The storage permission granted by user on request The storage permission not granted by user on request @@ -116,18 +116,18 @@ - Enable Launcher Icon - Disable Launcher Icon + Enable launcher icon + Disable launcher icon Enabling %1$s app launcher icon Disabling %1$s app launcher icon - Launcher Icon Enabled - Launcher Icon will be disabled. - Launcher Icon will be enabled. (Default) + Launcher icon enabled + Launcher icon will be disabled. + Launcher icon will be enabled. (Default) - Log Level + Log level Off Normal Debug