Compare commits

..

79 Commits

Author SHA1 Message Date
agnostic-apollo
5b657c6adf Release: v0.118.3 2025-05-23 02:53:15 +05:00
agnostic-apollo
4f033b3445 Fixed: Add explicit serialVersionUID to Serializable classes like ReportInfo and TextIOInfo
Reading `ReportInfo` with `Bundle.getSerializable()` by `ReportActivity` is triggering exception when default algorithm is used for `serialVersionUID` in Termux:API plugin app when error notification created in `ResultReturner.returnData()` by `TermuxPluginUtils.sendPluginCommandErrorNotification()` is clicked.

```
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.termux/com.termux.shared.activities.ReportActivity}: android.os.BadParcelableException: Parcelable encountered IOException reading a Serializable object (name = com.termux.shared.models.ReportInfo)
	at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:4280)
	at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:4467)
	at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:222)
	at android.app.servertransaction.TransactionExecutor.executeNonLifecycleItem(TransactionExecutor.java:133)
	at android.app.servertransaction.TransactionExecutor.executeTransactionItems(TransactionExecutor.java:103)
	at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:80)
	at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2823)
	at android.os.Handler.dispatchMessage(Handler.java:110)
	at android.os.Looper.loopOnce(Looper.java:248)
	at android.os.Looper.loop(Looper.java:338)
	at android.app.ActivityThread.main(ActivityThread.java:9067)
	at java.lang.reflect.Method.invoke(Native Method)
	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:593)
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:932)
Caused by: android.os.BadParcelableException: Parcelable encountered IOException reading a Serializable object (name = com.termux.shared.models.ReportInfo)
	at android.os.Parcel.readSerializableInternal(Parcel.java:5520)
	at android.os.Parcel.readValue(Parcel.java:5038)
	at android.os.Parcel.readValue(Parcel.java:4702)
	at android.os.Parcel.-$$Nest$mreadValue(Unknown Source:0)
	at android.os.Parcel$LazyValue.apply(Parcel.java:4811)
	at android.os.Parcel$LazyValue.apply(Parcel.java:4764)
	at android.os.BaseBundle.unwrapLazyValueFromMapLocked(BaseBundle.java:446)
	at android.os.BaseBundle.getValueAt(BaseBundle.java:426)
	at android.os.BaseBundle.getValue(BaseBundle.java:397)
	at android.os.BaseBundle.getValue(BaseBundle.java:380)
	at android.os.BaseBundle.getValue(BaseBundle.java:373)
	at android.os.BaseBundle.getSerializable(BaseBundle.java:1522)
	at android.os.Bundle.getSerializable(Bundle.java:1339)
	at com.termux.shared.activities.ReportActivity.updateUI(ReportActivity.java:140)
	at com.termux.shared.activities.ReportActivity.onCreate(ReportActivity.java:93)
	at android.app.Activity.performCreate(Activity.java:9155)
	at android.app.Activity.performCreate(Activity.java:9133)
	at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1521)
	at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:4262)
	... 13 more
Caused by: java.io.InvalidClassException: com.termux.shared.models.ReportInfo; local class incompatible: stream classdesc serialVersionUID = -5165426368218339031, local class serialVersionUID = 1
	at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:652)
	at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1743)
	at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1624)
	at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1902)
	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1442)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:430)
	at android.os.Parcel.readSerializableInternal(Parcel.java:5507)
	... 31 more

```

If using release APK with obfuscation enabled, then following exception will be triggered.

```
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.termux/com.termux.shared.activities.ReportActivity}: android.os.BadParcelableException: Parcelable encountered ClassNotFoundException reading a Serializable object (name = I0.a)
	at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3864)
	at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:4006)
	at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:111)
	at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
	at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
	at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2462)
	at android.os.Handler.dispatchMessage(Handler.java:106)
	at android.os.Looper.loopOnce(Looper.java:240)
	at android.os.Looper.loop(Looper.java:351)
	at android.app.ActivityThread.main(ActivityThread.java:8377)
	at java.lang.reflect.Method.invoke(Native Method)
	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:584)
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1013)
Caused by: android.os.BadParcelableException: Parcelable encountered ClassNotFoundException reading a Serializable object (name = I0.a)
	at android.os.Parcel.readSerializableInternal(Parcel.java:5113)
	at android.os.Parcel.readValue(Parcel.java:4655)
	at android.os.Parcel.readValue(Parcel.java:4363)
	at android.os.Parcel.-$$Nest$mreadValue(Unknown Source:0)
	at android.os.Parcel$LazyValue.apply(Parcel.java:4461)
	at android.os.Parcel$LazyValue.apply(Parcel.java:4420)
	at android.os.BaseBundle.getValueAt(BaseBundle.java:394)
	at android.os.BaseBundle.getValue(BaseBundle.java:374)
	at android.os.BaseBundle.getValue(BaseBundle.java:357)
	at android.os.BaseBundle.getValue(BaseBundle.java:350)
	at android.os.BaseBundle.getSerializable(BaseBundle.java:1451)
	at android.os.Bundle.getSerializable(Bundle.java:1144)
	at com.termux.shared.activities.ReportActivity.updateUI(ReportActivity.java:136)
	at com.termux.shared.activities.ReportActivity.onCreate(ReportActivity.java:89)
	at android.app.Activity.performCreate(Activity.java:8397)
	at android.app.Activity.performCreate(Activity.java:8370)
	at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1403)
	at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3837)
	... 12 more
Caused by: java.lang.ClassNotFoundException: I0.a
	at java.lang.Class.classForName(Native Method)
	at java.lang.Class.forName(Class.java:536)
	at android.os.Parcel$2.resolveClass(Parcel.java:5090)
	at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1733)
	at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1624)
	at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1902)
	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1442)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:430)
	at android.os.Parcel.readSerializableInternal(Parcel.java:5096)
	... 29 more
Caused by: java.lang.ClassNotFoundException: I0.a
	... 38 more
```

Related issue https://github.com/termux/termux-api/issues/762
2025-05-23 02:48:30 +05:00
Wang Han
fda17afaa0 Changed|Fixed: Bump org.lsposed.hiddenapibypass:hiddenapibypass to 6.1 to fix crash on Android 16 QPR1
```
Build fingerprint: 'google/shiba_beta/shiba:16/BP31.250502.008/13497110:user/release-keys'
Revision: 'MP1.0'
ABI: 'arm64'
Executable: /system/bin/app_process64
Cmdline: com.termux
pid: 22617, tid: 22617, name: com.termux  >>> com.termux <<<
uid: 10323
tagged_addr_ctrl: 0000000000000001 (PR_TAGGED_ADDR_ENABLE)
pac_enabled_keys: 000000000000000f (PR_PAC_APIAKEY, PR_PAC_APIBKEY, PR_PAC_APDAKEY, PR_PAC_APDBKEY)
esr: 0000000092000006 (Data Abort Exception 0x24)
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x000000000000000c
Cause: null pointer dereference
    x0  0000000070b6f798  x1  0000000002159228  x2  000000000000000c  x3  00000000ebad6073
    x4  00000000ebad6074  x5  00000000ebad6075  x6  00000000ebad6076  x7  00000000ebad6077
    x8  00000000ebad6078  x9  00000000ebad6079  x10 00000000ebad607a  x11 00000000ebad607b
    x12 00000000ebad607c  x13 00000000ebad607d  x14 00000000ebad607e  x15 00000000ebad607f
    x16 0000007feda203f0  x17 0000007976776a9c  x18 0000007cd0e44000  x19 b400007b850b8be0
    x20 0000000000000000  x21 b400007b850b8ca0  x22 0000000000000000  x23 00000000021592b8
    x24 000000006413b378  x25 0000000070b99ee0  x26 0000000070b9fc78  x27 0000000000000000
    x28 0000000000000053  x29 0000000070b99ee0
    lr  0000007976776a98  sp  0000007feda203f0  pc  0000007976776b0c  pst 0000000080001000
    esr 0000000092000006
26 total frames
backtrace:
      #00 pc 00000000000c8b0c  /data/app/~~p_sHRwZKj3QVf_xeRMrR3g==/com.termux-rodTRD4IY6G2qtCPCrHfhw==/oat/arm64/base.odex (org.lsposed.hiddenapibypass.HiddenApiBypass.getDeclaredMethods+780)
      #01 pc 00000000000c8dc4  /data/app/~~p_sHRwZKj3QVf_xeRMrR3g==/com.termux-rodTRD4IY6G2qtCPCrHfhw==/oat/arm64/base.odex (org.lsposed.hiddenapibypass.HiddenApiBypass.setHiddenApiExemptions+68)
      #02 pc 00000000000ae560  /data/app/~~p_sHRwZKj3QVf_xeRMrR3g==/com.termux-rodTRD4IY6G2qtCPCrHfhw==/oat/arm64/base.odex (com.termux.shared.reflection.ReflectionUtils.bypassHiddenAPIReflectionRestrictions+448)
      #03 pc 00000000000a342c  /data/app/~~p_sHRwZKj3QVf_xeRMrR3g==/com.termux-rodTRD4IY6G2qtCPCrHfhw==/oat/arm64/base.odex (com.termux.shared.android.SELinuxUtils.getContext+108)
      #04 pc 00000000000c0b60  /data/app/~~p_sHRwZKj3QVf_xeRMrR3g==/com.termux-rodTRD4IY6G2qtCPCrHfhw==/oat/arm64/base.odex (com.termux.shared.termux.shell.command.environment.TermuxAppShellEnvironment.setTermuxAppEnvironment+3216)
      #05 pc 000000000009eafc  /data/app/~~p_sHRwZKj3QVf_xeRMrR3g==/com.termux-rodTRD4IY6G2qtCPCrHfhw==/oat/arm64/base.odex (com.termux.app.TermuxApplication.onCreate+1596)
      ...
      #22 pc 0000000000105c98  /system/lib64/libandroid_runtime.so (_JNIEnv::CallStaticVoidMethod(_jclass*, _jmethodID*, ...)+104) (BuildId: ea93df5e792bbd2e0cbb71b2a7599aaa)
      #23 pc 000000000013121c  /system/lib64/libandroid_runtime.so (android::AndroidRuntime::start(char const*, android::Vector<android::String8> const&, bool)+908) (BuildId: ea93df5e792bbd2e0cbb71b2a7599aaa)
      #24 pc 000000000000459c  /system/bin/app_process64 (main+1212) (BuildId: a237cfae6965d7f0b950e5955c73432c)
      #25 pc 000000000006bb88  /apex/com.android.runtime/lib64/bionic/libc.so (__libc_init+120) (BuildId: eecb0fc2ec8128ec92c091c0160586d1)
```

Related commit https://github.com/LSPosed/AndroidHiddenApiBypass/commit/9efadf06
Related commit https://android-review.googlesource.com/c/platform/libcore/+/3380841
Related commit https://cs.android.com/android/_/android/platform/libcore/+/0dc31afe

Closes #4556
2025-05-23 02:48:30 +05:00
agnostic-apollo
431dc1d9d1 Fixed: Use openjdk11 for jitpack builds of termux libraries
`Unrecognized option: --add-exports=java.base/sun.nio.ch=ALL-UNNAMED`

Related commit 52da00e5
2025-05-23 02:47:41 +05:00
agnostic-apollo
849e6b10de Changed!: Remove TERMUX_APP__BUILD_DATA_DIR as a different naming scheme will be used for build variables and rename TERMUX__ROOTFS to TERMUX__ROOTFS_DIR 2025-05-23 01:38:33 +05:00
agnostic-apollo
2375652891 Fixed: Declare MANAGE_EXTERNAL_STORAGE as it being added in Termux:API version 0.51.0 is revoking the storage permission of Termux app after updates and sometimes later
Related issue #4486
2025-05-23 01:37:35 +05:00
agnostic-apollo
21c0d51fb6 Release: v0.118.2 2025-03-29 09:27:24 +05:00
agnostic-apollo
a8f8be18cb Changed: Bump apt-android-7 bootstraps to 2025.03.28-r1 2025-03-29 09:27:24 +05:00
agnostic-apollo
8f156dc58f Fixed: Do not assume failure if stderr is set for bootstrap second stage as a postinst script could be using stderr for logging
Exit code `0` should be enough to check for failure.
2025-03-29 09:27:24 +05:00
agnostic-apollo
2522c62173 Changed: Log a message if termux-bootstrap-second-stage.sh or bash is not found to run bootstrap second stage 2025-03-29 03:24:20 +05:00
agnostic-apollo
7f632cf4d2 Fixed: Generate debug report before deleting broken prefix directory after bootstrap second stage failure to get stat info at time of failure 2025-03-29 03:24:20 +05:00
agnostic-apollo
999f10d7c7 Added: Add package SE_PROCESS_CONTEXT, SE_FILE_CONTEXT and SE_INFO when generating app info markdown string 2025-03-29 03:24:19 +05:00
agnostic-apollo
3aa986cf7b Added: Add info to device info for supported ABIs for armv8l and 64-bit-only aarch64 devices and PAGE_SIZE for 16KB page size devices
- https://source.android.com/docs/core/architecture/16kb-page-size/16kb
2025-03-29 03:24:19 +05:00
agnostic-apollo
b7d2a4b663 Added: Export additional primary TERMUX_* scoped shell environment variables
- TERMUX__ROOTFS
- TERMUX__HOME
- TERMUX__PREFIX
- TERMUX_APP__DATA_DIR (Actual app data directory path assigned by Android for the `com.termux` app returned by `ApplicationInfo`)
- TERMUX_APP__LEGACY_DATA_DIR (Expected legacy app data directory path for the `com.termux` app, mainly accessible on primary user `0` and is a symlink/bind mount to/from `TERMUX_APP__DATA_DIR`)
- TERMUX_APP__BUILD_DATA_DIR (App data directory path used by `termux-packages` `properties.sh` while building packages and `TermuxConstants.java` while building Termux app)
- TERMUX__SE_PROCESS_CONTEXT (SeLinux process context for current process)
- TERMUX_APP__SE_FILE_CONTEXT (SeLinux file context for the `TERMUX_APP__DATA_DIR`)
- TERMUX_APP__SE_INFO (Termux app seInfo tag found in selinux policy used to set app process and app data files selinux context)

This is also required for `libtermux-exec-*-ld-preload.so` `$LD_PRELOAD` library as well for `termux-exec` version `>= 2.0.0`.

- https://github.com/termux/termux-exec-package/commit/db738a11

See also https://github.com/termux/termux-packages/blob/96eefda/scripts/properties.sh
2025-03-29 03:24:19 +05:00
agnostic-apollo
8440a72805 Added: Export ANDROID__BUILD_VERSION_SDK in shell environment variable for the Android build SDK version
This is also required for `libtermux-exec-*-ld-preload.so` `$LD_PRELOAD` library as well for `termux-exec` version `>= 2.0.0`.

- https://github.com/termux/termux-exec-package/commit/db738a11
2025-03-28 11:27:24 +05:00
agnostic-apollo
0cdbe9869e Added: Add functions to PackageUtils to get seInfo and seInfoUser of package 2025-03-28 11:10:42 +05:00
agnostic-apollo
f84853fc30 Added: Add SELinuxUtils to get process and file paths security contexts 2025-03-28 11:04:31 +05:00
agnostic-apollo
fcc3a4d951 Added: Add ReflectionUtils and add dependency for org.lsposed.hiddenapibypass:hiddenapibypass
The call to bypassHiddenAPIReflectionRestrictions() must be made before trying to reflect hidden or non-sdk APIs.

Reflection will be used for accessing hidden (@hide) APIs by Termux and its plugins later.

https://github.com/LSPosed/AndroidHiddenApiBypass
https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces
2025-03-28 11:04:31 +05:00
Johannes Altmanninger
3f2375029e Fixed: Fully consume unknown CSI sequences containing unsupported parameter and intermediate bytes
Standard ECMA-48: Control Functions for Coded Character Sets specifies the format of CSI commands.
- https://en.wikipedia.org/wiki/ANSI_escape_code#Control_Sequence_Introducer_commands
- https://invisible-island.net/xterm/ecma-48-parameter-format.html#section5.4

Previously unsupported bytes would be echoed to the terminal.

```shell
$ printf '\x1b[=u' # PF
u
$ printf '\x1b[=5u' # PPF
5u
$ printf '\x1b[=5!u' # PPIF
5!u
$ printf '\x1b[=5!%u' # PPIIF
5!0
$ printf '\x1b[=?5!%u' # PPPIIF
?5!0
```

This fixes a problem with fish shell 4.0.0 which uses that sequence.

Closes #4338

Co-authored-by: @krobelus <aclopte@gmail.com>
Co-authored-by: @agnostic-apollo  <agnosticapollo@gmail.com>
2025-03-28 10:06:50 +05:00
agnostic-apollo
e5b578be55 Fixed: Fix gradle build error if using jdk 17
`Unable to make field private final java.lang.String java.io.File.path accessible: module java.base does not "opens java.io" to unnamed module`
2025-03-28 10:06:50 +05:00
agnostic-apollo
c09d867b94 Fixed: Fix tests added in b84dc703 2025-03-28 10:06:49 +05:00
Jason Yu
49018f6d91 Added: Add empty and null strings tests for invalid urls to FileReceiverActivityTest 2025-03-28 10:06:49 +05:00
Fredrik Fornwall
0415681d53 Fixed: Implement colon separated CSI parameters 2025-03-28 10:06:49 +05:00
Evgeny Zhabotinsky
4baf12bae3 Fixed: Make ScrollDown escape respect margins
SD sequence (`${CSI}${N}T`) was scrolling the whole width
 of the terminal instead of just between the margins.
RI sequence (`${ESC}M`, move cursor up 1 line) was doing the same.
Fixed that.

Fixes #2576 where in tmux scrolling one of several
 side-by-side panels down resulted in all visually scrolling.
2025-03-28 10:06:49 +05:00
Tom Kranz
c5f000a162 Added: Basic MIME type recognition in ContentProvider 2025-03-28 10:06:49 +05:00
Matan Ziv-Av
c154407624 Fixed: Use Canvas.drawTextRun instead of drawText
drawText does (very) basic BiDi, which causes inconsistent behaviour.
This ensures everything is LtR.
2025-03-28 10:06:49 +05:00
Fredrik Fornwall
254e885fcb Added: Terminal CSI reporting of window and cell pixel size
Implement the following CSI escape sequences from
https://invisible-island.net/xterm/ctlseqs/ctlseqs.html:

> CSI Ps ; Ps ; Ps t
> [..]
>    Ps = 1 4  ⇒  Report xterm text area size in pixels.
>    Result is CSI  4 ;  height ;  width t
> [..]
>    Ps = 1 6  ⇒  Report xterm character cell size in pixels.
>    Result is CSI  6 ;  height ;  width t

Extracted from changes in https://github.com/termux/termux-app/pull/2973
by @MatanZ and adopted to play well with the just merged #3098 (.ws_xpixel
and .ws_ypixel values in winsize).
2025-03-28 10:06:49 +05:00
Artem Chepurnyi
ee9b31a0ee Fixed: Mark view as important for AutoFill before requesting an AutoFill
Co-authored-by: @AChep <mail@artemchep.com>
Co-authored-by: @agnostic-apollo  <agnosticapollo@gmail.com>
2025-03-28 10:06:49 +05:00
Dvd-Znf
68865fb148 Update latest version in README.md to v0.118.1 2025-03-28 10:06:48 +05:00
Krunal Patel
bb7fa88fb3 Added|Changed: Fill .ws_xpixel and .ws_ypixel in winsize
This allows to get terminal size in pixel using `TIOCGWINSZ` ioctl.
Set `.ws_xpixel` using `columns * cell_width` and set `.ws_ypixel` using `rows * cell_height`.
Cell width and height is font width and line spacing, respectively.
2025-03-28 10:06:48 +05:00
Fredrik Fornwall
f35063da74 Fixed: Parse (but ignore for now) terminal APC sequences 2025-03-28 10:06:48 +05:00
agnostic-apollo
043923e674 Added|Fixed: Do not show AutoFill UI on Termux start and add support for usernames
- The AutoFill type and hints are no longer hardcoded in `TerminalView` class and `TermuxActivity` layout xml. They are dynamically set to required values before making a manual AutoFill request and reverted back afterwards to default values. The hardcoded value `AUTOFILL_TYPE_TEXT` returned by `getAutofillType()` was causing the AutoFill UI to show on Activity starts, this will return `AUTOFILL_TYPE_NONE` by default now so that AutoFill UI isn't shown automatically.
- The AutoFill importance is no longer hardcoded in `TermuxActivity` layout xml and is returned by `TerminalView` class itself by `getImportantForAutofill()`.
- The AutoFill function in `TermuxActivity` for making a manual AutoFill request is moved to `TerminalView` class. This and moving of hardcoded values to `TerminalView` class mentioned above is done as complete logic of AutoFill should be handled by `TerminalView` class itself and not scattered in various places.
- The Terminal context menu now supports AutoFilling a username. Note that GBoard/Google Password Manager seems to have a bug where it will still show `Pick a saved password` instead of username, even though `AUTOFILL_HINT_USERNAME` is being requested, however it will still AutoFill a username of selected entry correctly.
- Pressing the back button to close the keyboard will also cancel the current manually requested AutoFill request and UI will not show when keyboard is opened again.

Closes #3909
2025-03-28 10:06:48 +05:00
agnostic-apollo
2d38a1ca6e Added: Add support for Share selected text of terminal in long hold MORE menu so that users don't have to copy and paste to move text between apps 2025-03-28 10:06:48 +05:00
agnostic-apollo
096464d2af Changed: Use ShareUtils to copy and paste text and prevent potential NPE
The `copyTextToClipboard()` method has been updated to pass clip label when copying text to clipboard and `getTextFromClipboard()` and `getTextStringFromClipboardIfSet()` methods have been added to get current clipboard.
2025-03-28 10:06:48 +05:00
agnostic-apollo
b48f788a94 Changed: Share terminal transcript with ShareUtils 2025-03-28 10:06:48 +05:00
Josh Triplett
915e053a10 Make Shift-PgUp and Shift-PgDn scroll by pages rather than lines
In other terminals, such as gnome-terminal, Shift-PgUp and Shift-PgDn
scroll the screen by a full page, rather than a single line. Adjust
termux to match.
2025-03-28 09:37:45 +05:00
agnostic-apollo
791bf5f88a Fixed: Fix SHIFT+PAGE_UP and SHIFT+PAGE_DOWN behaviour to scroll 1 line of scrollback history instead of scrolling command history or changing pages
This will work for both `SHIFT` extra key and hardware keyboards. The `SHIFT` extra key can be long held to lock it in an enabled state and `PGUP` and `PGDN` keys can be long held to repeat scrolling.

Closes #867
2025-03-28 09:37:35 +05:00
agnostic-apollo
fa710c47e9 Changed: Add PGUP and PGDN extra keys to repetitive keys so that long holding them triggers page scrolling instead of having to repeatedly press the key to change pages 2025-03-28 09:37:29 +05:00
Fredrik Fornwall
5b5e71949b Fixed: Improve handling of empty ';' SGR sequences
Currently the Termux terminal emulator prints "HI" in red with:

```sh
printf "\e[31;m HI \e[0m"
```

This is not how other terminals (tested on xterm, gnome-terminal,
alacritty and the mac built in terminal) handle it, since they parse
""\e[31;m" as "\e[31;0m", where the "0" resets the colors.

This change aligns with other terminals, as well as improves performance
by avoiding allocating a new int[] array for each byte processed by
`parseArg()`, and most importantly simplifies things by removing the
`mIsCSIStart` and `mLastCSIArg` state, preparing for supporting ':'
separated sub parameters such as used in
https://sw.kovidgoyal.net/kitty/underlines/
2025-03-28 09:31:54 +05:00
Fredrik Fornwall
f3e73880c9 Fixed: Use current bg color when scrolling with horizontal margins
Fixes https://github.com/termux/termux-packages/issues/12556

Issue was also reported here:
https://www.reddit.com/r/termux/comments/1df1dii/how_can_i_fix_this_annoying_screenfilling_thing/
2025-03-28 09:31:54 +05:00
agnostic-apollo
43317b78c9 Release: v0.118.1
The `versionCode` has been bumped to `1000` so that users who have installed from F-Droid or GitHub should not have the app attempted to be updated by Google PlayStore and failing and also shown in PlayStore app updates list due to non-collaborative `v0.120` app release on PlayStore that set the `versionCode` higher than the latest F-Droid or GitHub `118` release. Unlike F-Droid, PlayStore does not check for difference in app APK signature before attempting to download and then failing to install due to signature mismatch.

- https://github.com/termux/termux-app/discussions/4000
- https://github.com/termux/termux-app/issues/4012
2024-06-18 04:10:09 +05:00
agnostic-apollo
2a008d836e Changed: Update support and donate users to termux.dev domain 2024-06-18 04:00:51 +05:00
agnostic-apollo
daa7ca4d43 Changed: Bump apt-android-7 bootstraps to 2024.06.17-r1 2024-06-18 03:55:30 +05:00
agnostic-apollo
2c82a5581f Added: Add support for Termux bootstrap second stage by running termux-bootstrap-second-stage.sh
- 7827140577
- 7827140577/scripts/bootstrap/termux-bootstrap-second-stage.sh
2024-06-18 03:55:30 +05:00
agnostic-apollo
708281cea2 Changed: Bump actions/checkout and actions/upload-artifact to v4 2024-06-18 03:36:37 +05:00
agnostic-apollo
eb0cb408a4 Changed: Use GitHub cli instead of hub for uploading GitHub release files as later has been removed from runner images
- https://github.com/actions/runner-images/issues/8362
2024-06-18 03:32:45 +05:00
agnostic-apollo
d11c95b996 Added: Add support for GitHub action builds for github-releases/** branches 2024-06-18 03:28:50 +05:00
agnostic-apollo
9735ae284d Added: Request SET_ALARM permission to allow broadcasting an intent to set an alarm or timer in an alarm clock app
- https://developer.android.com/reference/android/Manifest.permission#SET_ALARM
- https://developer.android.com/reference/android/provider/AlarmClock

Closes #3990
2024-06-18 03:28:17 +05:00
agnostic-apollo
0813e46330 Fixed: Limit max combining characters in TerminalRow to 15 characters to prevent buffer overflows
The exception below causing app crash happens because of malicious input where combining characters keep getting added to same column of the row and this increases the size of `mSpaceUsed` and `mText`, eventually causing a buffer overflow of `mSpaceUsed`, which is limited to max `32767` value as per java `short` limit, but the limit itself isn't the issue, but an endless number of combining characters being added. Check `MAX_COMBINING_CHARACTERS_PER_COLUMN` field javadocs for why the limit `15` was chosen.

```
curl -o matroska.js https://kimapr.net/lappy/matroska.js
cat matroska.js
```

The `charCount` below refers to value of `Character.charCount(codePoint)`, like before `oldCharactersUsedForColumn` is appended to `newCharactersUsedForColumn`.

```
TerminalRow: codePoint=112, mColumns=98, mText=637, columnToSet=18, mSpaceUsed=590, javaCharDifference=0, oldStartOfColumnIndex=510, oldCharactersUsedForColumn=1, newCharactersUsedForColumn=1, oldNextColumnIndex=511, newNextColumnIndex=511, charCount=1, oldCodePointDisplayWidth=1, newCodePointDisplayWidth=1
TerminalRow: codePoint=40, mColumns=98, mText=637, columnToSet=19, mSpaceUsed=590, javaCharDifference=0, oldStartOfColumnIndex=511, oldCharactersUsedForColumn=1, newCharactersUsedForColumn=1, oldNextColumnIndex=512, newNextColumnIndex=512, charCount=1, oldCodePointDisplayWidth=1, newCodePointDisplayWidth=1
TerminalRow: codePoint=40, mColumns=98, mText=637, columnToSet=20, mSpaceUsed=590, javaCharDifference=0, oldStartOfColumnIndex=512, oldCharactersUsedForColumn=1, newCharactersUsedForColumn=1, oldNextColumnIndex=513, newNextColumnIndex=513, charCount=1, oldCodePointDisplayWidth=1, newCodePointDisplayWidth=1
TerminalRow: codePoint=101, mColumns=98, mText=637, columnToSet=21, mSpaceUsed=590, javaCharDifference=0, oldStartOfColumnIndex=513, oldCharactersUsedForColumn=1, newCharactersUsedForColumn=1, oldNextColumnIndex=514, newNextColumnIndex=514, charCount=1, oldCodePointDisplayWidth=1, newCodePointDisplayWidth=1
TerminalRow: codePoint=917772, mColumns=98, mText=147, columnToSet=18, mSpaceUsed=98, javaCharDifference=2, oldStartOfColumnIndex=18, oldCharactersUsedForColumn=1, newCharactersUsedForColumn=3, oldNextColumnIndex=19, newNextColumnIndex=21, charCount=2, oldCodePointDisplayWidth=1, newCodePointDisplayWidth=0
I TerminalRow: codePoint=65024, mColumns=98, mText=147, columnToSet=18, mSpaceUsed=100, javaCharDifference=1, oldStartOfColumnIndex=18, oldCharactersUsedForColumn=3, newCharactersUsedForColumn=4, oldNextColumnIndex=21, newNextColumnIndex=22, charCount=1, oldCodePointDisplayWidth=1, newCodePointDisplayWidth=0
TerminalRow: codePoint=917772, mColumns=98, mText=147, columnToSet=18, mSpaceUsed=101, javaCharDifference=2, oldStartOfColumnIndex=18, oldCharactersUsedForColumn=4, newCharactersUsedForColumn=6, oldNextColumnIndex=22, newNextColumnIndex=24, charCount=2, oldCodePointDisplayWidth=1, newCodePointDisplayWidth=0
...
TerminalRow: codePoint=917959, mColumns=98, mText=32781, columnToSet=18, mSpaceUsed=32763, javaCharDifference=2, oldStartOfColumnIndex=18, oldCharactersUsedForColumn=32666, newCharactersUsedForColumn=32668, oldNextColumnIndex=32684, newNextColumnIndex=32686, charCount=2, oldCodePointDisplayWidth=1, newCodePointDisplayWidth=0
TerminalRow: codePoint=917939, mColumns=98, mText=32781, columnToSet=18, mSpaceUsed=32765, javaCharDifference=2, oldStartOfColumnIndex=18, oldCharactersUsedForColumn=32668, newCharactersUsedForColumn=32670, oldNextColumnIndex=32686, newNextColumnIndex=32688, charCount=2, oldCodePointDisplayWidth=1, newCodePointDisplayWidth=0
TerminalRow: codePoint=917961, mColumns=98, mText=32781, columnToSet=18, mSpaceUsed=32767, javaCharDifference=2, oldStartOfColumnIndex=18, oldCharactersUsedForColumn=32670, newCharactersUsedForColumn=32672, oldNextColumnIndex=32688, newNextColumnIndex=32690, charCount=2, oldCodePointDisplayWidth=1, newCodePointDisplayWidth=0
TerminalRow: codePoint=917804, mColumns=98, mText=32781, columnToSet=18, mSpaceUsed=-32767, javaCharDifference=2, oldStartOfColumnIndex=18, oldCharactersUsedForColumn=1, newCharactersUsedForColumn=3, oldNextColumnIndex=19, newNextColumnIndex=21, charCount=2, oldCodePointDisplayWidth=1, newCodePointDisplayWidth=0
```

```
java.lang.ArrayIndexOutOfBoundsException: src.length=32781 srcPos=19 dst.length=32781 dstPos=21 length=-32786
	at java.lang.System.arraycopy(System.java:469)
	at com.termux.terminal.TerminalRow.setChar(TerminalRow.java:196)
	at com.termux.terminal.TerminalBuffer.setChar(TerminalBuffer.java:455)
	at com.termux.terminal.TerminalEmulator.emitCodePoint(TerminalEmulator.java:2380)
	at com.termux.terminal.TerminalEmulator.processCodePoint(TerminalEmulator.java:624)
	at com.termux.terminal.TerminalEmulator.processByte(TerminalEmulator.java:520)
	at com.termux.terminal.TerminalEmulator.append(TerminalEmulator.java:487)
	at com.termux.terminal.TerminalSession$MainThreadHandler.handleMessage(TerminalSession.java:358)
	at android.os.Handler.dispatchMessage(Handler.java:106)
	at android.os.Looper.loop(Looper.java:223)
	at android.app.ActivityThread.main(ActivityThread.java:7664)
	at java.lang.reflect.Method.invoke(Native Method)
	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
```

See also following links for history of related changes to `TerminalRow` for combining characters. Note that jackpal terminal does not crash for above, which termux-app is based on, but changes were done by fornwall in initial commit of termux-app to change the behaviour, hence the crash, but he added the `FIXME: Put a limit of combining characters` comment as a note to solve the current issue in future, which is now.

- 9a47042620
- https://github.com/jackpal/Android-Terminal-Emulator/pull/338
- a18ee58f7a (diff-f84d215b18106c037e01986a3968fa54b74691174a78fcc99493f745d3805be5)

Closes #3839
2024-06-18 03:20:11 +05:00
Lucy Phipps
6ece249c03 WcWidth.c: fix 2nd typo 2024-06-18 03:20:11 +05:00
Lucy Phipps
fc8245bba3 WcWidth.c: fix typo 2024-06-18 03:20:11 +05:00
Lucy Phipps
63833d9c2d update WcWidth.java to Unicode 15.0.0 2024-06-18 03:20:11 +05:00
agnostic-apollo
c9e2a75e82 Fixed: Fix shared terminal transcript joining back lines
Regression of 370ac2bd caused in 5f71e3e7 by the (in)famous @trygveaa
2024-06-18 03:20:11 +05:00
agnostic-apollo
9433f10757 Fixed: Ensure CSI parameter value is not greater than 9999 as per vt510 2024-06-18 03:20:11 +05:00
agnostic-apollo
fbf55fd40c Fixed: Fix CSI parameters parsing like for SGR sequences that start with a ; or have sequential ; characters
https://vt100.net/docs/vt510-rm/chapter4.html#S4.3.3

https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_(Control_Sequence_Introducer)_sequences

Credits for finding the issue belongs to @Screwtapello

https://github.com/mawww/kakoune/issues/4339#issuecomment-916980723

Closes #2272, Closes mawww/kakoune#4339
2024-06-18 03:20:10 +05:00
agnostic-apollo
160ab68e5b Changed: Use black or white cursor color based on terminal background instead of always white if colors.properties didn't have cursor color set
Credit for algorithm link belong to @Jamie-Landeg-Jones

Closes #2653
2024-06-18 03:20:10 +05:00
agnostic-apollo
903f2496cb Fixed: Fix message dialog button text not showing in day mode due to white text 2024-06-18 03:20:10 +05:00
agnostic-apollo
2dc7381b89 Fixed: Fix wrong input type selected if toolbar is switched back to extra keys after tapping terminal if in text input mode
Closes #2503
2024-06-18 03:20:10 +05:00
agnostic-apollo
e55639e41d Fixed: Change extra keys and terminal input view background to black
Required for day/night theming and should fix issues where both views were translucent with light terminal color themes.
2024-06-18 03:20:10 +05:00
agnostic-apollo
087da0b576 Fixed: Fix issue where a colour tint/highlight would be added to the terminal on activity re-creation
The fix in c6b4114f was not working for it.
2024-06-18 03:20:10 +05:00
agnostic-apollo
d7f22982a1 Fixed: Fix termux app restarting on samsung dex version < 3.0 when switching modes 2024-06-18 03:20:10 +05:00
agnostic-apollo
f222315b0f Fixed: Fix ArrayIndexOutOfBoundsException when setting zero width terminal character
java.lang.ArrayIndexOutOfBoundsException: length=64; index=-1
at com.termux.terminal.TerminalRow.setChar(TerminalRow.java:127)
at com.termux.terminal.TerminalBuffer.setChar(TerminalBuffer.java:413)
at com.termux.terminal.TerminalEmulator.emitCodePoint(TerminalEmulator.java:2329)
at com.termux.terminal.TerminalEmulator.processCodePoint(TerminalEmulator.java:617)
at com.termux.terminal.TerminalEmulator.processByte(TerminalEmulator.java:513)
at com.termux.terminal.TerminalEmulator.append(TerminalEmulator.java:480)
at com.termux.terminal.TerminalSession$MainThreadHandler.handleMessage(TerminalSession.java:339)
at android.os.Handler.dispatchMessage(Handler.java:110)
at android.os.Looper.loop(Looper.java:219)
at android.app.ActivityThread.main(ActivityThread.java:8349)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1055)
2024-06-18 03:20:10 +05:00
agnostic-apollo
e11bcfc9a1 Fixed: Log exception instead of crashing app on NumberFormatException for invalid termcap/terminfo string requested
java.lang.NumberFormatException: For input string: " a"
at java.lang.Long.parseLong(Long.java:583)
at java.lang.Long.valueOf(Long.java:781)
at java.lang.Long.decode(Long.java:933)
at com.termux.terminal.TerminalEmulator.doDeviceControl(TerminalEmulator.java:940)
at com.termux.terminal.TerminalEmulator.processCodePoint(TerminalEmulator.java:813)
2024-06-18 03:20:09 +05:00
agnostic-apollo
e3a50cbf32 Fixed: Use android.util.Log for terminal-emulator logging if TerminalSessionClient is null like when running tests 2024-06-18 03:20:09 +05:00
agnostic-apollo
af5fef4c4a Fixed: Fix CSI Delete Ps Column(s) (DECDC)
Firstly, `TerminalBuffer.blockSet()` was throwing the exception since `sx + w > mColumns` which was technically passed by TerminalEmulator.blockClear()`. Actual value would be `mCursorRow + columnsToMove + columnsToDelete > mColumns`.

Secondly, the call to `blockClear()` should not be needed since it the `blockCopy()` would overwrite the columns to be deleted on copy.

Run `printf "\e['~"` to delete 1 column and `printf "\e[3'~"` to delete 3 columns. Run `printf "\e[3'}"` to insert 2 columns.

java.lang.IllegalArgumentException: Illegal arguments! blockSet(78, 0, 1, 30, 32, 56, 30)
at com.termux.terminal.TerminalBuffer.blockSet(TerminalBuffer.java:397)
at com.termux.terminal.TerminalEmulator.blockClear(TerminalEmulator.java:2035)
at com.termux.terminal.TerminalEmulator.processCodePoint(TerminalEmulator.java:799)
2024-06-18 03:20:09 +05:00
agnostic-apollo
03e31d190d Fixed: Fix ArrayIndexOutOfBoundsException thrown because length was less than 0 when selecting text from terminal buffer
java.lang.ArrayIndexOutOfBoundsException: src.length=132 srcPos=90 dst.length=16 dstPos=0 length=-2
at java.lang.System.arraycopy(System.java:469)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:597)
at java.lang.StringBuilder.append(StringBuilder.java:191)
at com.termux.terminal.TerminalBuffer.getSelectedText(TerminalBuffer.java:97)
at com.termux.terminal.TerminalBuffer.getSelectedText(TerminalBuffer.java:57)
at com.termux.terminal.TerminalBuffer.getSelectedText(TerminalBuffer.java:53)
at com.termux.terminal.TerminalEmulator.getSelectedText(TerminalEmulator.java:2401)
at com.termux.view.textselection.TextSelectionCursorController$1.onActionItemClicked(TextSelectionCursorController.java:140)
2024-06-18 03:20:09 +05:00
agnostic-apollo
d24a04a10d Fixed: Fix issue where menu wouldn't show when text on bottom row of terminal was selected
Closes #2233
2024-06-18 03:20:09 +05:00
agnostic-apollo
aee0da49a0 Changed: Do not show toast if text null or empty 2024-06-18 03:20:09 +05:00
agnostic-apollo
87c8f3d35a Fixed: Fix NullPointerException when getting spanned markdown like for notification 2024-06-18 03:20:09 +05:00
agnostic-apollo
9259ef0be1 Changed: Bump to v0.118.0 2022-01-23 01:36:41 +05:00
agnostic-apollo
480f92880c Fixed: Fix bootstrap checksum check if it contained leading zeros 2022-01-23 01:36:41 +05:00
agnostic-apollo
b01a738791 Docs: Update README.md 2022-01-23 01:36:41 +05:00
agnostic-apollo
0eaaa1372a Changed: Bump bootstrap to v2022.01.07-r1 2022-01-23 01:36:41 +05:00
agnostic-apollo
903b1c75a2 Fixed: Fix bootstrap checksum check if it contained leading zeros 2022-01-23 01:36:41 +05:00
agnostic-apollo
085b17e496 Changed: Bump dependency versions 2022-01-23 01:36:41 +05:00
agnostic-apollo
897d911a52 Changed: Move to semantic versioning for app and library versions and add commit hash and github to APK file names
The `versionName` will now follow semantic version `2.0.0` spec in the format `major.minor.patch(-prerelease)(+buildmetadata)`. This will make versioning the prerelease and github debug builds versions easier and follow a spec. The @termux devs should make sure that when bumping `versionName` in `build.gradle` files and when creating a tag for new releases on github that they include the patch number as well, like `v0.1.0` instead of just `v0.1`. The `build.gradle` files and `attach_debug_apks_to_release` workflow will now validate the version as well and the build/attachment will fail if `versionName` does not follow the spec. https://semver.org/spec/v2.0.0.html

APKs released on github for debug build workflows and releases are now referred as `Github` releases as per 7b10a35f and 94e01d68, so APK filenames have been modified to include `github` in the filename. The APKs are still debuggable, so that tag remains too.

For github workflows the apk filename format will be `termux-app_<current_version>+<last_commit_hash>-github-debug_<arch>.apk`, like `termux-app_v0.1.0+xxxxxxxx-github-debug_arm64-v8a.apk` and for github releases it will be `termux-app_<release_version>+github-debug_<arch>.apk`, like `termux-app_v0.1+github-debug_arm64-v8a.apk`. The `last_commit_hash` will be the first `8` characters of the commit hash. The `<last_commit_hash>-github-debug` will act as `buildmetadata` and will not affect versioning precedence.

For github workflows triggered by `push` and `pull_request` triggers, `<current_version>+<last_commit_hash>` will be used as new `versionName`, like `v0.1.0+xxxxxxxx`. This will make tracking which build a user is using easier and help in resolving issues as well.

Note that users using github releases and termux devs using `$TERMUX_VERSION` environment variables in scripts should take commit hash into consideration and possibly use something like `dpkg --compare-versions "$TERMUX_VERSION" ge 0.1` where appropriate instead of mathematical comparisons.

The `app/build.gradle` now also supports following `TERMUX_` scoped environmental variables and `RELEASE_TAG` variable will not be used anymore since it may conflict with possibly other variables used by users. They will also allow enabling split APKs for both debug and release builds.

- `TERMUX_APP_VERSION_NAME` will be used as `versionName` if its set.
- `TERMUX_APK_VERSION_TAG` will be used as `termux-app_<TERMUX_APK_VERSION_TAG>_<arch>.apk` if its set. The `_<arch>` will only exist for split APKs.
- `TERMUX_SPLIT_APKS_FOR_DEBUG_BUILDS` will define whether split APKs should be enabled for debug builds. Default value is `1`.
- `TERMUX_SPLIT_APKS_FOR_RELEASE_BUILDS` will define whether split APKs should be enabled for release builds. Default value is `0` since F-Droid does not support split APKs, check #1904.

So based on above, if in future github releases are to be converted to `release` builds instead of `debug` builds, something like following can be done and even a workflow can be created for it. Users can also build split APKs release builds for themselves if they want.

```
export TERMUX_SPLIT_APKS_FOR_RELEASE_BUILDS=1
./gradlew assembleRelease -Pandroid.injected.signing.store.file="$(pwd)/app/dev_keystore.jks" -Pandroid.injected.signing.store.password=xrj45yWGLbsO7W0v -Pandroid.injected.signing.key.alias=alias -Pandroid.injected.signing.key.password=xrj45yWGLbsO7W0v
```

The APK will be found at `./app/build/outputs/apk/release/termux-app_<version>_<arch>.apk`

The `TERMUX_SPLIT_APKS_FOR_DEBUG_BUILDS` can be set to `0` to disable building split APKs which may be helpful for users building termux on device considering they will extra space and build time. Instructions for building are at https://github.com/termux/termux-packages/pull/7227#issuecomment-893022283.

```
export TERMUX_SPLIT_APKS_FOR_DEBUG_BUILDS=0
./gradlew assembleDebug
```

The APK will be found at `./app/build/outputs/apk/debug/termux-app_debug_universal.apk`

Note that F-Droid uses algorithm at https://gitlab.com/fdroid/fdroidserver/-/blob/2.1a0/fdroidserver/build.py#L746 to automatically detect built APKs, so ensure any modifications to location or file name are compliant. Current updates should be.

Auto updates are detected by checkupdates bot at https://gitlab.com/fdroid/fdroidserver/-/blob/master/fdroidserver/checkupdates.py
2022-01-23 01:36:41 +05:00
Henrik Grimler
cd5962c696 bootstrap archives: update to 2022.01.02-r1 2022-01-23 01:36:41 +05:00
Henrik Grimler
6e6da752bd Fixed: Fix copy&paste error in areHardwareKeyboardShortcutsDisabled
Fixes 829cc39868 ("Allow users to disable hardware keyboard
shortcuts").

Reported-by: @amogusissofunnyhahalmaogenzhumorbelike
2022-01-23 01:36:41 +05:00
Leonid Pliushch
6d60bc669b bootstrap archives: update to 2021.12.02-r1 2022-01-23 01:36:41 +05:00
52 changed files with 1724 additions and 254 deletions

View File

@@ -58,19 +58,18 @@ jobs:
"${APK_BASENAME_PREFIX}_armeabi-v7a.apk" \
"${APK_BASENAME_PREFIX}_x86_64.apk" \
"${APK_BASENAME_PREFIX}_x86.apk" \
> sha256sums); then
> "${APK_BASENAME_PREFIX}_sha256sums"); then
exit_on_error "Generate sha25sums failed for '$RELEASE_VERSION_NAME' release."
fi
echo "Attaching APKs to github release"
if ! hub release edit \
-m "" \
-a "$APK_DIR_PATH/${APK_BASENAME_PREFIX}_universal.apk" \
-a "$APK_DIR_PATH/${APK_BASENAME_PREFIX}_arm64-v8a.apk" \
-a "$APK_DIR_PATH/${APK_BASENAME_PREFIX}_armeabi-v7a.apk" \
-a "$APK_DIR_PATH/${APK_BASENAME_PREFIX}_x86_64.apk" \
-a "$APK_DIR_PATH/${APK_BASENAME_PREFIX}_x86.apk" \
-a "$APK_DIR_PATH/sha256sums" \
"$RELEASE_VERSION_NAME"; then
if ! gh release upload "$RELEASE_VERSION_NAME" \
"$APK_DIR_PATH/${APK_BASENAME_PREFIX}_universal.apk" \
"$APK_DIR_PATH/${APK_BASENAME_PREFIX}_arm64-v8a.apk" \
"$APK_DIR_PATH/${APK_BASENAME_PREFIX}_armeabi-v7a.apk" \
"$APK_DIR_PATH/${APK_BASENAME_PREFIX}_x86_64.apk" \
"$APK_DIR_PATH/${APK_BASENAME_PREFIX}_x86.apk" \
"$APK_DIR_PATH/${APK_BASENAME_PREFIX}_sha256sums" \
; then
exit_on_error "Attach APKs to release failed for '$RELEASE_VERSION_NAME' release."
fi

View File

@@ -4,6 +4,7 @@ on:
push:
branches:
- master
- 'github-releases/**'
pull_request:
branches:
- master
@@ -13,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Build APKs
shell: bash {0}
@@ -65,7 +66,7 @@ jobs:
fi
- name: Attach universal APK file
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: ${{ env.APK_BASENAME_PREFIX }}_universal
path: |
@@ -73,7 +74,7 @@ jobs:
${{ env.APK_DIR_PATH }}/output-metadata.json
- name: Attach arm64-v8a APK file
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: ${{ env.APK_BASENAME_PREFIX }}_arm64-v8a
path: |
@@ -81,7 +82,7 @@ jobs:
${{ env.APK_DIR_PATH }}/output-metadata.json
- name: Attach armeabi-v7a APK file
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: ${{ env.APK_BASENAME_PREFIX }}_armeabi-v7a
path: |
@@ -89,7 +90,7 @@ jobs:
${{ env.APK_DIR_PATH }}/output-metadata.json
- name: Attach x86_64 APK file
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: ${{ env.APK_BASENAME_PREFIX }}_x86_64
path: |
@@ -97,7 +98,7 @@ jobs:
${{ env.APK_DIR_PATH }}/output-metadata.json
- name: Attach x86 APK file
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: ${{ env.APK_BASENAME_PREFIX }}_x86
path: |
@@ -105,7 +106,7 @@ jobs:
${{ env.APK_DIR_PATH }}/output-metadata.json
- name: Attach sha256sums file
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: sha256sums
path: |

View File

@@ -15,5 +15,5 @@ jobs:
name: "Validation"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- uses: gradle/wrapper-validation-action@v1

View File

@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Execute tests
run: |
./gradlew test

View File

@@ -49,7 +49,7 @@ The core [Termux](https://github.com/termux/termux-app) app comes with the follo
## Installation
Latest version is `v0.118.0`.
Latest version is `v0.118.1`.
Termux can be obtained through various sources listed below for **only** Android `>= 7`. Support was dropped for Android `5` and `6` on [2020-01-01](https://www.reddit.com/r/termux/comments/dnzdbs/end_of_android56_support_on_20200101/) at `v0.83`, old builds are available on [archive.org](https://archive.org/details/termux-repositories-legacy).
@@ -145,7 +145,7 @@ The main ones are the following.
- [Termux Matrix Channel](https://matrix.to/#termux_termux:gitter.im)
- [Termux Dev Matrix Channel](https://matrix.to/#termux_dev:gitter.im)
- [Termux Twitter](https://twitter.com/termux/)
- [Termux Reports Email](mailto:termuxreports@groups.io)
- [Termux Reports Email](mailto:support@termux.dev)
### Wikis

View File

@@ -30,8 +30,8 @@ android {
applicationId "com.termux"
minSdkVersion project.properties.minSdkVersion.toInteger()
targetSdkVersion project.properties.targetSdkVersion.toInteger()
versionCode 118
versionName "0.118.0"
versionCode 1002
versionName "0.118.3"
if (appVersionName) versionName = appVersionName
validateVersionName(versionName)
@@ -195,11 +195,11 @@ clean {
task downloadBootstraps() {
doLast {
def version = "2022.01.07-r1"
downloadBootstrap("aarch64", "0fe6d0159d12fcb8baf7750ce9072b9b36f742662b02ad4da145ab85873614cd", version)
downloadBootstrap("arm", "0a6014e2ec3b7079524fee3caabd02be05bcb4add3c6cd9e5ad98408b428c717", version)
downloadBootstrap("i686", "c55369a9af1316dc3d99457aa23cce64b29c1e60e375159352c0d4b9cfed4ac6", version)
downloadBootstrap("x86_64", "e93a7c66d15edb7d1b5ceca906255c85ead35a8b6a52f93524e117cfb07e6778", version)
def version = "2025.03.28-r1+apt-android-7"
downloadBootstrap("aarch64", "c8d702b6f742935001c37cda81b8ac69504a95d5cf28f2899532dd8cd4b057eb", version)
downloadBootstrap("arm", "f3bb9d1b32552b34fff41861dbf193ec5ba2848d67d779ac1c7256da6640f85d", version)
downloadBootstrap("i686", "36db3e1ac3547f9a174fd763bd9a484fa1a3449cdd81e1cf2408ff0454f839c6", version)
downloadBootstrap("x86_64", "1c124ec2396ee70a51b0b0a574f29aa659526aa2b9f558f993b2fb05d1e51855", version)
}
}

View File

@@ -23,6 +23,7 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
@@ -33,6 +34,7 @@
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" tools:ignore="ProtectedPermissions" />
<uses-permission android:name="com.android.alarm.permission.SET_ALARM" />
<application
android:name=".app.TermuxApplication"
@@ -180,13 +182,24 @@
<meta-data
android:name="android.max_aspect"
android:value="10.0" />
<!-- https://developer.samsung.com/samsung-dex/modify-optimizing.html -->
<!-- Version < 3.0. DeX Mode and Screen Mirroring support -->
<meta-data
android:name="com.sec.android.support.multiwindow"
android:name="com.samsung.android.keepalive.density"
android:value="true" />
<!-- Version >= 3.0. DeX Dual Mode support -->
<meta-data
android:name="com.samsung.android.multidisplay.keep_process_alive"
android:value="true" />
<meta-data
android:name="com.sec.android.support.multiwindow"
android:value="true" />
</application>
</manifest>

View File

@@ -14,7 +14,6 @@ import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.view.ContextMenu;
@@ -25,7 +24,6 @@ import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.autofill.AutofillManager;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ListView;
@@ -36,6 +34,7 @@ import com.termux.R;
import com.termux.app.terminal.TermuxActivityRootView;
import com.termux.shared.activities.ReportActivity;
import com.termux.shared.packages.PermissionUtils;
import com.termux.shared.data.DataUtils;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY;
import com.termux.app.activities.HelpActivity;
@@ -162,7 +161,9 @@ public final class TermuxActivity extends Activity implements ServiceConnection
private static final int CONTEXT_MENU_SELECT_URL_ID = 0;
private static final int CONTEXT_MENU_SHARE_TRANSCRIPT_ID = 1;
private static final int CONTEXT_MENU_AUTOFILL_ID = 2;
private static final int CONTEXT_MENU_SHARE_SELECTED_TEXT = 10;
private static final int CONTEXT_MENU_AUTOFILL_USERNAME = 11;
private static final int CONTEXT_MENU_AUTOFILL_PASSWORD = 2;
private static final int CONTEXT_MENU_RESET_TERMINAL_ID = 3;
private static final int CONTEXT_MENU_KILL_PROCESS_ID = 4;
private static final int CONTEXT_MENU_STYLING_ID = 5;
@@ -587,17 +588,16 @@ public final class TermuxActivity extends Activity implements ServiceConnection
TerminalSession currentSession = getCurrentSession();
if (currentSession == null) return;
boolean addAutoFillMenu = false;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillManager autofillManager = getSystemService(AutofillManager.class);
if (autofillManager != null && autofillManager.isEnabled()) {
addAutoFillMenu = true;
}
}
boolean autoFillEnabled = mTerminalView.isAutoFillEnabled();
menu.add(Menu.NONE, CONTEXT_MENU_SELECT_URL_ID, Menu.NONE, R.string.action_select_url);
menu.add(Menu.NONE, CONTEXT_MENU_SHARE_TRANSCRIPT_ID, Menu.NONE, R.string.action_share_transcript);
if (addAutoFillMenu) menu.add(Menu.NONE, CONTEXT_MENU_AUTOFILL_ID, Menu.NONE, R.string.action_autofill_password);
if (!DataUtils.isNullOrEmpty(mTerminalView.getStoredSelectedText()))
menu.add(Menu.NONE, CONTEXT_MENU_SHARE_SELECTED_TEXT, Menu.NONE, R.string.action_share_selected_text);
if (autoFillEnabled)
menu.add(Menu.NONE, CONTEXT_MENU_AUTOFILL_USERNAME, Menu.NONE, R.string.action_autofill_username);
if (autoFillEnabled)
menu.add(Menu.NONE, CONTEXT_MENU_AUTOFILL_PASSWORD, Menu.NONE, R.string.action_autofill_password);
menu.add(Menu.NONE, CONTEXT_MENU_RESET_TERMINAL_ID, Menu.NONE, R.string.action_reset_terminal);
menu.add(Menu.NONE, CONTEXT_MENU_KILL_PROCESS_ID, Menu.NONE, getResources().getString(R.string.action_kill_process, getCurrentSession().getPid())).setEnabled(currentSession.isRunning());
menu.add(Menu.NONE, CONTEXT_MENU_STYLING_ID, Menu.NONE, R.string.action_style_terminal);
@@ -625,8 +625,14 @@ public final class TermuxActivity extends Activity implements ServiceConnection
case CONTEXT_MENU_SHARE_TRANSCRIPT_ID:
mTermuxTerminalViewClient.shareSessionTranscript();
return true;
case CONTEXT_MENU_AUTOFILL_ID:
requestAutoFill();
case CONTEXT_MENU_SHARE_SELECTED_TEXT:
mTermuxTerminalViewClient.shareSelectedText();
return true;
case CONTEXT_MENU_AUTOFILL_USERNAME:
mTerminalView.requestAutoFillUsername();
return true;
case CONTEXT_MENU_AUTOFILL_PASSWORD:
mTerminalView.requestAutoFillPassword();
return true;
case CONTEXT_MENU_RESET_TERMINAL_ID:
onResetTerminalSession(session);
@@ -654,6 +660,13 @@ public final class TermuxActivity extends Activity implements ServiceConnection
}
}
@Override
public void onContextMenuClosed(Menu menu) {
super.onContextMenuClosed(menu);
// onContextMenuClosed() is triggered twice if back button is pressed to dismiss instead of tap for some reason
mTerminalView.onContextMenuClosed(menu);
}
private void showKillSessionDialog(TerminalSession session) {
if (session == null) return;
@@ -700,15 +713,6 @@ public final class TermuxActivity extends Activity implements ServiceConnection
}
}
private void requestAutoFill() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AutofillManager autofillManager = getSystemService(AutofillManager.class);
if (autofillManager != null && autofillManager.isEnabled()) {
autofillManager.requestAutofill(mTerminalView);
}
}
}
/**

View File

@@ -16,8 +16,11 @@ import com.termux.shared.file.TermuxFileUtils;
import com.termux.shared.interact.MessageDialogUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.markdown.MarkdownUtils;
import com.termux.shared.models.ExecutionCommand;
import com.termux.shared.models.errors.Error;
import com.termux.shared.packages.PackageUtils;
import com.termux.shared.shell.TermuxShellEnvironmentClient;
import com.termux.shared.shell.TermuxTask;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.TermuxUtils;
@@ -187,7 +190,8 @@ final class TermuxInstaller {
outStream.write(buffer, 0, readBytes);
}
if (zipEntryName.startsWith("bin/") || zipEntryName.startsWith("libexec") ||
zipEntryName.startsWith("lib/apt/apt-helper") || zipEntryName.startsWith("lib/apt/methods")) {
zipEntryName.startsWith("lib/apt/apt-helper") || zipEntryName.startsWith("lib/apt/methods") ||
zipEntryName.equals("etc/termux/bootstrap/termux-bootstrap-second-stage.sh")) {
//noinspection OctalInteger
Os.chmod(targetFile.getAbsolutePath(), 0700);
}
@@ -208,6 +212,34 @@ final class TermuxInstaller {
throw new RuntimeException("Moving termux prefix staging to prefix directory failed");
}
// Run Termux bootstrap second stage.
String termuxBootstrapSecondStageFile = TERMUX_PREFIX_DIR_PATH + "/etc/termux/bootstrap/termux-bootstrap-second-stage.sh";
if (!FileUtils.fileExists(termuxBootstrapSecondStageFile, false)) {
Logger.logInfo(LOG_TAG, "Not running Termux bootstrap second stage since script not found at \"" + termuxBootstrapSecondStageFile + "\" path.");
} else {
if (!FileUtils.fileExists(TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + "/bash", true)) {
Logger.logInfo(LOG_TAG, "Not running Termux bootstrap second stage since bash not found.");
}
Logger.logInfo(LOG_TAG, "Running Termux bootstrap second stage.");
ExecutionCommand executionCommand = new ExecutionCommand(-1,
termuxBootstrapSecondStageFile, null, null,
null, true, false);
executionCommand.commandLabel = "Termux Bootstrap Second Stage Command";
executionCommand.backgroundCustomLogLevel = Logger.LOG_LEVEL_NORMAL;
TermuxTask termuxTask = TermuxTask.execute(activity, executionCommand, null, new TermuxShellEnvironmentClient(), true);
if (termuxTask == null || !executionCommand.isSuccessful() || executionCommand.resultData.exitCode != 0) {
// Generate debug report before deleting broken prefix directory to get `stat` info at time of failure.
showBootstrapErrorDialog(activity, whenDone, MarkdownUtils.getMarkdownCodeForString(executionCommand.toString(), true));
// Delete prefix directory as otherwise when app is restarted, the broken prefix directory would be used and logged into.
error = FileUtils.deleteFile("termux prefix directory", TERMUX_PREFIX_DIR_PATH, true);
if (error != null)
Logger.logErrorExtended(LOG_TAG, error.toString());
return;
}
}
Logger.logInfo(LOG_TAG, "Bootstrap packages installed successfully.");
activity.runOnUiThread(whenDone);

View File

@@ -161,6 +161,13 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
@Override
public String getType(@NonNull Uri uri) {
String path = uri.getLastPathSegment();
int extIndex = path.lastIndexOf('.') + 1;
if (extIndex > 0) {
MimeTypeMap mimeMap = MimeTypeMap.getSingleton();
String ext = path.substring(extIndex).toLowerCase();
return mimeMap.getMimeTypeFromExtension(ext);
}
return null;
}

View File

@@ -15,6 +15,7 @@ import android.widget.ListView;
import com.termux.R;
import com.termux.shared.shell.TermuxSession;
import com.termux.shared.interact.TextInputDialogUtils;
import com.termux.shared.interact.ShareUtils;
import com.termux.app.TermuxActivity;
import com.termux.shared.terminal.TermuxTerminalSessionClientBase;
import com.termux.shared.termux.TermuxConstants;
@@ -177,20 +178,16 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
public void onCopyTextToClipboard(TerminalSession session, String text) {
if (!mActivity.isVisible()) return;
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(text)));
ShareUtils.copyTextToClipboard(mActivity, text);
}
@Override
public void onPasteTextFromClipboard(TerminalSession session) {
if (!mActivity.isVisible()) return;
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clipData = clipboard.getPrimaryClip();
if (clipData != null) {
CharSequence paste = clipData.getItemAt(0).coerceToText(mActivity);
if (!TextUtils.isEmpty(paste)) mActivity.getTerminalView().mEmulator.paste(paste.toString());
}
String text = ShareUtils.getTextStringFromClipboardIfSet(mActivity, true);
if (text != null)
mActivity.getTerminalView().mEmulator.paste(text);
}
@Override

View File

@@ -6,7 +6,6 @@ import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Environment;
@@ -213,7 +212,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
@Override
public boolean isTerminalViewSelected() {
return mActivity.getTerminalToolbarViewPager() == null || mActivity.isTerminalViewSelected();
return mActivity.getTerminalToolbarViewPager() == null || mActivity.isTerminalViewSelected() || mActivity.getTerminalView().hasFocus();
}
@@ -541,7 +540,8 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
// disabled or hidden at startup, otherwise if hardware keyboard is attached and user
// starts typing on hardware keyboard without tapping on the terminal first, then a colour
// tint will be added to the terminal as highlight for the focussed view. Test with a light
// theme.
// theme. For android 8.+, the "defaultFocusHighlightEnabled" attribute is also set to false
// in TerminalView layout to fix the issue.
// If soft keyboard is disabled by user for Termux (check function docs for Termux behaviour info)
if (KeyboardUtils.shouldSoftKeyboardBeDisabled(mActivity,
@@ -646,17 +646,17 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
String transcriptText = ShellUtils.getTerminalSessionTranscriptText(session, false, true);
if (transcriptText == null) return;
try {
// See https://github.com/termux/termux-app/issues/1166.
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
transcriptText = DataUtils.getTruncatedCommandOutput(transcriptText, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, true, false).trim();
intent.putExtra(Intent.EXTRA_TEXT, transcriptText);
intent.putExtra(Intent.EXTRA_SUBJECT, mActivity.getString(R.string.title_share_transcript));
mActivity.startActivity(Intent.createChooser(intent, mActivity.getString(R.string.title_share_transcript_with)));
} catch (Exception e) {
Logger.logStackTraceWithMessage(LOG_TAG,"Failed to get share session transcript of length " + transcriptText.length(), e);
}
// See https://github.com/termux/termux-app/issues/1166.
transcriptText = DataUtils.getTruncatedCommandOutput(transcriptText, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, true, false).trim();
ShareUtils.shareText(mActivity, mActivity.getString(R.string.title_share_transcript),
transcriptText, mActivity.getString(R.string.title_share_transcript_with));
}
public void shareSelectedText() {
String selectedText = mActivity.getTerminalView().getStoredSelectedText();
if (DataUtils.isNullOrEmpty(selectedText)) return;
ShareUtils.shareText(mActivity, mActivity.getString(R.string.title_share_selected_text),
selectedText, mActivity.getString(R.string.title_share_selected_text_with));
}
public void showUrlSelection() {
@@ -677,9 +677,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
// Click to copy url to clipboard:
final AlertDialog dialog = new AlertDialog.Builder(mActivity).setItems(urls, (di, which) -> {
String url = (String) urls[which];
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(url)));
Toast.makeText(mActivity, R.string.msg_select_url_copied_to_clipboard, Toast.LENGTH_LONG).show();
ShareUtils.copyTextToClipboard(mActivity, url, mActivity.getString(R.string.msg_select_url_copied_to_clipboard));
}).setTitle(R.string.title_select_url_dialog).create();
// Long press to open URL:
@@ -755,12 +753,9 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
if (session == null) return;
if (!session.isRunning()) return;
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clipData = clipboard.getPrimaryClip();
if (clipData == null) return;
CharSequence paste = clipData.getItemAt(0).coerceToText(mActivity);
if (!TextUtils.isEmpty(paste))
session.getEmulator().paste(paste.toString());
String text = ShareUtils.getTextStringFromClipboardIfSet(mActivity, true);
if (text != null)
session.getEmulator().paste(text);
}
}

View File

@@ -46,6 +46,8 @@ public class TermuxFileReceiverActivity extends Activity {
private static final String LOG_TAG = "TermuxFileReceiverActivity";
static boolean isSharedTextAnUrl(String sharedText) {
if (sharedText == null || sharedText.isEmpty()) return false;
return Patterns.WEB_URL.matcher(sharedText).matches()
|| Pattern.matches("magnet:\\?xt=urn:btih:.*?", sharedText);
}

View File

@@ -1,4 +1,6 @@
<com.termux.app.terminal.TermuxActivityRootView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_termux_root_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -25,11 +27,11 @@
android:id="@+id/terminal_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:defaultFocusHighlightEnabled="false"
android:focusableInTouchMode="true"
android:scrollbarThumbVertical="@drawable/terminal_scroll_shape"
android:scrollbars="vertical"
android:importantForAutofill="no"
android:autofillHints="password" />
tools:ignore="UnusedAttribute" />
<LinearLayout
android:id="@+id/left_drawer"
@@ -96,7 +98,7 @@
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="37.5dp"
android:background="@android:drawable/screen_background_dark_transparent"
android:background="@color/black"
android:layout_alignParentBottom="true" />
</RelativeLayout>

View File

@@ -65,6 +65,11 @@
<string name="title_share_transcript">Terminal transcript</string>
<string name="title_share_transcript_with">Send transcript to:</string>
<string name="action_share_selected_text">Share selected text</string>
<string name="title_share_selected_text">Terminal Text</string>
<string name="title_share_selected_text_with">Send selected text to:</string>
<string name="action_autofill_username">Autofill username</string>
<string name="action_autofill_password">Autofill password</string>
<string name="action_reset_terminal">Reset</string>

View File

@@ -24,6 +24,8 @@ public class TermuxFileReceiverActivityTest {
List<String> invalidUrls = new ArrayList<>();
invalidUrls.add("a test with example.com");
invalidUrls.add("");
invalidUrls.add(null);
for (String url : invalidUrls) {
Assert.assertFalse(TermuxFileReceiverActivity.isSharedTextAnUrl(url));
}

View File

@@ -12,7 +12,12 @@
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
org.gradle.jvmargs=-Xmx2048M
org.gradle.jvmargs=-Xmx2048M \
--add-exports=java.base/sun.nio.ch=ALL-UNNAMED \
--add-opens=java.base/java.lang=ALL-UNNAMED \
--add-opens=java.base/java.lang.reflect=ALL-UNNAMED \
--add-opens=java.base/java.io=ALL-UNNAMED \
--add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED
android.useAndroidX=true
minSdkVersion=24

View File

@@ -1,2 +1,4 @@
jdk:
- openjdk11
env:
JITPACK_NDK_VERSION: "21.1.6352462"

View File

@@ -23,10 +23,10 @@ final class JNI {
* @return the file descriptor resulting from opening /dev/ptmx master device. The sub process will have opened the
* slave device counterpart (/dev/pts/$N) and have it as stdint, stdout and stderr.
*/
public static native int createSubprocess(String cmd, String cwd, String[] args, String[] envVars, int[] processId, int rows, int columns);
public static native int createSubprocess(String cmd, String cwd, String[] args, String[] envVars, int[] processId, int rows, int columns, int cellWidth, int cellHeight);
/** Set the window size for a given pty, which allows connected programs to learn how large their screen is. */
public static native void setPtyWindowSize(int fd, int rows, int cols);
public static native void setPtyWindowSize(int fd, int rows, int cols, int cellWidth, int cellHeight);
/**
* Causes the calling thread to wait for the process associated with the receiver to finish executing.

View File

@@ -0,0 +1,80 @@
package com.termux.terminal;
import android.util.Log;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
public class Logger {
public static void logError(TerminalSessionClient client, String logTag, String message) {
if (client != null)
client.logError(logTag, message);
else
Log.e(logTag, message);
}
public static void logWarn(TerminalSessionClient client, String logTag, String message) {
if (client != null)
client.logWarn(logTag, message);
else
Log.w(logTag, message);
}
public static void logInfo(TerminalSessionClient client, String logTag, String message) {
if (client != null)
client.logInfo(logTag, message);
else
Log.i(logTag, message);
}
public static void logDebug(TerminalSessionClient client, String logTag, String message) {
if (client != null)
client.logDebug(logTag, message);
else
Log.d(logTag, message);
}
public static void logVerbose(TerminalSessionClient client, String logTag, String message) {
if (client != null)
client.logVerbose(logTag, message);
else
Log.v(logTag, message);
}
public static void logStackTraceWithMessage(TerminalSessionClient client, String tag, String message, Throwable throwable) {
logError(client, tag, getMessageAndStackTraceString(message, throwable));
}
public static String getMessageAndStackTraceString(String message, Throwable throwable) {
if (message == null && throwable == null)
return null;
else if (message != null && throwable != null)
return message + ":\n" + getStackTraceString(throwable);
else if (throwable == null)
return message;
else
return getStackTraceString(throwable);
}
public static String getStackTraceString(Throwable throwable) {
if (throwable == null) return null;
String stackTraceString = null;
try {
StringWriter errors = new StringWriter();
PrintWriter pw = new PrintWriter(errors);
throwable.printStackTrace(pw);
pw.close();
stackTraceString = errors.toString();
errors.close();
} catch (IOException e) {
e.printStackTrace();
}
return stackTraceString;
}
}

View File

@@ -54,7 +54,7 @@ public final class TerminalBuffer {
}
public String getSelectedText(int selX1, int selY1, int selX2, int selY2, boolean joinBackLines) {
return getSelectedText(selX1, selY1, selX2, selY2, true, false);
return getSelectedText(selX1, selY1, selX2, selY2, joinBackLines, false);
}
public String getSelectedText(int selX1, int selY1, int selX2, int selY2, boolean joinBackLines, boolean joinFullLines) {
@@ -93,8 +93,11 @@ public final class TerminalBuffer {
if (c != ' ') lastPrintingCharIndex = i;
}
}
if (lastPrintingCharIndex != -1)
builder.append(line, x1Index, lastPrintingCharIndex - x1Index + 1);
int len = lastPrintingCharIndex - x1Index + 1;
if (lastPrintingCharIndex != -1 && len > 0)
builder.append(line, x1Index, len);
boolean lineFillsWidth = lastPrintingCharIndex == x2Index - 1;
if ((!joinBackLines || !rowLineWrap) && (!joinFullLines || !lineFillsWidth)
&& row < selY2 && row < mScreenRows - 1) builder.append('\n');
@@ -446,8 +449,8 @@ public final class TerminalBuffer {
}
public void setChar(int column, int row, int codePoint, long style) {
if (row >= mScreenRows || column >= mColumns)
throw new IllegalArgumentException("row=" + row + ", column=" + column + ", mScreenRows=" + mScreenRows + ", mColumns=" + mColumns);
if (row < 0 || row >= mScreenRows || column < 0 || column >= mColumns)
throw new IllegalArgumentException("TerminalBuffer.setChar(): row=" + row + ", column=" + column + ", mScreenRows=" + mScreenRows + ", mColumns=" + mColumns);
row = externalToInternalRow(row);
allocateFullLineIfNecessary(row).setChar(column, codePoint, style);
}

View File

@@ -71,6 +71,7 @@ public final class TerminalColorScheme {
public void updateWith(Properties props) {
reset();
boolean cursorPropExists = false;
for (Map.Entry<Object, Object> entries : props.entrySet()) {
String key = (String) entries.getKey();
String value = (String) entries.getValue();
@@ -82,6 +83,7 @@ public final class TerminalColorScheme {
colorIndex = TextStyle.COLOR_INDEX_BACKGROUND;
} else if (key.equals("cursor")) {
colorIndex = TextStyle.COLOR_INDEX_CURSOR;
cursorPropExists = true;
} else if (key.startsWith("color")) {
try {
colorIndex = Integer.parseInt(key.substring(5));
@@ -98,6 +100,27 @@ public final class TerminalColorScheme {
mDefaultColors[colorIndex] = colorValue;
}
if (!cursorPropExists)
setCursorColorForBackground();
}
/**
* If the "cursor" color is not set by user, we need to decide on the appropriate color that will
* be visible on the current terminal background. White will not be visible on light backgrounds
* and black won't be visible on dark backgrounds. So we find the perceived brightness of the
* background color and if its below the threshold (too dark), we use white cursor and if its
* above (too bright), we use black cursor.
*/
public void setCursorColorForBackground() {
int backgroundColor = mDefaultColors[TextStyle.COLOR_INDEX_BACKGROUND];
int brightness = TerminalColors.getPerceivedBrightnessOfColor(backgroundColor);
if (brightness > 0) {
if (brightness < 130)
mDefaultColors[TextStyle.COLOR_INDEX_CURSOR] = 0xffffffff;
else
mDefaultColors[TextStyle.COLOR_INDEX_CURSOR] = 0xff000000;
}
}
}

View File

@@ -1,5 +1,7 @@
package com.termux.terminal;
import android.graphics.Color;
/** Current terminal colors (if different from default). */
public final class TerminalColors {
@@ -73,4 +75,22 @@ public final class TerminalColors {
if (c != 0) mCurrentColors[intoIndex] = c;
}
/**
* Get the perceived brightness of the color based on its RGB components.
*
* https://www.nbdtech.com/Blog/archive/2008/04/27/Calculating-the-Perceived-Brightness-of-a-Color.aspx
* http://alienryderflex.com/hsp.html
*
* @param color The color code int.
* @return Returns value between 0-255.
*/
public static int getPerceivedBrightnessOfColor(int color) {
return (int)
Math.floor(Math.sqrt(
Math.pow(Color.red(color), 2) * 0.241 +
Math.pow(Color.green(color), 2) * 0.691 +
Math.pow(Color.blue(color), 2) * 0.068
));
}
}

View File

@@ -79,9 +79,17 @@ public final class TerminalEmulator {
private static final int ESC_CSI_SINGLE_QUOTE = 18;
/** Escape processing: CSI ! */
private static final int ESC_CSI_EXCLAMATION = 19;
/** Escape processing: "ESC _" or Application Program Command (APC). */
private static final int ESC_APC = 20;
/** Escape processing: "ESC _" or Application Program Command (APC), followed by Escape. */
private static final int ESC_APC_ESCAPE = 21;
/** Escape processing: ESC [ <parameter bytes> */
private static final int ESC_CSI_UNSUPPORTED_PARAMETER_BYTE = 22;
/** Escape processing: ESC [ <parameter bytes> <intermediate bytes> */
private static final int ESC_CSI_UNSUPPORTED_INTERMEDIATE_BYTE = 23;
/** The number of parameter arguments. This name comes from the ANSI standard for terminal escape codes. */
private static final int MAX_ESCAPE_PARAMETERS = 16;
/** The number of parameter arguments including colon separated sub-parameters. */
private static final int MAX_ESCAPE_PARAMETERS = 32;
/** Needs to be large enough to contain reasonable OSC 52 pastes. */
private static final int MAX_OSC_STRING_LENGTH = 8192;
@@ -126,13 +134,15 @@ public final class TerminalEmulator {
private String mTitle;
private final Stack<String> mTitleStack = new Stack<>();
/** The cursor position. Between (0,0) and (mRows-1, mColumns-1). */
private int mCursorRow, mCursorCol;
/** The number of character rows and columns in the terminal screen. */
public int mRows, mColumns;
/** Size of a terminal cell in pixels. */
private int mCellWidthPixels, mCellHeightPixels;
/** The number of terminal transcript rows that can be scrolled back to. */
public static final int TERMINAL_TRANSCRIPT_ROWS_MIN = 100;
public static final int TERMINAL_TRANSCRIPT_ROWS_MAX = 50000;
@@ -172,6 +182,8 @@ public final class TerminalEmulator {
private int mArgIndex;
/** Holds the arguments of the current escape sequence. */
private final int[] mArgs = new int[MAX_ESCAPE_PARAMETERS];
/** Holds the bit flags which arguments are sub parameters (after a colon) - bit N is set if <code>mArgs[N]</code> is a sub parameter. */
private int mArgsSubParamsBitSet = 0;
/** Holds OSC and device control arguments, which can be strings. */
private final StringBuilder mOSCOrDeviceControlArgs = new StringBuilder();
@@ -232,15 +244,17 @@ public final class TerminalEmulator {
private boolean mCursorBlinkState;
/**
* Current foreground and background colors. Can either be a color index in [0,259] or a truecolor (24-bit) value.
* Current foreground, background and underline colors. Can either be a color index in [0,259] or a truecolor (24-bit) value.
* For a 24-bit value the top byte (0xff000000) is set.
*
* <p>Note that the underline color is currently parsed but not yet used during rendering.
*
* @see TextStyle
*/
int mForeColor, mBackColor;
int mForeColor, mBackColor, mUnderlineColor;
/** Current {@link TextStyle} effect. */
private int mEffect;
int mEffect;
/**
* The number of scrolled lines since last calling {@link #clearScrollCounter()}. Used for moving selection up along
@@ -308,13 +322,15 @@ public final class TerminalEmulator {
}
}
public TerminalEmulator(TerminalOutput session, int columns, int rows, Integer transcriptRows, TerminalSessionClient client) {
public TerminalEmulator(TerminalOutput session, int columns, int rows, int cellWidthPixels, int cellHeightPixels, Integer transcriptRows, TerminalSessionClient client) {
mSession = session;
mScreen = mMainBuffer = new TerminalBuffer(columns, getTerminalTranscriptRows(transcriptRows), rows);
mAltBuffer = new TerminalBuffer(columns, rows, rows);
mClient = client;
mRows = rows;
mColumns = columns;
mCellWidthPixels = cellWidthPixels;
mCellHeightPixels = cellHeightPixels;
mTabStop = new boolean[mColumns];
reset();
}
@@ -364,7 +380,10 @@ public final class TerminalEmulator {
}
}
public void resize(int columns, int rows) {
public void resize(int columns, int rows, int cellWidthPixels, int cellHeightPixels) {
this.mCellWidthPixels = cellWidthPixels;
this.mCellHeightPixels = cellHeightPixels;
if (mRows == rows && mColumns == columns) {
return;
} else if (columns < 2 || rows < 2) {
@@ -546,6 +565,15 @@ public final class TerminalEmulator {
}
public void processCodePoint(int b) {
// The Application Program-Control (APC) string might be arbitrary non-printable characters, so handle that early.
if (mEscapeState == ESC_APC) {
doApc(b);
return;
} else if (mEscapeState == ESC_APC_ESCAPE) {
doApcEscape(b);
return;
}
switch (b) {
case 0: // Null character (NUL, ^@). Do nothing.
break;
@@ -631,6 +659,10 @@ public final class TerminalEmulator {
case ESC_CSI:
doCsi(b);
break;
case ESC_CSI_UNSUPPORTED_PARAMETER_BYTE:
case ESC_CSI_UNSUPPORTED_INTERMEDIATE_BYTE:
doCsiUnsupportedParameterOrIntermediateByte(b);
break;
case ESC_CSI_EXCLAMATION:
if (b == 'p') { // Soft terminal reset (DECSTR, http://vt100.net/docs/vt510-rm/DECSTR).
reset();
@@ -796,7 +828,6 @@ public final class TerminalEmulator {
int columnsToDelete = Math.min(getArg0(1), columnsAfterCursor);
int columnsToMove = columnsAfterCursor - columnsToDelete;
mScreen.blockCopy(mCursorCol + columnsToDelete, 0, columnsToMove, mRows, mCursorCol, 0);
blockClear(mCursorRow + columnsToMove, 0, columnsToDelete, mRows);
} else {
unknownSequence(b);
}
@@ -825,7 +856,7 @@ public final class TerminalEmulator {
if (internalBit != -1) {
value = isDecsetInternalBitSet(internalBit) ? 1 : 2; // 1=set, 2=reset.
} else {
mClient.logError(LOG_TAG, "Got DECRQM for unrecognized private DEC mode=" + mode);
Logger.logError(mClient, LOG_TAG, "Got DECRQM for unrecognized private DEC mode=" + mode);
value = 0; // 0=not recognized, 3=permanently set, 4=permanently reset
}
}
@@ -936,10 +967,17 @@ public final class TerminalEmulator {
for (String part : dcs.substring(2).split(";")) {
if (part.length() % 2 == 0) {
StringBuilder transBuffer = new StringBuilder();
char c;
for (int i = 0; i < part.length(); i += 2) {
char c = (char) Long.decode("0x" + part.charAt(i) + "" + part.charAt(i + 1)).longValue();
try {
c = (char) Long.decode("0x" + part.charAt(i) + "" + part.charAt(i + 1)).longValue();
} catch (NumberFormatException e) {
Logger.logStackTraceWithMessage(mClient, LOG_TAG, "Invalid device termcap/terminfo encoded name \"" + part + "\"", e);
continue;
}
transBuffer.append(c);
}
String trans = transBuffer.toString();
String responseValue;
switch (trans) {
@@ -962,7 +1000,7 @@ public final class TerminalEmulator {
case "&8": // Undo key - ignore.
break;
default:
mClient.logWarn(LOG_TAG, "Unhandled termcap/terminfo name: '" + trans + "'");
Logger.logWarn(mClient, LOG_TAG, "Unhandled termcap/terminfo name: '" + trans + "'");
}
// Respond with invalid request:
mSession.write("\033P0+r" + part + "\033\\");
@@ -974,12 +1012,12 @@ public final class TerminalEmulator {
mSession.write("\033P1+r" + part + "=" + hexEncoded + "\033\\");
}
} else {
mClient.logError(LOG_TAG, "Invalid device termcap/terminfo name of odd length: " + part);
Logger.logError(mClient, LOG_TAG, "Invalid device termcap/terminfo name of odd length: " + part);
}
}
} else {
if (LOG_ESCAPE_SEQUENCES)
mClient.logError(LOG_TAG, "Unrecognized device control string: " + dcs);
Logger.logError(mClient, LOG_TAG, "Unrecognized device control string: " + dcs);
}
finishSequence();
}
@@ -996,12 +1034,67 @@ public final class TerminalEmulator {
}
}
/**
* When in {@link #ESC_APC} (APC, Application Program Command) sequence.
*/
private void doApc(int b) {
if (b == 27) {
continueSequence(ESC_APC_ESCAPE);
}
// Eat APC sequences silently for now.
}
/**
* When in {@link #ESC_APC} (APC, Application Program Command) sequence.
*/
private void doApcEscape(int b) {
if (b == '\\') {
// A String Terminator (ST), ending the APC escape sequence.
finishSequence();
} else {
// The Escape character was not the start of a String Terminator (ST),
// but instead just data inside of the APC escape sequence.
continueSequence(ESC_APC);
}
}
private int nextTabStop(int numTabs) {
for (int i = mCursorCol + 1; i < mColumns; i++)
if (mTabStop[i] && --numTabs == 0) return Math.min(i, mRightMargin);
return mRightMargin - 1;
}
/**
* Process byte while in the {@link #ESC_CSI_UNSUPPORTED_PARAMETER_BYTE} or
* {@link #ESC_CSI_UNSUPPORTED_INTERMEDIATE_BYTE} escape state.
*
* Parse unsupported parameter, intermediate and final bytes but ignore them.
*
* > For Control Sequence Introducer, ... the ESC [ is followed by
* > - any number (including none) of "parameter bytes" in the range 0x300x3F (ASCII 09:;<=>?),
* > - then by any number of "intermediate bytes" in the range 0x200x2F (ASCII space and !"#$%&'()*+,-./),
* > - then finally by a single "final byte" in the range 0x400x7E (ASCII @AZ[\]^_`az{|}~).
*
* - https://en.wikipedia.org/wiki/ANSI_escape_code#Control_Sequence_Introducer_commands
* - https://invisible-island.net/xterm/ecma-48-parameter-format.html#section5.4
*/
private void doCsiUnsupportedParameterOrIntermediateByte(int b) {
if (mEscapeState == ESC_CSI_UNSUPPORTED_PARAMETER_BYTE && b >= 0x30 && b <= 0x3F) {
// Supported `09:;>?` or unsupported `<=` parameter byte after an
// initial unsupported parameter byte in `doCsi()`, or a sequential parameter byte.
continueSequence(ESC_CSI_UNSUPPORTED_PARAMETER_BYTE);
} else if (b >= 0x20 && b <= 0x2F) {
// Optional intermediate byte `!"#$%&'()*+,-./` after parameter or intermediate byte.
continueSequence(ESC_CSI_UNSUPPORTED_INTERMEDIATE_BYTE);
} else if (b >= 0x40 && b <= 0x7E) {
// Final byte `@AZ[\]^_`az{|}~` after parameter or intermediate byte.
// Calling `unknownSequence()` would log an error with only a final byte, so ignore it for now.
finishSequence();
} else {
unknownSequence(b);
}
}
/** Process byte while in the {@link #ESC_CSI_QUESTIONMARK} escape state. */
private void doCsiQuestionMark(int b) {
switch (b) {
@@ -1069,7 +1162,7 @@ public final class TerminalEmulator {
int externalBit = mArgs[i];
int internalBit = mapDecSetBitToInternalBit(externalBit);
if (internalBit == -1) {
mClient.logWarn(LOG_TAG, "Ignoring request to save/recall decset bit=" + externalBit);
Logger.logWarn(mClient, LOG_TAG, "Ignoring request to save/recall decset bit=" + externalBit);
} else {
if (b == 's') {
mSavedDecSetFlags |= internalBit;
@@ -1259,7 +1352,7 @@ public final class TerminalEmulator {
// (1) enables this feature for keys except for those with well-known behavior, e.g., Tab, Backarrow and
// some special control character cases, e.g., Control-Space to make a NUL.
// (2) enables this feature for keys including the exceptions listed.
mClient.logError(LOG_TAG, "(ignored) CSI > MODIFY RESOURCE: " + getArg0(-1) + " to " + getArg1(-1));
Logger.logError(mClient, LOG_TAG, "(ignored) CSI > MODIFY RESOURCE: " + getArg0(-1) + " to " + getArg1(-1));
break;
default:
parseArg(b);
@@ -1271,6 +1364,7 @@ public final class TerminalEmulator {
mEscapeState = ESC;
mArgIndex = 0;
Arrays.fill(mArgs, -1);
mArgsSubParamsBitSet = 0;
}
private void doLinefeed() {
@@ -1365,8 +1459,8 @@ public final class TerminalEmulator {
// http://www.vt100.net/docs/vt100-ug/chapter3.html: "Move the active position to the same horizontal
// position on the preceding line. If the active position is at the top margin, a scroll down is performed".
if (mCursorRow <= mTopMargin) {
mScreen.blockCopy(0, mTopMargin, mColumns, mBottomMargin - (mTopMargin + 1), 0, mTopMargin + 1);
blockClear(0, mTopMargin, mColumns);
mScreen.blockCopy(mLeftMargin, mTopMargin, mRightMargin - mLeftMargin, mBottomMargin - (mTopMargin + 1), mLeftMargin, mTopMargin + 1);
blockClear(mLeftMargin, mTopMargin, mRightMargin - mLeftMargin);
} else {
mCursorRow--;
}
@@ -1391,6 +1485,9 @@ public final class TerminalEmulator {
case '>': // DECKPNM
setDecsetinternalBit(DECSET_BIT_APPLICATION_KEYPAD, false);
break;
case '_': // APC - Application Program Command.
continueSequence(ESC_APC);
break;
default:
unknownSequence(b);
break;
@@ -1572,8 +1669,8 @@ public final class TerminalEmulator {
final int linesToScrollArg = getArg0(1);
final int linesBetweenTopAndBottomMargins = mBottomMargin - mTopMargin;
final int linesToScroll = Math.min(linesBetweenTopAndBottomMargins, linesToScrollArg);
mScreen.blockCopy(0, mTopMargin, mColumns, linesBetweenTopAndBottomMargins - linesToScroll, 0, mTopMargin + linesToScroll);
blockClear(0, mTopMargin, mColumns, linesToScroll);
mScreen.blockCopy(mLeftMargin, mTopMargin, mRightMargin - mLeftMargin, linesBetweenTopAndBottomMargins - linesToScroll, mLeftMargin, mTopMargin + linesToScroll);
blockClear(mLeftMargin, mTopMargin, mRightMargin - mLeftMargin, linesToScroll);
} else {
// "${CSI}${func};${startx};${starty};${firstrow};${lastrow}T" - initiate highlight mouse tracking.
unimplementedSequence(b);
@@ -1595,12 +1692,16 @@ public final class TerminalEmulator {
}
mCursorCol = newCol;
break;
case '?': // Esc [ ? -- start of a private mode set
case '?': // Esc [ ? -- start of a private parameter byte
continueSequence(ESC_CSI_QUESTIONMARK);
break;
case '>': // "Esc [ >" --
case '>': // "Esc [ >" -- start of a private parameter byte
continueSequence(ESC_CSI_BIGGERTHAN);
break;
case '<': // "Esc [ <" -- start of a private parameter byte
case '=': // "Esc [ =" -- start of a private parameter byte
continueSequence(ESC_CSI_UNSUPPORTED_PARAMETER_BYTE);
break;
case '`': // Horizontal position absolute (HPA - http://www.vt100.net/docs/vt510-rm/HPA).
setCursorColRespectingOriginMode(getArg0(1) - 1);
break;
@@ -1700,8 +1801,10 @@ public final class TerminalEmulator {
mSession.write("\033[3;0;0t");
break;
case 14: // Report xterm window in pixels. Result is CSI 4 ; height ; width t
// We just report characters time 12 here.
mSession.write(String.format(Locale.US, "\033[4;%d;%dt", mRows * 12, mColumns * 12));
mSession.write(String.format(Locale.US, "\033[4;%d;%dt", mRows * mCellHeightPixels, mColumns * mCellWidthPixels));
break;
case 16: // Report xterm character cell size in pixels. Result is CSI 6 ; height ; width t
mSession.write(String.format(Locale.US, "\033[6;%d;%dt", mCellHeightPixels, mCellWidthPixels));
break;
case 18: // Report the size of the text area in characters. Result is CSI 8 ; height ; width t
mSession.write(String.format(Locale.US, "\033[8;%d;%dt", mRows, mColumns));
@@ -1750,7 +1853,12 @@ public final class TerminalEmulator {
private void selectGraphicRendition() {
if (mArgIndex >= mArgs.length) mArgIndex = mArgs.length - 1;
for (int i = 0; i <= mArgIndex; i++) {
int code = mArgs[i];
// Skip leading sub parameters:
if ((mArgsSubParamsBitSet & (1 << i)) != 0) {
continue;
}
int code = getArg(i, 0, false);
if (code < 0) {
if (mArgIndex > 0) {
continue;
@@ -1769,7 +1877,19 @@ public final class TerminalEmulator {
} else if (code == 3) {
mEffect |= TextStyle.CHARACTER_ATTRIBUTE_ITALIC;
} else if (code == 4) {
mEffect |= TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE;
if (i + 1 <= mArgIndex && ((mArgsSubParamsBitSet & (1 << (i + 1))) != 0)) {
// Sub parameter, see https://sw.kovidgoyal.net/kitty/underlines/
i++;
if (mArgs[i] == 0) {
// No underline.
mEffect &= ~TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE;
} else {
// Different variations of underlines: https://sw.kovidgoyal.net/kitty/underlines/
mEffect |= TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE;
}
} else {
mEffect |= TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE;
}
} else if (code == 5) {
mEffect |= TextStyle.CHARACTER_ATTRIBUTE_BLINK;
} else if (code == 7) {
@@ -1798,40 +1918,43 @@ public final class TerminalEmulator {
mEffect &= ~TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH;
} else if (code >= 30 && code <= 37) {
mForeColor = code - 30;
} else if (code == 38 || code == 48) {
// Extended set foreground(38)/background (48) color.
} else if (code == 38 || code == 48 || code == 58) {
// Extended set foreground(38)/background(48)/underline(58) color.
// This is followed by either "2;$R;$G;$B" to set a 24-bit color or
// "5;$INDEX" to set an indexed color.
if (i + 2 > mArgIndex) continue;
int firstArg = mArgs[i + 1];
if (firstArg == 2) {
if (i + 4 > mArgIndex) {
mClient.logWarn(LOG_TAG, "Too few CSI" + code + ";2 RGB arguments");
Logger.logWarn(mClient, LOG_TAG, "Too few CSI" + code + ";2 RGB arguments");
} else {
int red = mArgs[i + 2], green = mArgs[i + 3], blue = mArgs[i + 4];
int red = getArg(i + 2, 0, false);
int green = getArg(i + 3, 0, false);
int blue = getArg(i + 4, 0, false);
if (red < 0 || green < 0 || blue < 0 || red > 255 || green > 255 || blue > 255) {
finishSequenceAndLogError("Invalid RGB: " + red + "," + green + "," + blue);
} else {
int argbColor = 0xff000000 | (red << 16) | (green << 8) | blue;
if (code == 38) {
mForeColor = argbColor;
} else {
mBackColor = argbColor;
int argbColor = 0xff_00_00_00 | (red << 16) | (green << 8) | blue;
switch (code) {
case 38: mForeColor = argbColor; break;
case 48: mBackColor = argbColor; break;
case 58: mUnderlineColor = argbColor; break;
}
}
i += 4; // "2;P_r;P_g;P_r"
}
} else if (firstArg == 5) {
int color = mArgs[i + 2];
int color = getArg(i + 2, 0, false);
i += 2; // "5;P_s"
if (color >= 0 && color < TextStyle.NUM_INDEXED_COLORS) {
if (code == 38) {
mForeColor = color;
} else {
mBackColor = color;
switch (code) {
case 38: mForeColor = color; break;
case 48: mBackColor = color; break;
case 58: mUnderlineColor = color; break;
}
} else {
if (LOG_ESCAPE_SEQUENCES) mClient.logWarn(LOG_TAG, "Invalid color index: " + color);
if (LOG_ESCAPE_SEQUENCES) Logger.logWarn(mClient, LOG_TAG, "Invalid color index: " + color);
}
} else {
finishSequenceAndLogError("Invalid ISO-8613-3 SGR first argument: " + firstArg);
@@ -1842,13 +1965,15 @@ public final class TerminalEmulator {
mBackColor = code - 40;
} else if (code == 49) { // Set default background color.
mBackColor = TextStyle.COLOR_INDEX_BACKGROUND;
} else if (code == 59) { // Set default underline color.
mUnderlineColor = TextStyle.COLOR_INDEX_FOREGROUND;
} else if (code >= 90 && code <= 97) { // Bright foreground colors (aixterm codes).
mForeColor = code - 90 + 8;
} else if (code >= 100 && code <= 107) { // Bright background color (aixterm codes).
mBackColor = code - 100 + 8;
} else {
if (LOG_ESCAPE_SEQUENCES)
mClient.logWarn(LOG_TAG, String.format("SGR unknown code %d", code));
Logger.logWarn(mClient, LOG_TAG, String.format("SGR unknown code %d", code));
}
}
}
@@ -1982,7 +2107,7 @@ public final class TerminalEmulator {
String clipboardText = new String(Base64.decode(textParameter.substring(startIndex), 0), StandardCharsets.UTF_8);
mSession.onCopyTextToClipboard(clipboardText);
} catch (Exception e) {
mClient.logError(LOG_TAG, "OSC Manipulate selection, invalid string '" + textParameter + "");
Logger.logError(mClient, LOG_TAG, "OSC Manipulate selection, invalid string '" + textParameter + "");
}
break;
case 104:
@@ -2077,17 +2202,36 @@ public final class TerminalEmulator {
private void scrollDownOneLine() {
mScrollCounter++;
long currentStyle = getStyle();
if (mLeftMargin != 0 || mRightMargin != mColumns) {
// Horizontal margin: Do not put anything into scroll history, just non-margin part of screen up.
mScreen.blockCopy(mLeftMargin, mTopMargin + 1, mRightMargin - mLeftMargin, mBottomMargin - mTopMargin - 1, mLeftMargin, mTopMargin);
// .. and blank bottom row between margins:
mScreen.blockSet(mLeftMargin, mBottomMargin - 1, mRightMargin - mLeftMargin, 1, ' ', mEffect);
mScreen.blockSet(mLeftMargin, mBottomMargin - 1, mRightMargin - mLeftMargin, 1, ' ', currentStyle);
} else {
mScreen.scrollDownOneLine(mTopMargin, mBottomMargin, getStyle());
mScreen.scrollDownOneLine(mTopMargin, mBottomMargin, currentStyle);
}
}
/** Process the next ASCII character of a parameter. */
/**
* Process the next ASCII character of a parameter.
*
* <p>You must use the ; character to separate parameters and : to separate sub-parameters.
*
* <p>Parameter characters modify the action or interpretation of the sequence. Originally
* you can use up to 16 parameters per sequence, but following at least xterm and alacritty
* we use a common space for parameters and sub-parameters, allowing 32 in total.
*
* <p>All parameters are unsigned, positive decimal integers, with the most significant
* digit sent first. Any parameter greater than 9999 (decimal) is set to 9999
* (decimal). If you do not specify a value, a 0 value is assumed. A 0 value
* or omitted parameter indicates a default value for the sequence. For most
* sequences, the default value is 1.
*
* <p>References:
* <a href="https://vt100.net/docs/vt510-rm/chapter4.html#S4.3.3">VT510 Video Terminal Programmer Information: Control Sequences</a>
* <a href="https://github.com/alacritty/vte/issues/22">alacritty/vte: Implement colon separated CSI parameters</a>
* */
private void parseArg(int b) {
if (b >= '0' && b <= '9') {
if (mArgIndex < mArgs.length) {
@@ -2099,12 +2243,19 @@ public final class TerminalEmulator {
} else {
value = thisDigit;
}
if (value > 9999)
value = 9999;
mArgs[mArgIndex] = value;
}
continueSequence(mEscapeState);
} else if (b == ';') {
if (mArgIndex < mArgs.length) {
} else if (b == ';' || b == ':') {
if (mArgIndex + 1 < mArgs.length) {
mArgIndex++;
if (b == ':') {
mArgsSubParamsBitSet |= 1 << mArgIndex;
}
} else {
logError("Too many parameters when in state: " + mEscapeState);
}
continueSequence(mEscapeState);
} else {
@@ -2178,7 +2329,7 @@ public final class TerminalEmulator {
}
private void finishSequenceAndLogError(String error) {
if (LOG_ESCAPE_SEQUENCES) mClient.logWarn(LOG_TAG, error);
if (LOG_ESCAPE_SEQUENCES) Logger.logWarn(mClient, LOG_TAG, error);
finishSequence();
}
@@ -2326,7 +2477,14 @@ public final class TerminalEmulator {
}
int offsetDueToCombiningChar = ((displayWidth <= 0 && mCursorCol > 0 && !mAboutToAutoWrap) ? 1 : 0);
mScreen.setChar(mCursorCol - offsetDueToCombiningChar, mCursorRow, codePoint, getStyle());
int column = mCursorCol - offsetDueToCombiningChar;
// Fix TerminalRow.setChar() ArrayIndexOutOfBoundsException index=-1 exception reported
// The offsetDueToCombiningChar would never be 1 if mCursorCol was 0 to get column/index=-1,
// so was mCursorCol changed after the offsetDueToCombiningChar conditional by another thread?
// TODO: Check if there are thread synchronization issues with mCursorCol and mCursorRow, possibly causing others bugs too.
if (column < 0) column = 0;
mScreen.setChar(column, mCursorRow, codePoint, getStyle());
if (autoWrap && displayWidth > 0)
mAboutToAutoWrap = (mCursorCol == mRightMargin - displayWidth);

View File

@@ -11,11 +11,37 @@ public final class TerminalRow {
private static final float SPARE_CAPACITY_FACTOR = 1.5f;
/**
* Max combining characters that can exist in a column, that are separate from the base character
* itself. Any additional combining characters will be ignored and not added to the column.
*
* There does not seem to be limit in unicode standard for max number of combination characters
* that can be combined but such characters are primarily under 10.
*
* "Section 3.6 Combination" of unicode standard contains combining characters info.
* - https://www.unicode.org/versions/Unicode15.0.0/ch03.pdf
* - https://en.wikipedia.org/wiki/Combining_character#Unicode_ranges
* - https://stackoverflow.com/questions/71237212/what-is-the-maximum-number-of-unicode-combined-characters-that-may-be-needed-to
*
* UAX15-D3 Stream-Safe Text Format limits to max 30 combining characters.
* > The value of 30 is chosen to be significantly beyond what is required for any linguistic or technical usage.
* > While it would have been feasible to chose a smaller number, this value provides a very wide margin,
* > yet is well within the buffer size limits of practical implementations.
* - https://unicode.org/reports/tr15/#Stream_Safe_Text_Format
* - https://stackoverflow.com/a/11983435/14686958
*
* We choose the value 15 because it should be enough for terminal based applications and keep
* the memory usage low for a terminal row, won't affect performance or cause terminal to
* lag or hang, and will keep malicious applications from causing harm. The value can be
* increased if ever needed for legitimate applications.
*/
private static final int MAX_COMBINING_CHARACTERS_PER_COLUMN = 15;
/** The number of columns in this terminal row. */
private final int mColumns;
/** The text filling this terminal row. */
public char[] mText;
/** The number of java char:s used in {@link #mText}. */
/** The number of java chars used in {@link #mText}. */
private short mSpaceUsed;
/** If this row has been line wrapped due to text output at the end of line. */
boolean mLineWrap;
@@ -124,6 +150,9 @@ public final class TerminalRow {
// https://github.com/steven676/Android-Terminal-Emulator/commit/9a47042620bec87617f0b4f5d50568535668fe26
public void setChar(int columnToSet, int codePoint, long style) {
if (columnToSet < 0 || columnToSet >= mStyle.length)
throw new IllegalArgumentException("TerminalRow.setChar(): columnToSet=" + columnToSet + ", codePoint=" + codePoint + ", style=" + style);
mStyle[columnToSet] = style;
final int newCodePointDisplayWidth = WcWidth.width(codePoint);
@@ -160,18 +189,25 @@ public final class TerminalRow {
// Get the number of elements in the mText array this column uses now
int oldCharactersUsedForColumn;
if (columnToSet + oldCodePointDisplayWidth < mColumns) {
oldCharactersUsedForColumn = findStartOfColumn(columnToSet + oldCodePointDisplayWidth) - oldStartOfColumnIndex;
int oldEndOfColumnIndex = findStartOfColumn(columnToSet + oldCodePointDisplayWidth);
oldCharactersUsedForColumn = oldEndOfColumnIndex - oldStartOfColumnIndex;
} else {
// Last character.
oldCharactersUsedForColumn = mSpaceUsed - oldStartOfColumnIndex;
}
// If MAX_COMBINING_CHARACTERS_PER_COLUMN already exist in column, then ignore adding additional combining characters.
if (newIsCombining) {
int combiningCharsCount = WcWidth.zeroWidthCharsCount(mText, oldStartOfColumnIndex, oldStartOfColumnIndex + oldCharactersUsedForColumn);
if (combiningCharsCount >= MAX_COMBINING_CHARACTERS_PER_COLUMN)
return;
}
// Find how many chars this column will need
int newCharactersUsedForColumn = Character.charCount(codePoint);
if (newIsCombining) {
// Combining characters are added to the contents of the column instead of overwriting them, so that they
// modify the existing contents.
// FIXME: Put a limit of combining characters.
// FIXME: Unassigned characters also get width=0.
newCharactersUsedForColumn += oldCharactersUsedForColumn;
}
@@ -186,7 +222,7 @@ public final class TerminalRow {
if (mSpaceUsed + javaCharDifference > text.length) {
// We need to grow the array
char[] newText = new char[text.length + mColumns];
System.arraycopy(text, 0, newText, 0, oldStartOfColumnIndex + oldCharactersUsedForColumn);
System.arraycopy(text, 0, newText, 0, oldNextColumnIndex);
System.arraycopy(text, oldNextColumnIndex, newText, newNextColumnIndex, oldCharactersAfterColumn);
mText = text = newText;
} else {

View File

@@ -21,7 +21,7 @@ import java.util.UUID;
* A terminal session, consisting of a process coupled to a terminal interface.
* <p>
* The subprocess will be executed by the constructor, and when the size is made known by a call to
* {@link #updateSize(int, int)} terminal emulation will begin and threads will be spawned to handle the subprocess I/O.
* {@link #updateSize(int, int, int, int)} terminal emulation will begin and threads will be spawned to handle the subprocess I/O.
* All terminal emulation and callback methods will be performed on the main thread.
* <p>
* The child process may be exited forcefully by using the {@link #finishIfRunning()} method.
@@ -61,7 +61,7 @@ public final class TerminalSession extends TerminalOutput {
/**
* The file descriptor referencing the master half of a pseudo-terminal pair, resulting from calling
* {@link JNI#createSubprocess(String, String, String[], String[], int[], int, int)}.
* {@link JNI#createSubprocess(String, String, String[], String[], int[], int, int, int, int)}.
*/
private int mTerminalFileDescriptor;
@@ -100,12 +100,12 @@ public final class TerminalSession extends TerminalOutput {
}
/** Inform the attached pty of the new size and reflow or initialize the emulator. */
public void updateSize(int columns, int rows) {
public void updateSize(int columns, int rows, int cellWidthPixels, int cellHeightPixels) {
if (mEmulator == null) {
initializeEmulator(columns, rows);
initializeEmulator(columns, rows, cellWidthPixels, cellHeightPixels);
} else {
JNI.setPtyWindowSize(mTerminalFileDescriptor, rows, columns);
mEmulator.resize(columns, rows);
JNI.setPtyWindowSize(mTerminalFileDescriptor, rows, columns, cellWidthPixels, cellHeightPixels);
mEmulator.resize(columns, rows, cellWidthPixels, cellHeightPixels);
}
}
@@ -120,11 +120,11 @@ public final class TerminalSession extends TerminalOutput {
* @param columns The number of columns in the terminal window.
* @param rows The number of rows in the terminal window.
*/
public void initializeEmulator(int columns, int rows) {
mEmulator = new TerminalEmulator(this, columns, rows, mTranscriptRows, mClient);
public void initializeEmulator(int columns, int rows, int cellWidthPixels, int cellHeightPixels) {
mEmulator = new TerminalEmulator(this, columns, rows, cellWidthPixels, cellHeightPixels, mTranscriptRows, mClient);
int[] processId = new int[1];
mTerminalFileDescriptor = JNI.createSubprocess(mShellPath, mCwd, mArgs, mEnv, processId, rows, columns);
mTerminalFileDescriptor = JNI.createSubprocess(mShellPath, mCwd, mArgs, mEnv, processId, rows, columns, cellWidthPixels, cellHeightPixels);
mShellPid = processId[0];
final FileDescriptor terminalFileDescriptorWrapped = wrapFileDescriptor(mTerminalFileDescriptor, mClient);
@@ -236,7 +236,7 @@ public final class TerminalSession extends TerminalOutput {
try {
Os.kill(mShellPid, OsConstants.SIGKILL);
} catch (ErrnoException e) {
mClient.logWarn(LOG_TAG, "Failed sending SIGKILL: " + e.getMessage());
Logger.logWarn(mClient, LOG_TAG, "Failed sending SIGKILL: " + e.getMessage());
}
}
}
@@ -308,7 +308,7 @@ public final class TerminalSession extends TerminalOutput {
return outputPath;
}
} catch (IOException | SecurityException e) {
mClient.logStackTraceWithMessage(LOG_TAG, "Error getting current directory", e);
Logger.logStackTraceWithMessage(mClient, LOG_TAG, "Error getting current directory", e);
}
return null;
}
@@ -326,7 +326,7 @@ public final class TerminalSession extends TerminalOutput {
descriptorField.setAccessible(true);
descriptorField.set(result, fileDescriptor);
} catch (NoSuchFieldException | IllegalAccessException | IllegalArgumentException e) {
client.logStackTraceWithMessage(LOG_TAG, "Error accessing FileDescriptor#descriptor private field", e);
Logger.logStackTraceWithMessage(client, LOG_TAG, "Error accessing FileDescriptor#descriptor private field", e);
System.exit(1);
}
return result;

View File

@@ -1,7 +1,7 @@
package com.termux.terminal;
/**
* Implementation of wcwidth(3) for Unicode 9.
* Implementation of wcwidth(3) for Unicode 15.
*
* Implementation from https://github.com/jquast/wcwidth but we return 0 for unprintable characters.
*
@@ -9,12 +9,13 @@ package com.termux.terminal;
* Must be kept in sync with the following:
* https://github.com/termux/wcwidth
* https://github.com/termux/libandroid-support
* https://github.com/termux/termux-packages/tree/master/libandroid-support
* https://github.com/termux/termux-packages/tree/master/packages/libandroid-support
*/
public final class WcWidth {
// From https://github.com/jquast/wcwidth/blob/master/wcwidth/table_zero.py
// at commit b29897e5a1b403a0e36f7fc991614981cbc42475 (2020-07-14):
// from https://github.com/jquast/wcwidth/pull/64
// at commit 1b9b6585b0080ea5cb88dc9815796505724793fe (2022-12-16):
private static final int[][] ZERO_WIDTH = {
{0x00300, 0x0036f}, // Combining Grave Accent ..Combining Latin Small Le
{0x00483, 0x00489}, // Combining Cyrillic Titlo..Combining Cyrillic Milli
@@ -40,7 +41,8 @@ public final class WcWidth {
{0x00825, 0x00827}, // Samaritan Vowel Sign Sho..Samaritan Vowel Sign U
{0x00829, 0x0082d}, // Samaritan Vowel Sign Lon..Samaritan Mark Nequdaa
{0x00859, 0x0085b}, // Mandaic Affrication Mark..Mandaic Gemination Mark
{0x008d3, 0x008e1}, // Arabic Small Low Waw ..Arabic Small High Sign S
{0x00898, 0x0089f}, // Arabic Small High Word A..Arabic Half Madda Over M
{0x008ca, 0x008e1}, // Arabic Small High Farsi ..Arabic Small High Sign S
{0x008e3, 0x00902}, // Arabic Turned Damma Belo..Devanagari Sign Anusvara
{0x0093a, 0x0093a}, // Devanagari Vowel Sign Oe..Devanagari Vowel Sign Oe
{0x0093c, 0x0093c}, // Devanagari Sign Nukta ..Devanagari Sign Nukta
@@ -74,13 +76,14 @@ public final class WcWidth {
{0x00b3f, 0x00b3f}, // Oriya Vowel Sign I ..Oriya Vowel Sign I
{0x00b41, 0x00b44}, // Oriya Vowel Sign U ..Oriya Vowel Sign Vocalic
{0x00b4d, 0x00b4d}, // Oriya Sign Virama ..Oriya Sign Virama
{0x00b55, 0x00b56}, // (nil) ..Oriya Ai Length Mark
{0x00b55, 0x00b56}, // Oriya Sign Overline ..Oriya Ai Length Mark
{0x00b62, 0x00b63}, // Oriya Vowel Sign Vocalic..Oriya Vowel Sign Vocalic
{0x00b82, 0x00b82}, // Tamil Sign Anusvara ..Tamil Sign Anusvara
{0x00bc0, 0x00bc0}, // Tamil Vowel Sign Ii ..Tamil Vowel Sign Ii
{0x00bcd, 0x00bcd}, // Tamil Sign Virama ..Tamil Sign Virama
{0x00c00, 0x00c00}, // Telugu Sign Combining Ca..Telugu Sign Combining Ca
{0x00c04, 0x00c04}, // Telugu Sign Combining An..Telugu Sign Combining An
{0x00c3c, 0x00c3c}, // Telugu Sign Nukta ..Telugu Sign Nukta
{0x00c3e, 0x00c40}, // Telugu Vowel Sign Aa ..Telugu Vowel Sign Ii
{0x00c46, 0x00c48}, // Telugu Vowel Sign E ..Telugu Vowel Sign Ai
{0x00c4a, 0x00c4d}, // Telugu Vowel Sign O ..Telugu Sign Virama
@@ -97,7 +100,7 @@ public final class WcWidth {
{0x00d41, 0x00d44}, // Malayalam Vowel Sign U ..Malayalam Vowel Sign Voc
{0x00d4d, 0x00d4d}, // Malayalam Sign Virama ..Malayalam Sign Virama
{0x00d62, 0x00d63}, // Malayalam Vowel Sign Voc..Malayalam Vowel Sign Voc
{0x00d81, 0x00d81}, // (nil) ..(nil)
{0x00d81, 0x00d81}, // Sinhala Sign Candrabindu..Sinhala Sign Candrabindu
{0x00dca, 0x00dca}, // Sinhala Sign Al-lakuna ..Sinhala Sign Al-lakuna
{0x00dd2, 0x00dd4}, // Sinhala Vowel Sign Ketti..Sinhala Vowel Sign Ketti
{0x00dd6, 0x00dd6}, // Sinhala Vowel Sign Diga ..Sinhala Vowel Sign Diga
@@ -106,7 +109,7 @@ public final class WcWidth {
{0x00e47, 0x00e4e}, // Thai Character Maitaikhu..Thai Character Yamakkan
{0x00eb1, 0x00eb1}, // Lao Vowel Sign Mai Kan ..Lao Vowel Sign Mai Kan
{0x00eb4, 0x00ebc}, // Lao Vowel Sign I ..Lao Semivowel Sign Lo
{0x00ec8, 0x00ecd}, // Lao Tone Mai Ek ..Lao Niggahita
{0x00ec8, 0x00ece}, // Lao Tone Mai Ek ..(nil)
{0x00f18, 0x00f19}, // Tibetan Astrological Sig..Tibetan Astrological Sig
{0x00f35, 0x00f35}, // Tibetan Mark Ngas Bzung ..Tibetan Mark Ngas Bzung
{0x00f37, 0x00f37}, // Tibetan Mark Ngas Bzung ..Tibetan Mark Ngas Bzung
@@ -130,7 +133,7 @@ public final class WcWidth {
{0x0109d, 0x0109d}, // Myanmar Vowel Sign Aiton..Myanmar Vowel Sign Aiton
{0x0135d, 0x0135f}, // Ethiopic Combining Gemin..Ethiopic Combining Gemin
{0x01712, 0x01714}, // Tagalog Vowel Sign I ..Tagalog Sign Virama
{0x01732, 0x01734}, // Hanunoo Vowel Sign I ..Hanunoo Sign Pamudpod
{0x01732, 0x01733}, // Hanunoo Vowel Sign I ..Hanunoo Vowel Sign U
{0x01752, 0x01753}, // Buhid Vowel Sign I ..Buhid Vowel Sign U
{0x01772, 0x01773}, // Tagbanwa Vowel Sign I ..Tagbanwa Vowel Sign U
{0x017b4, 0x017b5}, // Khmer Vowel Inherent Aq ..Khmer Vowel Inherent Aa
@@ -139,6 +142,7 @@ public final class WcWidth {
{0x017c9, 0x017d3}, // Khmer Sign Muusikatoan ..Khmer Sign Bathamasat
{0x017dd, 0x017dd}, // Khmer Sign Atthacan ..Khmer Sign Atthacan
{0x0180b, 0x0180d}, // Mongolian Free Variation..Mongolian Free Variation
{0x0180f, 0x0180f}, // Mongolian Free Variation..Mongolian Free Variation
{0x01885, 0x01886}, // Mongolian Letter Ali Gal..Mongolian Letter Ali Gal
{0x018a9, 0x018a9}, // Mongolian Letter Ali Gal..Mongolian Letter Ali Gal
{0x01920, 0x01922}, // Limbu Vowel Sign A ..Limbu Vowel Sign U
@@ -154,7 +158,7 @@ public final class WcWidth {
{0x01a65, 0x01a6c}, // Tai Tham Vowel Sign I ..Tai Tham Vowel Sign Oa B
{0x01a73, 0x01a7c}, // Tai Tham Vowel Sign Oa A..Tai Tham Sign Khuen-lue
{0x01a7f, 0x01a7f}, // Tai Tham Combining Crypt..Tai Tham Combining Crypt
{0x01ab0, 0x01ac0}, // Combining Doubled Circum..(nil)
{0x01ab0, 0x01ace}, // Combining Doubled Circum..Combining Latin Small Le
{0x01b00, 0x01b03}, // Balinese Sign Ulu Ricem ..Balinese Sign Surang
{0x01b34, 0x01b34}, // Balinese Sign Rerekan ..Balinese Sign Rerekan
{0x01b36, 0x01b3a}, // Balinese Vowel Sign Ulu ..Balinese Vowel Sign Ra R
@@ -177,8 +181,7 @@ public final class WcWidth {
{0x01ced, 0x01ced}, // Vedic Sign Tiryak ..Vedic Sign Tiryak
{0x01cf4, 0x01cf4}, // Vedic Tone Candra Above ..Vedic Tone Candra Above
{0x01cf8, 0x01cf9}, // Vedic Tone Ring Above ..Vedic Tone Double Ring A
{0x01dc0, 0x01df9}, // Combining Dotted Grave A..Combining Wide Inverted
{0x01dfb, 0x01dff}, // Combining Deletion Mark ..Combining Right Arrowhea
{0x01dc0, 0x01dff}, // Combining Dotted Grave A..Combining Right Arrowhea
{0x020d0, 0x020f0}, // Combining Left Harpoon A..Combining Asterisk Above
{0x02cef, 0x02cf1}, // Coptic Combining Ni Abov..Coptic Combining Spiritu
{0x02d7f, 0x02d7f}, // Tifinagh Consonant Joine..Tifinagh Consonant Joine
@@ -193,7 +196,7 @@ public final class WcWidth {
{0x0a806, 0x0a806}, // Syloti Nagri Sign Hasant..Syloti Nagri Sign Hasant
{0x0a80b, 0x0a80b}, // Syloti Nagri Sign Anusva..Syloti Nagri Sign Anusva
{0x0a825, 0x0a826}, // Syloti Nagri Vowel Sign ..Syloti Nagri Vowel Sign
{0x0a82c, 0x0a82c}, // (nil) ..(nil)
{0x0a82c, 0x0a82c}, // Syloti Nagri Sign Altern..Syloti Nagri Sign Altern
{0x0a8c4, 0x0a8c5}, // Saurashtra Sign Virama ..Saurashtra Sign Candrabi
{0x0a8e0, 0x0a8f1}, // Combining Devanagari Dig..Combining Devanagari Sig
{0x0a8ff, 0x0a8ff}, // Devanagari Vowel Sign Ay..Devanagari Vowel Sign Ay
@@ -233,13 +236,18 @@ public final class WcWidth {
{0x10a3f, 0x10a3f}, // Kharoshthi Virama ..Kharoshthi Virama
{0x10ae5, 0x10ae6}, // Manichaean Abbreviation ..Manichaean Abbreviation
{0x10d24, 0x10d27}, // Hanifi Rohingya Sign Har..Hanifi Rohingya Sign Tas
{0x10eab, 0x10eac}, // (nil) ..(nil)
{0x10eab, 0x10eac}, // Yezidi Combining Hamza M..Yezidi Combining Madda M
{0x10efd, 0x10eff}, // (nil) ..(nil)
{0x10f46, 0x10f50}, // Sogdian Combining Dot Be..Sogdian Combining Stroke
{0x10f82, 0x10f85}, // Old Uyghur Combining Dot..Old Uyghur Combining Two
{0x11001, 0x11001}, // Brahmi Sign Anusvara ..Brahmi Sign Anusvara
{0x11038, 0x11046}, // Brahmi Vowel Sign Aa ..Brahmi Virama
{0x11070, 0x11070}, // Brahmi Sign Old Tamil Vi..Brahmi Sign Old Tamil Vi
{0x11073, 0x11074}, // Brahmi Vowel Sign Old Ta..Brahmi Vowel Sign Old Ta
{0x1107f, 0x11081}, // Brahmi Number Joiner ..Kaithi Sign Anusvara
{0x110b3, 0x110b6}, // Kaithi Vowel Sign U ..Kaithi Vowel Sign Ai
{0x110b9, 0x110ba}, // Kaithi Sign Virama ..Kaithi Sign Nukta
{0x110c2, 0x110c2}, // Kaithi Vowel Sign Vocali..Kaithi Vowel Sign Vocali
{0x11100, 0x11102}, // Chakma Sign Candrabindu ..Chakma Sign Visarga
{0x11127, 0x1112b}, // Chakma Vowel Sign A ..Chakma Vowel Sign Uu
{0x1112d, 0x11134}, // Chakma Vowel Sign Ai ..Chakma Maayyaa
@@ -247,11 +255,12 @@ public final class WcWidth {
{0x11180, 0x11181}, // Sharada Sign Candrabindu..Sharada Sign Anusvara
{0x111b6, 0x111be}, // Sharada Vowel Sign U ..Sharada Vowel Sign O
{0x111c9, 0x111cc}, // Sharada Sandhi Mark ..Sharada Extra Short Vowe
{0x111cf, 0x111cf}, // (nil) ..(nil)
{0x111cf, 0x111cf}, // Sharada Sign Inverted Ca..Sharada Sign Inverted Ca
{0x1122f, 0x11231}, // Khojki Vowel Sign U ..Khojki Vowel Sign Ai
{0x11234, 0x11234}, // Khojki Sign Anusvara ..Khojki Sign Anusvara
{0x11236, 0x11237}, // Khojki Sign Nukta ..Khojki Sign Shadda
{0x1123e, 0x1123e}, // Khojki Sign Sukun ..Khojki Sign Sukun
{0x11241, 0x11241}, // (nil) ..(nil)
{0x112df, 0x112df}, // Khudawadi Sign Anusvara ..Khudawadi Sign Anusvara
{0x112e3, 0x112ea}, // Khudawadi Vowel Sign U ..Khudawadi Sign Virama
{0x11300, 0x11301}, // Grantha Sign Combining A..Grantha Sign Candrabindu
@@ -283,9 +292,9 @@ public final class WcWidth {
{0x11727, 0x1172b}, // Ahom Vowel Sign Aw ..Ahom Sign Killer
{0x1182f, 0x11837}, // Dogra Vowel Sign U ..Dogra Sign Anusvara
{0x11839, 0x1183a}, // Dogra Sign Virama ..Dogra Sign Nukta
{0x1193b, 0x1193c}, // (nil) ..(nil)
{0x1193e, 0x1193e}, // (nil) ..(nil)
{0x11943, 0x11943}, // (nil) ..(nil)
{0x1193b, 0x1193c}, // Dives Akuru Sign Anusvar..Dives Akuru Sign Candrab
{0x1193e, 0x1193e}, // Dives Akuru Virama ..Dives Akuru Virama
{0x11943, 0x11943}, // Dives Akuru Sign Nukta ..Dives Akuru Sign Nukta
{0x119d4, 0x119d7}, // Nandinagari Vowel Sign U..Nandinagari Vowel Sign V
{0x119da, 0x119db}, // Nandinagari Vowel Sign E..Nandinagari Vowel Sign A
{0x119e0, 0x119e0}, // Nandinagari Sign Virama ..Nandinagari Sign Virama
@@ -313,12 +322,20 @@ public final class WcWidth {
{0x11d95, 0x11d95}, // Gunjala Gondi Sign Anusv..Gunjala Gondi Sign Anusv
{0x11d97, 0x11d97}, // Gunjala Gondi Virama ..Gunjala Gondi Virama
{0x11ef3, 0x11ef4}, // Makasar Vowel Sign I ..Makasar Vowel Sign U
{0x11f00, 0x11f01}, // (nil) ..(nil)
{0x11f36, 0x11f3a}, // (nil) ..(nil)
{0x11f40, 0x11f40}, // (nil) ..(nil)
{0x11f42, 0x11f42}, // (nil) ..(nil)
{0x13440, 0x13440}, // (nil) ..(nil)
{0x13447, 0x13455}, // (nil) ..(nil)
{0x16af0, 0x16af4}, // Bassa Vah Combining High..Bassa Vah Combining High
{0x16b30, 0x16b36}, // Pahawh Hmong Mark Cim Tu..Pahawh Hmong Mark Cim Ta
{0x16f4f, 0x16f4f}, // Miao Sign Consonant Modi..Miao Sign Consonant Modi
{0x16f8f, 0x16f92}, // Miao Tone Right ..Miao Tone Below
{0x16fe4, 0x16fe4}, // (nil) ..(nil)
{0x16fe4, 0x16fe4}, // Khitan Small Script Fill..Khitan Small Script Fill
{0x1bc9d, 0x1bc9e}, // Duployan Thick Letter Se..Duployan Double Mark
{0x1cf00, 0x1cf2d}, // Znamenny Combining Mark ..Znamenny Combining Mark
{0x1cf30, 0x1cf46}, // Znamenny Combining Tonal..Znamenny Priznak Modifie
{0x1d167, 0x1d169}, // Musical Symbol Combining..Musical Symbol Combining
{0x1d17b, 0x1d182}, // Musical Symbol Combining..Musical Symbol Combining
{0x1d185, 0x1d18b}, // Musical Symbol Combining..Musical Symbol Combining
@@ -335,15 +352,19 @@ public final class WcWidth {
{0x1e01b, 0x1e021}, // Combining Glagolitic Let..Combining Glagolitic Let
{0x1e023, 0x1e024}, // Combining Glagolitic Let..Combining Glagolitic Let
{0x1e026, 0x1e02a}, // Combining Glagolitic Let..Combining Glagolitic Let
{0x1e08f, 0x1e08f}, // (nil) ..(nil)
{0x1e130, 0x1e136}, // Nyiakeng Puachue Hmong T..Nyiakeng Puachue Hmong T
{0x1e2ae, 0x1e2ae}, // Toto Sign Rising Tone ..Toto Sign Rising Tone
{0x1e2ec, 0x1e2ef}, // Wancho Tone Tup ..Wancho Tone Koini
{0x1e4ec, 0x1e4ef}, // (nil) ..(nil)
{0x1e8d0, 0x1e8d6}, // Mende Kikakui Combining ..Mende Kikakui Combining
{0x1e944, 0x1e94a}, // Adlam Alif Lengthener ..Adlam Nukta
{0xe0100, 0xe01ef}, // Variation Selector-17 ..Variation Selector-256
};
// https://github.com/jquast/wcwidth/blob/master/wcwidth/table_wide.py
// at commit b29897e5a1b403a0e36f7fc991614981cbc42475 (2020-07-14):
// from https://github.com/jquast/wcwidth/pull/64
// at commit 1b9b6585b0080ea5cb88dc9815796505724793fe (2022-12-16):
private static final int[][] WIDE_EASTASIAN = {
{0x01100, 0x0115f}, // Hangul Choseong Kiyeok ..Hangul Choseong Filler
{0x0231a, 0x0231b}, // Watch ..Hourglass
@@ -392,7 +413,7 @@ public final class WcWidth {
{0x03190, 0x031e3}, // Ideographic Annotation L..Cjk Stroke Q
{0x031f0, 0x0321e}, // Katakana Letter Small Ku..Parenthesized Korean Cha
{0x03220, 0x03247}, // Parenthesized Ideograph ..Circled Ideograph Koto
{0x03250, 0x04dbf}, // Partnership Sign ..(nil)
{0x03250, 0x04dbf}, // Partnership Sign ..Cjk Unified Ideograph-4d
{0x04e00, 0x0a48c}, // Cjk Unified Ideograph-4e..Yi Syllable Yyr
{0x0a490, 0x0a4c6}, // Yi Radical Qot ..Yi Radical Ke
{0x0a960, 0x0a97c}, // Hangul Choseong Tikeut-m..Hangul Choseong Ssangyeo
@@ -404,13 +425,18 @@ public final class WcWidth {
{0x0fe68, 0x0fe6b}, // Small Reverse Solidus ..Small Commercial At
{0x0ff01, 0x0ff60}, // Fullwidth Exclamation Ma..Fullwidth Right White Pa
{0x0ffe0, 0x0ffe6}, // Fullwidth Cent Sign ..Fullwidth Won Sign
{0x16fe0, 0x16fe4}, // Tangut Iteration Mark ..(nil)
{0x16ff0, 0x16ff1}, // (nil) ..(nil)
{0x16fe0, 0x16fe4}, // Tangut Iteration Mark ..Khitan Small Script Fill
{0x16ff0, 0x16ff1}, // Vietnamese Alternate Rea..Vietnamese Alternate Rea
{0x17000, 0x187f7}, // (nil) ..(nil)
{0x18800, 0x18cd5}, // Tangut Component-001 ..(nil)
{0x18800, 0x18cd5}, // Tangut Component-001 ..Khitan Small Script Char
{0x18d00, 0x18d08}, // (nil) ..(nil)
{0x1b000, 0x1b11e}, // Katakana Letter Archaic ..Hentaigana Letter N-mu-m
{0x1aff0, 0x1aff3}, // Katakana Letter Minnan T..Katakana Letter Minnan T
{0x1aff5, 0x1affb}, // Katakana Letter Minnan T..Katakana Letter Minnan N
{0x1affd, 0x1affe}, // Katakana Letter Minnan N..Katakana Letter Minnan N
{0x1b000, 0x1b122}, // Katakana Letter Archaic ..Katakana Letter Archaic
{0x1b132, 0x1b132}, // (nil) ..(nil)
{0x1b150, 0x1b152}, // Hiragana Letter Small Wi..Hiragana Letter Small Wo
{0x1b155, 0x1b155}, // (nil) ..(nil)
{0x1b164, 0x1b167}, // Katakana Letter Small Wi..Katakana Letter Small N
{0x1b170, 0x1b2fb}, // Nushu Character-1b170 ..Nushu Character-1b2fb
{0x1f004, 0x1f004}, // Mahjong Tile Red Dragon ..Mahjong Tile Red Dragon
@@ -443,24 +469,24 @@ public final class WcWidth {
{0x1f680, 0x1f6c5}, // Rocket ..Left Luggage
{0x1f6cc, 0x1f6cc}, // Sleeping Accommodation ..Sleeping Accommodation
{0x1f6d0, 0x1f6d2}, // Place Of Worship ..Shopping Trolley
{0x1f6d5, 0x1f6d7}, // Hindu Temple ..(nil)
{0x1f6d5, 0x1f6d7}, // Hindu Temple ..Elevator
{0x1f6dc, 0x1f6df}, // (nil) ..Ring Buoy
{0x1f6eb, 0x1f6ec}, // Airplane Departure ..Airplane Arriving
{0x1f6f4, 0x1f6fc}, // Scooter ..(nil)
{0x1f6f4, 0x1f6fc}, // Scooter ..Roller Skate
{0x1f7e0, 0x1f7eb}, // Large Orange Circle ..Large Brown Square
{0x1f90c, 0x1f93a}, // (nil) ..Fencer
{0x1f7f0, 0x1f7f0}, // Heavy Equals Sign ..Heavy Equals Sign
{0x1f90c, 0x1f93a}, // Pinched Fingers ..Fencer
{0x1f93c, 0x1f945}, // Wrestlers ..Goal Net
{0x1f947, 0x1f978}, // First Place Medal ..(nil)
{0x1f97a, 0x1f9cb}, // Face With Pleading Eyes ..(nil)
{0x1f9cd, 0x1f9ff}, // Standing Person ..Nazar Amulet
{0x1fa70, 0x1fa74}, // Ballet Shoes ..(nil)
{0x1fa78, 0x1fa7a}, // Drop Of Blood ..Stethoscope
{0x1fa80, 0x1fa86}, // Yo-yo ..(nil)
{0x1fa90, 0x1faa8}, // Ringed Planet ..(nil)
{0x1fab0, 0x1fab6}, // (nil) ..(nil)
{0x1fac0, 0x1fac2}, // (nil) ..(nil)
{0x1fad0, 0x1fad6}, // (nil) ..(nil)
{0x1f947, 0x1f9ff}, // First Place Medal ..Nazar Amulet
{0x1fa70, 0x1fa7c}, // Ballet Shoes ..Crutch
{0x1fa80, 0x1fa88}, // Yo-yo ..(nil)
{0x1fa90, 0x1fabd}, // Ringed Planet ..(nil)
{0x1fabf, 0x1fac5}, // (nil) ..Person With Crown
{0x1face, 0x1fadb}, // (nil) ..(nil)
{0x1fae0, 0x1fae8}, // Melting Face ..(nil)
{0x1faf0, 0x1faf8}, // Hand With Index Finger A..(nil)
{0x20000, 0x2fffd}, // Cjk Unified Ideograph-20..(nil)
{0x30000, 0x3fffd}, // (nil) ..(nil)
{0x30000, 0x3fffd}, // Cjk Unified Ideograph-30..(nil)
};
@@ -512,4 +538,29 @@ public final class WcWidth {
return Character.isHighSurrogate(c) ? width(Character.toCodePoint(c, chars[index + 1])) : width(c);
}
/**
* The zero width characters count like combining characters in the `chars` array from start
* index to end index (exclusive).
*/
public static int zeroWidthCharsCount(char[] chars, int start, int end) {
if (start < 0 || start >= chars.length)
return 0;
int count = 0;
for (int i = start; i < end && i < chars.length;) {
if (Character.isHighSurrogate(chars[i])) {
if (width(Character.toCodePoint(chars[i], chars[i + 1])) <= 0) {
count++;
}
i += 2;
} else {
if (width(chars[i]) <= 0) {
count++;
}
i++;
}
}
return count;
}
}

View File

@@ -29,7 +29,9 @@ static int create_subprocess(JNIEnv* env,
char** envp,
int* pProcessId,
jint rows,
jint columns)
jint columns,
jint cell_width,
jint cell_height)
{
int ptm = open("/dev/ptmx", O_RDWR | O_CLOEXEC);
if (ptm < 0) return throw_runtime_exception(env, "Cannot open /dev/ptmx");
@@ -57,7 +59,7 @@ static int create_subprocess(JNIEnv* env,
tcsetattr(ptm, TCSANOW, &tios);
/** Set initial winsize. */
struct winsize sz = { .ws_row = (unsigned short) rows, .ws_col = (unsigned short) columns };
struct winsize sz = { .ws_row = (unsigned short) rows, .ws_col = (unsigned short) columns, .ws_xpixel = (unsigned short) (columns * cell_width), .ws_ypixel = (unsigned short) (rows * cell_height)};
ioctl(ptm, TIOCSWINSZ, &sz);
pid_t pid = fork();
@@ -121,7 +123,9 @@ JNIEXPORT jint JNICALL Java_com_termux_terminal_JNI_createSubprocess(
jobjectArray envVars,
jintArray processIdArray,
jint rows,
jint columns)
jint columns,
jint cell_width,
jint cell_height)
{
jsize size = args ? (*env)->GetArrayLength(env, args) : 0;
char** argv = NULL;
@@ -156,7 +160,7 @@ JNIEXPORT jint JNICALL Java_com_termux_terminal_JNI_createSubprocess(
int procId = 0;
char const* cmd_cwd = (*env)->GetStringUTFChars(env, cwd, NULL);
char const* cmd_utf8 = (*env)->GetStringUTFChars(env, cmd, NULL);
int ptm = create_subprocess(env, cmd_utf8, cmd_cwd, argv, envp, &procId, rows, columns);
int ptm = create_subprocess(env, cmd_utf8, cmd_cwd, argv, envp, &procId, rows, columns, cell_width, cell_height);
(*env)->ReleaseStringUTFChars(env, cmd, cmd_utf8);
(*env)->ReleaseStringUTFChars(env, cmd, cmd_cwd);
@@ -178,9 +182,9 @@ JNIEXPORT jint JNICALL Java_com_termux_terminal_JNI_createSubprocess(
return ptm;
}
JNIEXPORT void JNICALL Java_com_termux_terminal_JNI_setPtyWindowSize(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint fd, jint rows, jint cols)
JNIEXPORT void JNICALL Java_com_termux_terminal_JNI_setPtyWindowSize(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint fd, jint rows, jint cols, jint cell_width, jint cell_height)
{
struct winsize sz = { .ws_row = (unsigned short) rows, .ws_col = (unsigned short) cols };
struct winsize sz = { .ws_row = (unsigned short) rows, .ws_col = (unsigned short) cols, .ws_xpixel = (unsigned short) (cols * cell_width), .ws_ypixel = (unsigned short) (rows * cell_height) };
ioctl(fd, TIOCSWINSZ, &sz);
}

View File

@@ -0,0 +1,21 @@
package com.termux.terminal;
public class ApcTest extends TerminalTestCase {
public void testApcConsumed() {
// At time of writing this is part of what yazi sends for probing for kitty graphics protocol support:
// https://github.com/sxyazi/yazi/blob/0cdaff98d0b3723caff63eebf1974e7907a43a2c/yazi-adapter/src/emulator.rs#L129
// This should not result in anything being written to the screen: If kitty graphics protocol support
// is implemented it should instead result in an error code on stdin, and if not it should be consumed
// silently just as xterm does. See https://sw.kovidgoyal.net/kitty/graphics-protocol/.
withTerminalSized(2, 2)
.enterString("\033_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\033\\")
.assertLinesAre(" ", " ");
// It is ok for the APC content to be non printable characters:
withTerminalSized(12, 2)
.enterString("hello \033_some\023\033_\\apc#end\033\\ world")
.assertLinesAre("hello world", " ");
}
}

View File

@@ -1,5 +1,7 @@
package com.termux.terminal;
import java.util.List;
/** "\033[" is the Control Sequence Introducer char sequence (CSI). */
public class ControlSequenceIntroducerTest extends TerminalTestCase {
@@ -62,4 +64,68 @@ public class ControlSequenceIntroducerTest extends TerminalTestCase {
assertEquals("y\nz", mTerminal.getScreen().getTranscriptText());
}
public void testReportPixelSize() {
int columns = 3;
int rows = 3;
withTerminalSized(columns, rows);
int cellWidth = TerminalTest.INITIAL_CELL_WIDTH_PIXELS;
int cellHeight = TerminalTest.INITIAL_CELL_HEIGHT_PIXELS;
assertEnteringStringGivesResponse("\033[14t", "\033[4;" + (rows*cellHeight) + ";" + (columns*cellWidth) + "t");
assertEnteringStringGivesResponse("\033[16t", "\033[6;" + cellHeight + ";" + cellWidth + "t");
columns = 23;
rows = 33;
resize(columns, rows);
assertEnteringStringGivesResponse("\033[14t", "\033[4;" + (rows*cellHeight) + ";" + (columns*cellWidth) + "t");
assertEnteringStringGivesResponse("\033[16t", "\033[6;" + cellHeight + ";" + cellWidth + "t");
cellWidth = 8;
cellHeight = 18;
mTerminal.resize(columns, rows, cellWidth, cellHeight);
assertEnteringStringGivesResponse("\033[14t", "\033[4;" + (rows*cellHeight) + ";" + (columns*cellWidth) + "t");
assertEnteringStringGivesResponse("\033[16t", "\033[6;" + cellHeight + ";" + cellWidth + "t");
}
/**
* See <a href="https://sw.kovidgoyal.net/kitty/underlines/">Colored and styled underlines</a>:
*
* <pre>
* <ESC>[4:0m # no underline
* <ESC>[4:1m # straight underline
* <ESC>[4:2m # double underline
* <ESC>[4:3m # curly underline
* <ESC>[4:4m # dotted underline
* <ESC>[4:5m # dashed underline
* <ESC>[4m # straight underline (for backwards compat)
* <ESC>[24m # no underline (for backwards compat)
* </pre>
* <p>
* We currently parse the variants, but map them to normal/no underlines as appropriate
*/
public void testUnderlineVariants() {
for (String suffix : List.of("", ":1", ":2", ":3", ":4", ":5")) {
for (String stop : List.of("24", "4:0")) {
withTerminalSized(3, 3);
enterString("\033[4" + suffix + "m").assertLinesAre(" ", " ", " ");
assertEquals(TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE, mTerminal.mEffect);
enterString("\033[4;1m").assertLinesAre(" ", " ", " ");
assertEquals(TextStyle.CHARACTER_ATTRIBUTE_BOLD | TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE, mTerminal.mEffect);
enterString("\033[" + stop + "m").assertLinesAre(" ", " ", " ");
assertEquals(TextStyle.CHARACTER_ATTRIBUTE_BOLD, mTerminal.mEffect);
}
}
}
public void testManyParameters() {
StringBuilder b = new StringBuilder("\033[");
for (int i = 0; i < 30; i++) {
b.append("0;");
}
b.append("4:2");
// This clearing of underline should be ignored as the parameters pass the threshold for too many parameters:
b.append("4:0m");
withTerminalSized(3, 3)
.enterString(b.toString())
.assertLinesAre(" ", " ", " ");
assertEquals(TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE, mTerminal.mEffect);
}
}

View File

@@ -11,10 +11,10 @@ public class HistoryTest extends TerminalTestCase {
assertLinesAre("777", "888", "999");
assertHistoryStartsWith("666", "555");
mTerminal.resize(cols, 2);
resize(cols, 2);
assertHistoryStartsWith("777", "666", "555");
mTerminal.resize(cols, 3);
resize(cols, 3);
assertHistoryStartsWith("666", "555");
}

View File

@@ -72,11 +72,11 @@ public class ResizeTest extends TerminalTestCase {
enterString("\r\n");
}
assertLinesAre("998 ", "999 ", " ");
mTerminal.resize(cols, 2);
resize(cols, 2);
assertLinesAre("999 ", " ");
mTerminal.resize(cols, 5);
resize(cols, 5);
assertLinesAre("996 ", "997 ", "998 ", "999 ", " ");
mTerminal.resize(cols, rows);
resize(cols, rows);
assertLinesAre("998 ", "999 ", " ");
}

View File

@@ -75,6 +75,16 @@ public class ScrollRegionTest extends TerminalTestCase {
withTerminalSized(3, 3).enterString("\033[?69h\033[2sABC\033[?6h\033ED").assertLinesAre("ABC", " D ", " ");
}
public void testRiRespectsLeftMargin() {
// Reverse Index (RI), ${ESC}M, should respect horizontal margins:
withTerminalSized(4, 3).enterString("ABCD\033[?69h\033[2;3s\033[?6h\033M").assertLinesAre("A D", " BC ", " ");
}
public void testSdRespectsLeftMargin() {
// Scroll Down (SD), ${CSI}${N}T, should respect horizontal margins:
withTerminalSized(4, 3).enterString("ABCD\033[?69h\033[2;3s\033[?6h\033[2T").assertLinesAre("A D", " ", " BC ");
}
public void testBackwardIndex() {
// vttest "Menu 11.3.2: VT420 Cursor-Movement Test", test 7.
// Without margins:
@@ -127,4 +137,31 @@ public class ScrollRegionTest extends TerminalTestCase {
" xxx"
);
}
/**
* See <a href="https://github.com/termux/termux-packages/issues/12556">reported issue</a>.
*/
public void testClearingWhenScrollingWithMargins() {
int newForeground = 2;
int newBackground = 3;
int size = 3;
TerminalTestCase terminal = withTerminalSized(size, size)
// Enable horizontal margin and set left margin to 1:
.enterString("\033[?69h\033[2s")
// Set foreground and background color:
.enterString("\033[" + (30 + newForeground) + ";" + (40 + newBackground) + "m")
// Enter newlines to scroll down:
.enterString("\r\n\r\n\r\n\r\n\r\n");
for (int row = 0; row < size; row++) {
for (int col = 0; col < size; col++) {
// The first column (outside of the scrolling area, due to us setting a left scroll
// margin of 1) should be unmodified, the others should use the current style:
int expectedForeground = col == 0 ? TextStyle.COLOR_INDEX_FOREGROUND : newForeground;
int expectedBackground = col == 0 ? TextStyle.COLOR_INDEX_BACKGROUND : newBackground;
terminal.assertForegroundColorAt(row, col, expectedForeground);
terminal.assertBackgroundColorAt(row, col, expectedBackground);
}
}
}
}

View File

@@ -82,7 +82,7 @@ public class TerminalTest extends TerminalTestCase {
assertEnteringStringGivesResponse("\033[18t", "\033[8;5;5t");
for (int width = 3; width < 12; width++) {
for (int height = 3; height < 12; height++) {
mTerminal.resize(width, height);
resize(width, height);
assertEnteringStringGivesResponse("\033[18t", "\033[8;" + height + ";" + width + "t");
}
}
@@ -137,6 +137,11 @@ public class TerminalTest extends TerminalTestCase {
}
public void testSelectGraphics() {
selectGraphicsTestRun(';');
selectGraphicsTestRun(':');
}
public void selectGraphicsTestRun(char separator) {
withTerminalSized(5, 5);
enterString("\033[31m");
assertEquals(mTerminal.mForeColor, 1);
@@ -151,31 +156,63 @@ public class TerminalTest extends TerminalTestCase {
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
// Test CSI resetting to default if sequence starts with ; or has sequential ;;
// Check TerminalEmulator.parseArg()
enterString("\033[31m\033[m");
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
enterString("\033[31m\033[;m".replace(';', separator));
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
enterString("\033[31m\033[0m");
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
enterString("\033[31m\033[0;m".replace(';', separator));
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
enterString("\033[31;;m");
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
enterString("\033[31::m");
assertEquals(1, mTerminal.mForeColor);
enterString("\033[31;m");
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
enterString("\033[31:m");
assertEquals(1, mTerminal.mForeColor);
enterString("\033[31;;41m");
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
assertEquals(1, mTerminal.mBackColor);
enterString("\033[0m");
assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
// 256 colors:
enterString("\033[38;5;119m");
enterString("\033[38;5;119m".replace(';', separator));
assertEquals(119, mTerminal.mForeColor);
assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
enterString("\033[48;5;129m");
enterString("\033[48;5;129m".replace(';', separator));
assertEquals(119, mTerminal.mForeColor);
assertEquals(129, mTerminal.mBackColor);
// Invalid parameter:
enterString("\033[48;8;129m");
enterString("\033[48;8;129m".replace(';', separator));
assertEquals(119, mTerminal.mForeColor);
assertEquals(129, mTerminal.mBackColor);
// Multiple parameters at once:
enterString("\033[38;5;178;48;5;179;m");
enterString("\033[38;5;178".replace(';', separator) + ";" + "48;5;179m".replace(';', separator));
assertEquals(178, mTerminal.mForeColor);
assertEquals(179, mTerminal.mBackColor);
// Omitted parameter means zero:
enterString("\033[38;5;m".replace(';', separator));
assertEquals(0, mTerminal.mForeColor);
assertEquals(179, mTerminal.mBackColor);
enterString("\033[48;5;m".replace(';', separator));
assertEquals(0, mTerminal.mForeColor);
assertEquals(0, mTerminal.mBackColor);
// 24 bit colors:
enterString(("\033[0m")); // Reset fg and bg colors.
enterString("\033[38;2;255;127;2m");
enterString("\033[38;2;255;127;2m".replace(';', separator));
int expectedForeground = 0xff000000 | (255 << 16) | (127 << 8) | 2;
assertEquals(expectedForeground, mTerminal.mForeColor);
assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
enterString("\033[48;2;1;2;254m");
enterString("\033[48;2;1;2;254m".replace(';', separator));
int expectedBackground = 0xff000000 | (1 << 16) | (2 << 8) | 254;
assertEquals(expectedForeground, mTerminal.mForeColor);
assertEquals(expectedBackground, mTerminal.mBackColor);
@@ -184,14 +221,30 @@ public class TerminalTest extends TerminalTestCase {
enterString(("\033[0m")); // Reset fg and bg colors.
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
enterString("\033[38;2;255;127;2;48;2;1;2;254m");
enterString("\033[38;2;255;127;2".replace(';', separator) + ";" + "48;2;1;2;254m".replace(';', separator));
assertEquals(expectedForeground, mTerminal.mForeColor);
assertEquals(expectedBackground, mTerminal.mBackColor);
// 24 bit colors, invalid input:
enterString("\033[38;2;300;127;2;48;2;1;300;254m");
enterString("\033[38;2;300;127;2;48;2;1;300;254m".replace(';', separator));
assertEquals(expectedForeground, mTerminal.mForeColor);
assertEquals(expectedBackground, mTerminal.mBackColor);
// 24 bit colors, omitted parameter means zero:
enterString("\033[38;2;255;127;m".replace(';', separator));
expectedForeground = 0xff000000 | (255 << 16) | (127 << 8);
assertEquals(expectedForeground, mTerminal.mForeColor);
assertEquals(expectedBackground, mTerminal.mBackColor);
enterString("\033[38;2;123;;77m".replace(';', separator));
expectedForeground = 0xff000000 | (123 << 16) | 77;
assertEquals(expectedForeground, mTerminal.mForeColor);
assertEquals(expectedBackground, mTerminal.mBackColor);
// 24 bit colors, extra sub-parameters are skipped:
expectedForeground = 0xff000000 | (255 << 16) | (127 << 8) | 2;
enterString("\033[0;38:2:255:127:2:48:2:1:2:254m");
assertEquals(expectedForeground, mTerminal.mForeColor);
assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
}
public void testBackgroundColorErase() {

View File

@@ -13,7 +13,10 @@ import java.util.Set;
public abstract class TerminalTestCase extends TestCase {
public static class MockTerminalOutput extends TerminalOutput {
public static final int INITIAL_CELL_WIDTH_PIXELS = 13;
public static final int INITIAL_CELL_HEIGHT_PIXELS = 15;
public static class MockTerminalOutput extends TerminalOutput {
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
public final List<ChangedTitle> titleChanges = new ArrayList<>();
public final List<String> clipboardPuts = new ArrayList<>();
@@ -108,7 +111,7 @@ public abstract class TerminalTestCase extends TestCase {
protected TerminalTestCase withTerminalSized(int columns, int rows) {
// The tests aren't currently using the client, so a null client will suffice, a dummy client should be implemented if needed
mTerminal = new TerminalEmulator(mOutput, columns, rows, rows * 2, null);
mTerminal = new TerminalEmulator(mOutput, columns, rows, INITIAL_CELL_WIDTH_PIXELS, INITIAL_CELL_HEIGHT_PIXELS, rows * 2, null);
return this;
}
@@ -201,7 +204,7 @@ public abstract class TerminalTestCase extends TestCase {
}
public TerminalTestCase resize(int cols, int rows) {
mTerminal.resize(cols, rows);
mTerminal.resize(cols, rows, INITIAL_CELL_WIDTH_PIXELS, INITIAL_CELL_HEIGHT_PIXELS);
assertInvariants();
return this;
}
@@ -301,6 +304,11 @@ public abstract class TerminalTestCase extends TestCase {
assertEquals(color, TextStyle.decodeForeColor(style));
}
public void assertBackgroundColorAt(int externalRow, int column, int color) {
long style = mTerminal.getScreen().mLines[mTerminal.getScreen().externalToInternalRow(externalRow)].getStyle(column);
assertEquals(color, TextStyle.decodeBackColor(style));
}
public TerminalTestCase assertColor(int colorIndex, int expected) {
int actual = mTerminal.mColors.mCurrentColors[colorIndex];
if (expected != actual) {

View File

@@ -233,7 +233,7 @@ public final class TerminalRenderer {
mTextPaint.setColor(foreColor);
// The text alignment is the default Paint.Align.LEFT.
canvas.drawText(text, startCharIndex, runWidthChars, left, y - mFontLineSpacingAndAscent, mTextPaint);
canvas.drawTextRun(text, startCharIndex, runWidthChars, startCharIndex, runWidthChars, left, y - mFontLineSpacingAndAscent, false, mTextPaint);
}
if (savedMatrix) canvas.restore();

View File

@@ -2,6 +2,7 @@ package com.termux.view;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
@@ -10,6 +11,7 @@ import android.graphics.Typeface;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.text.Editable;
import android.text.InputType;
import android.text.TextUtils;
@@ -19,17 +21,20 @@ import android.view.HapticFeedbackConstants;
import android.view.InputDevice;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewTreeObserver;
import android.view.accessibility.AccessibilityManager;
import android.view.autofill.AutofillManager;
import android.view.autofill.AutofillValue;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.widget.Scroller;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.termux.terminal.KeyHandler;
@@ -81,6 +86,43 @@ public final class TerminalView extends View {
/** If non-zero, this is the last unicode code point received if that was a combining character. */
int mCombiningAccent;
/**
* The current AutoFill type returned for {@link View#getAutofillType()} by {@link #getAutofillType()}.
*
* The default is {@link #AUTOFILL_TYPE_NONE} so that AutoFill UI, like toolbar above keyboard
* is not shown automatically, like on Activity starts/View create. This value should be updated
* to required value, like {@link #AUTOFILL_TYPE_TEXT} before calling
* {@link AutofillManager#requestAutofill(View)} so that AutoFill UI shows. The updated value
* set will automatically be restored to {@link #AUTOFILL_TYPE_NONE} in
* {@link #autofill(AutofillValue)} so that AutoFill UI isn't shown anymore by calling
* {@link #resetAutoFill()}.
*/
@RequiresApi(api = Build.VERSION_CODES.O)
private int mAutoFillType = AUTOFILL_TYPE_NONE;
/**
* The current AutoFill type returned for {@link View#getImportantForAutofill()} by
* {@link #getImportantForAutofill()}.
*
* The default is {@link #IMPORTANT_FOR_AUTOFILL_NO} so that view is not considered important
* for AutoFill. This value should be updated to required value, like
* {@link #IMPORTANT_FOR_AUTOFILL_YES} before calling {@link AutofillManager#requestAutofill(View)}
* so that Android and apps consider the view as important for AutoFill to process the request.
* The updated value set will automatically be restored to {@link #IMPORTANT_FOR_AUTOFILL_NO} in
* {@link #autofill(AutofillValue)} by calling {@link #resetAutoFill()}.
*/
@RequiresApi(api = Build.VERSION_CODES.O)
private int mAutoFillImportance = IMPORTANT_FOR_AUTOFILL_NO;
/**
* The current AutoFill hints returned for {@link View#getAutofillHints()} ()} by {@link #getAutofillHints()} ()}.
*
* The default is an empty `string[]`. This value should be updated to required value. The
* updated value set will automatically be restored an empty `string[]` in
* {@link #autofill(AutofillValue)} by calling {@link #resetAutoFill()}.
*/
private String[] mAutoFillHints = new String[0];
private final boolean mAccessibilityEnabled;
private static final String LOG_TAG = "TerminalView";
@@ -440,6 +482,14 @@ public final class TerminalView extends View {
if (mAccessibilityEnabled) setContentDescription(getText());
}
/** This must be called by the hosting activity in {@link Activity#onContextMenuClosed(Menu)}
* when context menu for the {@link TerminalView} is started by
* {@link TextSelectionCursorController#ACTION_MORE} is closed. */
public void onContextMenuClosed(Menu menu) {
// Unset the stored text since it shouldn't be used anymore and should be cleared from memory
unsetStoredSelectedText();
}
/**
* Sets the text size, which in turn sets the number of rows and columns.
*
@@ -550,11 +600,14 @@ public final class TerminalView extends View {
if (action == MotionEvent.ACTION_DOWN) showContextMenu();
return true;
} else if (event.isButtonPressed(MotionEvent.BUTTON_TERTIARY)) {
ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clipData = clipboard.getPrimaryClip();
ClipboardManager clipboardManager = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clipData = clipboardManager.getPrimaryClip();
if (clipData != null) {
CharSequence paste = clipData.getItemAt(0).coerceToText(getContext());
if (!TextUtils.isEmpty(paste)) mEmulator.paste(paste.toString());
ClipData.Item clipItem = clipData.getItemAt(0);
if (clipItem != null) {
CharSequence text = clipItem.coerceToText(getContext());
if (!TextUtils.isEmpty(text)) mEmulator.paste(text.toString());
}
}
} else if (mEmulator.isMouseTrackingActive()) { // BUTTON_PRIMARY.
switch (event.getAction()) {
@@ -578,6 +631,7 @@ public final class TerminalView extends View {
if (TERMINAL_VIEW_KEY_LOGGING_ENABLED)
mClient.logInfo(LOG_TAG, "onKeyPreIme(keyCode=" + keyCode + ", event=" + event + ")");
if (keyCode == KeyEvent.KEYCODE_BACK) {
cancelRequestAutoFill();
if (isSelectingText()) {
stopTextSelectionMode();
return true;
@@ -839,6 +893,9 @@ public final class TerminalView extends View {
if (mEmulator != null)
mEmulator.setCursorBlinkState(true);
if (handleKeyCodeAction(keyCode, keyMod))
return true;
TerminalEmulator term = mTermSession.getEmulator();
String code = KeyHandler.getCode(keyCode, keyMod, term.isCursorKeysApplicationMode(), term.isKeypadApplicationMode());
if (code == null) return false;
@@ -846,6 +903,26 @@ public final class TerminalView extends View {
return true;
}
public boolean handleKeyCodeAction(int keyCode, int keyMod) {
boolean shiftDown = (keyMod & KeyHandler.KEYMOD_SHIFT) != 0;
switch (keyCode) {
case KeyEvent.KEYCODE_PAGE_UP:
case KeyEvent.KEYCODE_PAGE_DOWN:
// shift+page_up and shift+page_down should scroll scrollback history instead of
// scrolling command history or changing pages
if (shiftDown) {
long time = SystemClock.uptimeMillis();
MotionEvent motionEvent = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0, 0, 0);
doScroll(motionEvent, keyCode == KeyEvent.KEYCODE_PAGE_UP ? -mEmulator.mRows : mEmulator.mRows);
motionEvent.recycle();
return true;
}
}
return false;
}
/**
* Called when a key is released in the view.
*
@@ -893,7 +970,7 @@ public final class TerminalView extends View {
int newRows = Math.max(4, (viewHeight - mRenderer.mFontLineSpacingAndAscent) / mRenderer.mFontLineSpacing);
if (mEmulator == null || (newColumns != mEmulator.mColumns || newRows != mEmulator.mRows)) {
mTermSession.updateSize(newColumns, newRows);
mTermSession.updateSize(newColumns, newRows, (int) mRenderer.getFontWidth(), mRenderer.getFontLineSpacing());
mEmulator = mTermSession.getEmulator();
mClient.onEmulatorSet();
@@ -971,12 +1048,20 @@ public final class TerminalView extends View {
if (value.isText()) {
mTermSession.write(value.getTextValue().toString());
}
resetAutoFill();
}
@RequiresApi(api = Build.VERSION_CODES.O)
@Override
public int getAutofillType() {
return AUTOFILL_TYPE_TEXT;
return mAutoFillType;
}
@RequiresApi(api = Build.VERSION_CODES.O)
@Override
public String[] getAutofillHints() {
return mAutoFillHints;
}
@RequiresApi(api = Build.VERSION_CODES.O)
@@ -985,6 +1070,95 @@ public final class TerminalView extends View {
return AutofillValue.forText("");
}
@RequiresApi(api = Build.VERSION_CODES.O)
@Override
public int getImportantForAutofill() {
return mAutoFillImportance;
}
@RequiresApi(api = Build.VERSION_CODES.O)
private synchronized void resetAutoFill() {
// Restore none type so that AutoFill UI isn't shown anymore.
mAutoFillType = AUTOFILL_TYPE_NONE;
mAutoFillImportance = IMPORTANT_FOR_AUTOFILL_NO;
mAutoFillHints = new String[0];
}
public AutofillManager getAutoFillManagerService() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return null;
try {
Context context = getContext();
if (context == null) return null;
return context.getSystemService(AutofillManager.class);
} catch (Exception e) {
mClient.logStackTraceWithMessage(LOG_TAG, "Failed to get AutofillManager service", e);
return null;
}
}
public boolean isAutoFillEnabled() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return false;
try {
AutofillManager autofillManager = getAutoFillManagerService();
return autofillManager != null && autofillManager.isEnabled();
} catch (Exception e) {
mClient.logStackTraceWithMessage(LOG_TAG, "Failed to check if Autofill is enabled", e);
return false;
}
}
public synchronized void requestAutoFillUsername() {
requestAutoFill(
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? new String[]{View.AUTOFILL_HINT_USERNAME} :
null);
}
public synchronized void requestAutoFillPassword() {
requestAutoFill(
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? new String[]{View.AUTOFILL_HINT_PASSWORD} :
null);
}
public synchronized void requestAutoFill(String[] autoFillHints) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
if (autoFillHints == null || autoFillHints.length < 1) return;
try {
AutofillManager autofillManager = getAutoFillManagerService();
if (autofillManager != null && autofillManager.isEnabled()) {
// Update type that will be returned by `getAutofillType()` so that AutoFill UI is shown.
mAutoFillType = AUTOFILL_TYPE_TEXT;
// Update importance that will be returned by `getImportantForAutofill()` so that
// AutoFill considers the view as important.
mAutoFillImportance = IMPORTANT_FOR_AUTOFILL_YES;
// Update hints that will be returned by `getAutofillHints()` for which to show AutoFill UI.
mAutoFillHints = autoFillHints;
autofillManager.requestAutofill(this);
}
} catch (Exception e) {
mClient.logStackTraceWithMessage(LOG_TAG, "Failed to request Autofill", e);
}
}
public synchronized void cancelRequestAutoFill() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
if (mAutoFillType == AUTOFILL_TYPE_NONE) return;
try {
AutofillManager autofillManager = getAutoFillManagerService();
if (autofillManager != null && autofillManager.isEnabled()) {
resetAutoFill();
autofillManager.cancel();
}
} catch (Exception e) {
mClient.logStackTraceWithMessage(LOG_TAG, "Failed to cancel Autofill request", e);
}
}
/**
@@ -1184,6 +1358,25 @@ public final class TerminalView extends View {
}
}
/** Get the currently selected text if selecting. */
public String getSelectedText() {
if (isSelectingText() && mTextSelectionCursorController != null)
return mTextSelectionCursorController.getSelectedText();
else
return null;
}
/** Get the selected text stored before "MORE" button was pressed on the context menu. */
@Nullable
public String getStoredSelectedText() {
return mTextSelectionCursorController != null ? mTextSelectionCursorController.getStoredSelectedText() : null;
}
/** Unset the selected text stored before "MORE" button was pressed on the context menu. */
public void unsetStoredSelectedText() {
if (mTextSelectionCursorController != null) mTextSelectionCursorController.unsetStoredSelectedText();
}
private ActionMode getTextSelectionActionMode() {
if (mTextSelectionCursorController != null) {
return mTextSelectionCursorController.getActionMode();

View File

@@ -12,6 +12,8 @@ import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.Nullable;
import com.termux.terminal.TerminalBuffer;
import com.termux.terminal.WcWidth;
import com.termux.view.R;
@@ -21,6 +23,7 @@ public class TextSelectionCursorController implements CursorController {
private final TerminalView terminalView;
private final TextSelectionHandleView mStartHandle, mEndHandle;
private String mStoredSelectedText;
private boolean mIsSelectingText = false;
private long mShowStartTime = System.currentTimeMillis();
@@ -28,9 +31,9 @@ public class TextSelectionCursorController implements CursorController {
private int mSelX1 = -1, mSelX2 = -1, mSelY1 = -1, mSelY2 = -1;
private ActionMode mActionMode;
private final int ACTION_COPY = 1;
private final int ACTION_PASTE = 2;
private final int ACTION_MORE = 3;
public final int ACTION_COPY = 1;
public final int ACTION_PASTE = 2;
public final int ACTION_MORE = 3;
public TextSelectionCursorController(TerminalView terminalView) {
this.terminalView = terminalView;
@@ -113,7 +116,7 @@ public class TextSelectionCursorController implements CursorController {
ClipboardManager clipboard = (ClipboardManager) terminalView.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
menu.add(Menu.NONE, ACTION_COPY, Menu.NONE, R.string.copy_text).setShowAsAction(show);
menu.add(Menu.NONE, ACTION_PASTE, Menu.NONE, R.string.paste_text).setEnabled(clipboard.hasPrimaryClip()).setShowAsAction(show);
menu.add(Menu.NONE, ACTION_PASTE, Menu.NONE, R.string.paste_text).setEnabled(clipboard != null && clipboard.hasPrimaryClip()).setShowAsAction(show);
menu.add(Menu.NONE, ACTION_MORE, Menu.NONE, R.string.text_selection_more);
return true;
}
@@ -132,7 +135,7 @@ public class TextSelectionCursorController implements CursorController {
switch (item.getItemId()) {
case ACTION_COPY:
String selectedText = terminalView.mEmulator.getSelectedText(mSelX1, mSelY1, mSelX2, mSelY2).trim();
String selectedText = getSelectedText();
terminalView.mTermSession.onCopyTextToClipboard(selectedText);
terminalView.stopTextSelectionMode();
break;
@@ -141,7 +144,13 @@ public class TextSelectionCursorController implements CursorController {
terminalView.mTermSession.onPasteTextFromClipboard();
break;
case ACTION_MORE:
terminalView.stopTextSelectionMode(); //we stop text selection first, otherwise handles will show above popup
// We first store the selected text in case TerminalViewClient needs the
// selected text before MORE button was pressed since we are going to
// stop selection mode
mStoredSelectedText = getSelectedText();
// The text selection needs to be stopped before showing context menu,
// otherwise handles will show above popup
terminalView.stopTextSelectionMode();
terminalView.showContextMenu();
break;
}
@@ -183,14 +192,19 @@ public class TextSelectionCursorController implements CursorController {
int y1 = Math.round((mSelY1 - 1 - terminalView.getTopRow()) * terminalView.mRenderer.getFontLineSpacing());
int y2 = Math.round((mSelY2 + 1 - terminalView.getTopRow()) * terminalView.mRenderer.getFontLineSpacing());
if (x1 > x2) {
int tmp = x1;
x1 = x2;
x2 = tmp;
}
outRect.set(x1, y1 + mHandleHeight, x2, y2 + mHandleHeight);
int terminalBottom = terminalView.getBottom();
int top = y1 + mHandleHeight;
int bottom = y2 + mHandleHeight;
if (top > terminalBottom) top = terminalBottom;
if (bottom > terminalBottom) bottom = terminalBottom;
outRect.set(x1, top, x2, bottom);
}
}, ActionMode.TYPE_FLOATING);
}
@@ -351,6 +365,22 @@ public class TextSelectionCursorController implements CursorController {
sel[3] = mSelX2;
}
/** Get the currently selected text. */
public String getSelectedText() {
return terminalView.mEmulator.getSelectedText(mSelX1, mSelY1, mSelX2, mSelY2);
}
/** Get the selected text stored before "MORE" button was pressed on the context menu. */
@Nullable
public String getStoredSelectedText() {
return mStoredSelectedText;
}
/** Unset the selected text stored before "MORE" button was pressed on the context menu. */
public void unsetStoredSelectedText() {
mStoredSelectedText = null;
}
public ActionMode getActionMode() {
return mActionMode;
}

View File

@@ -14,6 +14,7 @@ android {
implementation "io.noties.markwon:ext-strikethrough:$markwonVersion"
implementation "io.noties.markwon:linkify:$markwonVersion"
implementation "io.noties.markwon:recycler:$markwonVersion"
implementation "org.lsposed.hiddenapibypass:hiddenapibypass:6.1"
// Do not increment version higher than 1.0.0-alpha09 since it will break ViewUtils and needs to be looked into
// noinspection GradleDependency

View File

@@ -0,0 +1,96 @@
package com.termux.shared.android;
import android.annotation.SuppressLint;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.termux.shared.logger.Logger;
import com.termux.shared.reflection.ReflectionUtils;
import java.lang.reflect.Method;
public class SELinuxUtils {
public static final String ANDROID_OS_SELINUX_CLASS = "android.os.SELinux";
private static final String LOG_TAG = "SELinuxUtils";
/**
* Gets the security context of the current process.
*
* @return Returns a {@link String} representing the security context of the current process.
* This will be {@code null} if an exception is raised.
*/
@Nullable
public static String getContext() {
ReflectionUtils.bypassHiddenAPIReflectionRestrictions();
String methodName = "getContext";
try {
@SuppressLint("PrivateApi") Class<?> clazz = Class.forName(ANDROID_OS_SELINUX_CLASS);
Method method = ReflectionUtils.getDeclaredMethod(clazz, methodName);
if (method == null) {
Logger.logError(LOG_TAG, "Failed to get " + methodName + "() method of " + ANDROID_OS_SELINUX_CLASS + " class");
return null;
}
return (String) ReflectionUtils.invokeMethod(method, null).value;
} catch (Exception e) {
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to call " + methodName + "() method of " + ANDROID_OS_SELINUX_CLASS + " class", e);
return null;
}
}
/**
* Get the security context of a given process id.
*
* @param pid The pid of process.
* @return Returns a {@link String} representing the security context of the given pid.
* This will be {@code null} if an exception is raised.
*/
@Nullable
public static String getPidContext(int pid) {
ReflectionUtils.bypassHiddenAPIReflectionRestrictions();
String methodName = "getPidContext";
try {
@SuppressLint("PrivateApi") Class<?> clazz = Class.forName(ANDROID_OS_SELINUX_CLASS);
Method method = ReflectionUtils.getDeclaredMethod(clazz, methodName, int.class);
if (method == null) {
Logger.logError(LOG_TAG, "Failed to get " + methodName + "() method of " + ANDROID_OS_SELINUX_CLASS + " class");
return null;
}
return (String) ReflectionUtils.invokeMethod(method, null, pid).value;
} catch (Exception e) {
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to call " + methodName + "() method of " + ANDROID_OS_SELINUX_CLASS + " class", e);
return null;
}
}
/**
* Get the security context of a file object.
*
* @param path The pathname of the file object.
* @return Returns a {@link String} representing the security context of the file.
* This will be {@code null} if an exception is raised.
*/
@Nullable
public static String getFileContext(@NonNull String path) {
ReflectionUtils.bypassHiddenAPIReflectionRestrictions();
String methodName = "getFileContext";
try {
@SuppressLint("PrivateApi") Class<?> clazz = Class.forName(ANDROID_OS_SELINUX_CLASS);
Method method = ReflectionUtils.getDeclaredMethod(clazz, methodName, String.class);
if (method == null) {
Logger.logError(LOG_TAG, "Failed to get " + methodName + "() method of " + ANDROID_OS_SELINUX_CLASS + " class");
return null;
}
return (String) ReflectionUtils.invokeMethod(method, null, path).value;
} catch (Exception e) {
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to call " + methodName + "() method of " + ANDROID_OS_SELINUX_CLASS + " class", e);
return null;
}
}
}

View File

@@ -4,11 +4,14 @@ import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Color;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import com.termux.shared.R;
import com.termux.shared.logger.Logger;
public class MessageDialogUtils {
@@ -74,7 +77,19 @@ public class MessageDialogUtils {
if (onDismiss != null)
builder.setOnDismissListener(onDismiss);
builder.show();
AlertDialog dialog = builder.create();
dialog.setOnShowListener(dialogInterface -> {
Logger.logError("dialog");
Button button = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
if (button != null)
button.setTextColor(Color.BLACK);
button = dialog.getButton(AlertDialog.BUTTON_NEGATIVE);
if (button != null)
button.setTextColor(Color.BLACK);
});
dialog.show();
}
public static void exitAppWithErrorMessage(Context context, String titleText, String messageText) {

View File

@@ -11,7 +11,6 @@ import android.net.Uri;
import android.os.Environment;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import com.termux.shared.R;
import com.termux.shared.data.DataUtils;
@@ -23,6 +22,8 @@ import com.termux.shared.packages.PermissionUtils;
import java.nio.charset.Charset;
import javax.annotation.Nullable;
public class ShareUtils {
private static final String LOG_TAG = "ShareUtils";
@@ -56,6 +57,18 @@ public class ShareUtils {
* @param text The text to share.
*/
public static void shareText(final Context context, final String subject, final String text) {
shareText(context, subject, text, null);
}
/**
* Share text.
*
* @param context The context for operations.
* @param subject The subject for sharing.
* @param text The text to share.
* @param title The title for share menu.
*/
public static void shareText(final Context context, final String subject, final String text, @Nullable final String title) {
if (context == null || text == null) return;
final Intent shareTextIntent = new Intent(Intent.ACTION_SEND);
@@ -63,29 +76,85 @@ public class ShareUtils {
shareTextIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
shareTextIntent.putExtra(Intent.EXTRA_TEXT, DataUtils.getTruncatedCommandOutput(text, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, true, false, false));
openSystemAppChooser(context, shareTextIntent, context.getString(R.string.title_share_with));
openSystemAppChooser(context, shareTextIntent, DataUtils.isNullOrEmpty(title) ? context.getString(R.string.title_share_with) : title);
}
/** Wrapper for {@link #copyTextToClipboard(Context, String, String, String)} with `null` `clipDataLabel` and `toastString`. */
public static void copyTextToClipboard(Context context, final String text) {
copyTextToClipboard(context, null, text, null);
}
/** Wrapper for {@link #copyTextToClipboard(Context, String, String, String)} with `null` `clipDataLabel`. */
public static void copyTextToClipboard(Context context, final String text, final String toastString) {
copyTextToClipboard(context, null, text, toastString);
}
/**
* Copy the text to clipboard.
* Copy the text to primary clip of the clipboard.
*
* @param context The context for operations.
* @param clipDataLabel The label to show to the user describing the copied text.
* @param text The text to copy.
* @param toastString If this is not {@code null} or empty, then a toast is shown if copying to
* clipboard is successful.
*/
public static void copyTextToClipboard(final Context context, final String text, final String toastString) {
public static void copyTextToClipboard(Context context, @Nullable final String clipDataLabel,
final String text, final String toastString) {
if (context == null || text == null) return;
final ClipboardManager clipboardManager = ContextCompat.getSystemService(context, ClipboardManager.class);
ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
if (clipboardManager == null) return;
if (clipboardManager != null) {
clipboardManager.setPrimaryClip(ClipData.newPlainText(null, DataUtils.getTruncatedCommandOutput(text, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, true, false, false)));
if (toastString != null && !toastString.isEmpty())
Logger.showToast(context, toastString, true);
}
clipboardManager.setPrimaryClip(ClipData.newPlainText(clipDataLabel,
DataUtils.getTruncatedCommandOutput(text, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES,
true, false, false)));
if (toastString != null && !toastString.isEmpty())
Logger.showToast(context, toastString, true);
}
/**
* Wrapper for {@link #getTextFromClipboard(Context, boolean)} that returns primary text {@link String}
* if its set and not empty.
*/
@Nullable
public static String getTextStringFromClipboardIfSet(Context context, boolean coerceToText) {
CharSequence textCharSequence = getTextFromClipboard(context, coerceToText);
if (textCharSequence == null) return null;
String textString = textCharSequence.toString();
return !textString.isEmpty() ? textString : null;
}
/**
* Get the text from primary clip of the clipboard.
*
* @param context The context for operations.
* @param coerceToText Whether to call {@link ClipData.Item#coerceToText(Context)} to coerce
* non-text data to text.
* @return Returns the {@link CharSequence} of primary text. This will be `null` if failed to get it.
*/
@Nullable
public static CharSequence getTextFromClipboard(Context context, boolean coerceToText) {
if (context == null) return null;
ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
if (clipboardManager == null) return null;
ClipData clipData = clipboardManager.getPrimaryClip();
if (clipData == null) return null;
ClipData.Item clipItem = clipData.getItemAt(0);
if (clipItem == null) return null;
return coerceToText ? clipItem.coerceToText(context) : clipItem.getText();
}
/**
* Open a url.
*

View File

@@ -7,6 +7,7 @@ import android.util.Log;
import android.widget.Toast;
import com.termux.shared.R;
import com.termux.shared.data.DataUtils;
import com.termux.shared.termux.TermuxConstants;
import java.io.IOException;
@@ -363,7 +364,7 @@ public class Logger {
public static void showToast(final Context context, final String toastText, boolean longDuration) {
if (context == null) return;
if (context == null || DataUtils.isNullOrEmpty(toastText)) return;
new Handler(Looper.getMainLooper()).post(() -> Toast.makeText(context, toastText, longDuration ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT).show());
}

View File

@@ -191,9 +191,8 @@ public class MarkdownUtils {
}
public static Spanned getSpannedMarkdownText(Context context, String string) {
if (context == null || string == null) return null;
final Markwon markwon = getSpannedMarkwonBuilder(context);
return markwon.toMarkdown(string);
}

View File

@@ -1,5 +1,7 @@
package com.termux.shared.models;
import androidx.annotation.Keep;
import com.termux.shared.markdown.MarkdownUtils;
import com.termux.shared.termux.AndroidUtils;
@@ -10,6 +12,25 @@ import java.io.Serializable;
*/
public class ReportInfo implements Serializable {
/**
* Explicitly define `serialVersionUID` to prevent exceptions on deserialization.
*
* Like when calling `Bundle.getSerializable()` on Android.
* `android.os.BadParcelableException: Parcelable encountered IOException reading a Serializable object` (name = <class_name>)
* `java.io.InvalidClassException: <class_name>; local class incompatible`
*
* The `@Keep` annotation is necessary to prevent the field from being removed by proguard when
* app is compiled, even if its kept during library compilation.
*
* **See Also:**
* - https://docs.oracle.com/javase/8/docs/platform/serialization/spec/version.html#a6678
* - https://docs.oracle.com/javase/8/docs/platform/serialization/spec/class.html#a4100
*/
@Keep
private static final long serialVersionUID = 1L;
/** The user action that was being processed for which the report was generated. */
public final String userAction;
/** The internal app component that sent the report. */

View File

@@ -3,6 +3,7 @@ package com.termux.shared.models;
import android.graphics.Color;
import android.graphics.Typeface;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import com.termux.shared.activities.TextIOActivity;
@@ -19,6 +20,25 @@ import java.io.Serializable;
*/
public class TextIOInfo implements Serializable {
/**
* Explicitly define `serialVersionUID` to prevent exceptions on deserialization.
*
* Like when calling `Bundle.getSerializable()` on Android.
* `android.os.BadParcelableException: Parcelable encountered IOException reading a Serializable object` (name = <class_name>)
* `java.io.InvalidClassException: <class_name>; local class incompatible`
*
* The `@Keep` annotation is necessary to prevent the field from being removed by proguard when
* app is compiled, even if its kept during library compilation.
*
* **See Also:**
* - https://docs.oracle.com/javase/8/docs/platform/serialization/spec/version.html#a6678
* - https://docs.oracle.com/javase/8/docs/platform/serialization/spec/class.html#a4100
*/
@Keep
private static final long serialVersionUID = 1L;
public static final int GENERAL_DATA_SIZE_LIMIT_IN_BYTES = 1000;
public static final int LABEL_SIZE_LIMIT_IN_BYTES = 4000;
public static final int TEXT_SIZE_LIMIT_IN_BYTES = 100000 - GENERAL_DATA_SIZE_LIMIT_IN_BYTES - LABEL_SIZE_LIMIT_IN_BYTES; // < 100KB

View File

@@ -8,6 +8,7 @@ import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.UserManager;
import androidx.annotation.NonNull;
@@ -17,8 +18,10 @@ import com.termux.shared.R;
import com.termux.shared.data.DataUtils;
import com.termux.shared.interact.MessageDialogUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.reflection.ReflectionUtils;
import com.termux.shared.termux.TermuxConstants;
import java.lang.reflect.Field;
import java.security.MessageDigest;
import java.util.List;
@@ -94,6 +97,55 @@ public class PackageUtils {
}
}
/**
* Get the {@code seInfo} {@link Field} of the {@link ApplicationInfo} class.
*
* String retrieved from the seinfo tag found in selinux policy. This value can be set through
* the mac_permissions.xml policy construct. This value is used for setting an SELinux security
* context on the process as well as its data directory.
*
* https://cs.android.com/android/platform/superproject/+/android-7.1.0_r1:frameworks/base/core/java/android/content/pm/ApplicationInfo.java;l=609
* https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/core/java/android/content/pm/ApplicationInfo.java;l=981
* https://cs.android.com/android/platform/superproject/+/android-7.0.0_r1:frameworks/base/services/core/java/com/android/server/pm/SELinuxMMAC.java;l=282
* https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/services/core/java/com/android/server/pm/SELinuxMMAC.java;l=375
* https://cs.android.com/android/_/android/platform/frameworks/base/+/be0b8896d1bc385d4c8fb54c21929745935dcbea
*
* @param applicationInfo The {@link ApplicationInfo} for the package.
* @return Returns the selinux info or {@code null} if an exception was raised.
*/
@Nullable
public static String getApplicationInfoSeInfoForPackage(@NonNull final ApplicationInfo applicationInfo) {
ReflectionUtils.bypassHiddenAPIReflectionRestrictions();
try {
return (String) ReflectionUtils.invokeField(ApplicationInfo.class, Build.VERSION.SDK_INT < Build.VERSION_CODES.O ? "seinfo" : "seInfo", applicationInfo).value;
} catch (Exception e) {
// ClassCastException may be thrown
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get seInfo field value for ApplicationInfo class", e);
return null;
}
}
/**
* Get the {@code seInfoUser} {@link Field} of the {@link ApplicationInfo} class.
*
* Also check {@link #getApplicationInfoSeInfoForPackage(ApplicationInfo)}.
*
* @param applicationInfo The {@link ApplicationInfo} for the package.
* @return Returns the selinux info user or {@code null} if an exception was raised.
*/
@Nullable
public static String getApplicationInfoSeInfoUserForPackage(@NonNull final ApplicationInfo applicationInfo) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return null;
ReflectionUtils.bypassHiddenAPIReflectionRestrictions();
try {
return (String) ReflectionUtils.invokeField(ApplicationInfo.class, "seInfoUser", applicationInfo).value;
} catch (Exception e) {
// ClassCastException may be thrown
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get seInfoUser field value for ApplicationInfo class", e);
return null;
}
}
/**
* Get the app name for the package associated with the {@code context}.
*

View File

@@ -0,0 +1,275 @@
package com.termux.shared.reflection;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.termux.shared.logger.Logger;
import org.lsposed.hiddenapibypass.HiddenApiBypass;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
public class ReflectionUtils {
private static boolean HIDDEN_API_REFLECTION_RESTRICTIONS_BYPASSED = Build.VERSION.SDK_INT < Build.VERSION_CODES.P;
private static final String LOG_TAG = "ReflectionUtils";
/**
* Bypass android hidden API reflection restrictions.
* https://github.com/LSPosed/AndroidHiddenApiBypass
* https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces
*/
public static void bypassHiddenAPIReflectionRestrictions() {
if (!HIDDEN_API_REFLECTION_RESTRICTIONS_BYPASSED && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
Logger.logDebug(LOG_TAG, "Bypassing android hidden api reflection restrictions");
HiddenApiBypass.addHiddenApiExemptions("");
HIDDEN_API_REFLECTION_RESTRICTIONS_BYPASSED = true;
}
}
/** Check if android hidden API reflection restrictions are bypassed. */
public static boolean areHiddenAPIReflectionRestrictionsBypassed() {
return HIDDEN_API_REFLECTION_RESTRICTIONS_BYPASSED;
}
/**
* Get a {@link Field} for the specified class.
*
* @param clazz The {@link Class} for which to return the field.
* @param fieldName The name of the {@link Field}.
* @return Returns the {@link Field} if getting the it was successful, otherwise {@code null}.
*/
@Nullable
public static Field getDeclaredField(@NonNull Class<?> clazz, @NonNull String fieldName) {
try {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
return field;
} catch (Exception e) {
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get \"" + fieldName + "\" field for \"" + clazz.getName() + "\" class", e);
return null;
}
}
/** Class that represents result of invoking a field. */
public static class FieldInvokeResult {
public boolean success;
public Object value;
FieldInvokeResult(boolean success, Object value) {
this.value = success;
this.value = value;
}
}
/**
* Get a value for a {@link Field} of an object for the specified class.
*
* @param clazz The {@link Class} to which the object belongs to.
* @param fieldName The name of the {@link Field}.
* @param object The {@link Object} instance from which to get the field value.
* @return Returns the {@link FieldInvokeResult} of invoking the field. The
* {@link FieldInvokeResult#success} will be {@code true} if invoking the field was successful,
* otherwise {@code false}. The {@link FieldInvokeResult#value} will contain the field
* {@link Object} value.
*/
@NonNull
public static <T> FieldInvokeResult invokeField(@NonNull Class<T> clazz, @NonNull String fieldName, T object) {
try {
Field field = getDeclaredField(clazz, fieldName);
if (field == null) return new FieldInvokeResult(false, null);
return new FieldInvokeResult(true, field.get(object));
} catch (Exception e) {
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get \"" + fieldName + "\" field value for \"" + clazz.getName() + "\" class", e);
return new FieldInvokeResult(false, null);
}
}
/**
* Wrapper for {@link #getDeclaredMethod(Class, String, Class[])} without parameters.
*/
@Nullable
public static Method getDeclaredMethod(@NonNull Class<?> clazz, @NonNull String methodName) {
return getDeclaredMethod(clazz, methodName, new Class<?>[0]);
}
/**
* Get a {@link Method} for the specified class with the specified parameters.
*
* @param clazz The {@link Class} for which to return the method.
* @param methodName The name of the {@link Method}.
* @param parameterTypes The parameter types of the method.
* @return Returns the {@link Method} if getting the it was successful, otherwise {@code null}.
*/
@Nullable
public static Method getDeclaredMethod(@NonNull Class<?> clazz, @NonNull String methodName, Class<?>... parameterTypes) {
try {
Method method = clazz.getDeclaredMethod(methodName, parameterTypes);
method.setAccessible(true);
return method;
} catch (Exception e) {
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get \"" + methodName + "\" method for \"" + clazz.getName() + "\" class with parameter types: " + Arrays.toString(parameterTypes), e);
return null;
}
}
/**
* Wrapper for {@link #invokeVoidMethod(Method, Object, Object...)} without arguments.
*/
public static boolean invokeVoidMethod(@NonNull Method method, Object obj) {
return invokeVoidMethod(method, obj, new Object[0]);
}
/**
* Invoke a {@link Method} on the specified object with the specified arguments that returns
* {@code void}.
*
* @param method The {@link Method} to invoke.
* @param obj The {@link Object} the method should be invoked from.
* @param args The arguments to pass to the method.
* @return Returns {@code true} if invoking the method was successful, otherwise {@code false}.
*/
public static boolean invokeVoidMethod(@NonNull Method method, Object obj, Object... args) {
try {
method.setAccessible(true);
method.invoke(obj, args);
return true;
} catch (Exception e) {
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to invoke \"" + method.getName() + "\" method with object \"" + obj + "\" and args: " + Arrays.toString(args), e);
return false;
}
}
/** Class that represents result of invoking a method that has a non-void return type. */
public static class MethodInvokeResult {
public boolean success;
public Object value;
MethodInvokeResult(boolean success, Object value) {
this.value = success;
this.value = value;
}
}
/**
* Wrapper for {@link #invokeMethod(Method, Object, Object...)} without arguments.
*/
@NonNull
public static MethodInvokeResult invokeMethod(@NonNull Method method, Object obj) {
return invokeMethod(method, obj, new Object[0]);
}
/**
* Invoke a {@link Method} on the specified object with the specified arguments.
*
* @param method The {@link Method} to invoke.
* @param obj The {@link Object} the method should be invoked from.
* @param args The arguments to pass to the method.
* @return Returns the {@link MethodInvokeResult} of invoking the method. The
* {@link MethodInvokeResult#success} will be {@code true} if invoking the method was successful,
* otherwise {@code false}. The {@link MethodInvokeResult#value} will contain the {@link Object}
* returned by the method.
*/
@NonNull
public static MethodInvokeResult invokeMethod(@NonNull Method method, Object obj, Object... args) {
try {
method.setAccessible(true);
return new MethodInvokeResult(true, method.invoke(obj, args));
} catch (Exception e) {
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to invoke \"" + method.getName() + "\" method with object \"" + obj + "\" and args: " + Arrays.toString(args), e);
return new MethodInvokeResult(false, null);
}
}
/**
* Wrapper for {@link #getConstructor(String, Class[])} without parameters.
*/
@Nullable
public static Constructor<?> getConstructor(@NonNull String className) {
return getConstructor(className, new Class<?>[0]);
}
/**
* Wrapper for {@link #getConstructor(Class, Class[])} to get a {@link Constructor} for the
* {@code className}.
*/
@Nullable
public static Constructor<?> getConstructor(@NonNull String className, Class<?>... parameterTypes) {
try {
return getConstructor(Class.forName(className), parameterTypes);
} catch (Exception e) {
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get constructor for \"" + className + "\" class with parameter types: " + Arrays.toString(parameterTypes), e);
return null;
}
}
/**
* Get a {@link Constructor} for the specified class with the specified parameters.
*
* @param clazz The {@link Class} for which to return the constructor.
* @param parameterTypes The parameter types of the constructor.
* @return Returns the {@link Constructor} if getting the it was successful, otherwise {@code null}.
*/
@Nullable
public static Constructor<?> getConstructor(@NonNull Class<?> clazz, Class<?>... parameterTypes) {
try {
Constructor<?> constructor = clazz.getConstructor(parameterTypes);
constructor.setAccessible(true);
return constructor;
} catch (Exception e) {
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get constructor for \"" + clazz.getName() + "\" class with parameter types: " + Arrays.toString(parameterTypes), e);
return null;
}
}
/**
* Wrapper for {@link #invokeConstructor(Constructor, Object...)} without arguments.
*/
@Nullable
public static Object invokeConstructor(@NonNull Constructor<?> constructor) {
return invokeConstructor(constructor, new Object[0]);
}
/**
* Invoke a {@link Constructor} with the specified arguments.
*
* @param constructor The {@link Constructor} to invoke.
* @param args The arguments to pass to the constructor.
* @return Returns the new instance if invoking the constructor was successful, otherwise {@code null}.
*/
@Nullable
public static Object invokeConstructor(@NonNull Constructor<?> constructor, Object... args) {
try {
constructor.setAccessible(true);
return constructor.newInstance(args);
} catch (Exception e) {
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to invoke \"" + constructor.getName() + "\" constructor with args: " + Arrays.toString(args), e);
return null;
}
}
}

View File

@@ -1,9 +1,13 @@
package com.termux.shared.shell;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.os.Build;
import androidx.annotation.NonNull;
import com.termux.shared.android.SELinuxUtils;
import com.termux.shared.data.DataUtils;
import com.termux.shared.models.errors.Error;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.file.FileUtils;
@@ -57,10 +61,41 @@ public class TermuxShellUtils {
if (TERMUX_API_VERSION_NAME != null)
environment.add("TERMUX_API_VERSION=" + TERMUX_API_VERSION_NAME);
environment.add("TERM=xterm-256color");
environment.add("COLORTERM=truecolor");
try {
ApplicationInfo applicationInfo = currentPackageContext.getPackageManager().getApplicationInfo(
TermuxConstants.TERMUX_PACKAGE_NAME, 0);
if (applicationInfo != null && !applicationInfo.enabled) {
applicationInfo = null;
}
if (applicationInfo != null) {
environment.add("TERMUX_APP__DATA_DIR=" + applicationInfo.dataDir);
environment.add("TERMUX_APP__LEGACY_DATA_DIR=" + "/data/data/" + applicationInfo.packageName);
environment.add("TERMUX_APP__SE_FILE_CONTEXT=" + SELinuxUtils.getFileContext(applicationInfo.dataDir));
String seInfoUser = PackageUtils.getApplicationInfoSeInfoUserForPackage(applicationInfo);
environment.add("TERMUX_APP__SE_INFO=" + PackageUtils.getApplicationInfoSeInfoForPackage(applicationInfo) +
(DataUtils.isNullOrEmpty(seInfoUser) ? "" : seInfoUser));
}
} catch (final Exception e) {
// Ignore
}
environment.add("TERMUX__ROOTFS_DIR=" + TermuxConstants.TERMUX_FILES_DIR_PATH);
environment.add("HOME=" + TermuxConstants.TERMUX_HOME_DIR_PATH);
environment.add("TERMUX__HOME=" + TermuxConstants.TERMUX_HOME_DIR_PATH);
environment.add("PREFIX=" + TermuxConstants.TERMUX_PREFIX_DIR_PATH);
environment.add("TERMUX__PREFIX=" + TermuxConstants.TERMUX_PREFIX_DIR_PATH);
environment.add("TERMUX__SE_PROCESS_CONTEXT=" + SELinuxUtils.getContext());
environment.add("BOOTCLASSPATH=" + System.getenv("BOOTCLASSPATH"));
environment.add("ANDROID_ROOT=" + System.getenv("ANDROID_ROOT"));
environment.add("ANDROID_DATA=" + System.getenv("ANDROID_DATA"));
@@ -75,6 +110,8 @@ public class TermuxShellUtils {
addToEnvIfPresent(environment, "ANDROID_RUNTIME_ROOT");
addToEnvIfPresent(environment, "ANDROID_TZDATA_ROOT");
environment.add("ANDROID__BUILD_VERSION_SDK=" + Build.VERSION.SDK_INT);
if (isFailSafe) {
// Keep the default path so that system binaries can be used in the failsafe session.
environment.add("PATH= " + System.getenv("PATH"));

View File

@@ -10,7 +10,10 @@ import java.util.Map;
public class ExtraKeysConstants {
/** Defines the repetitive keys that can be passed to {@link ExtraKeysView#setRepetitiveKeys(List)}. */
public static List<String> PRIMARY_REPETITIVE_KEYS = Arrays.asList("UP", "DOWN", "LEFT", "RIGHT", "BKSP", "DEL");
public static List<String> PRIMARY_REPETITIVE_KEYS = Arrays.asList(
"UP", "DOWN", "LEFT", "RIGHT",
"BKSP", "DEL",
"PGUP", "PGDN");

View File

@@ -2,11 +2,16 @@ package com.termux.shared.termux;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.os.Build;
import android.system.Os;
import android.system.OsConstants;
import androidx.annotation.NonNull;
import com.google.common.base.Joiner;
import com.termux.shared.android.SELinuxUtils;
import com.termux.shared.data.DataUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.markdown.MarkdownUtils;
import com.termux.shared.packages.PackageUtils;
@@ -32,6 +37,8 @@ public class AndroidUtils {
*/
public static String getAppInfoMarkdownString(@NonNull final Context context) {
StringBuilder markdownString = new StringBuilder();
ApplicationInfo applicationInfo = context.getApplicationInfo();
if (applicationInfo == null) return null;
AndroidUtils.appendPropertyToMarkdown(markdownString,"APP_NAME", PackageUtils.getAppNameForPackage(context));
AndroidUtils.appendPropertyToMarkdown(markdownString,"PACKAGE_NAME", PackageUtils.getPackageNameForPackage(context));
@@ -44,6 +51,13 @@ public class AndroidUtils {
AndroidUtils.appendPropertyToMarkdown(markdownString,"IS_INSTALLED_ON_EXTERNAL_STORAGE", true);
}
AndroidUtils.appendPropertyToMarkdown(markdownString,"SE_PROCESS_CONTEXT", SELinuxUtils.getContext());
AndroidUtils.appendPropertyToMarkdown(markdownString,"SE_FILE_CONTEXT", SELinuxUtils.getFileContext(context.getFilesDir().getAbsolutePath()));
String seInfoUser = PackageUtils.getApplicationInfoSeInfoUserForPackage(applicationInfo);
AndroidUtils.appendPropertyToMarkdown(markdownString,"SE_INFO", PackageUtils.getApplicationInfoSeInfoForPackage(applicationInfo) +
(DataUtils.isNullOrEmpty(seInfoUser) ? "" : seInfoUser));
String filesDir = context.getFilesDir().getAbsolutePath();
if (!filesDir.equals("/data/user/0/" + context.getPackageName() + "/files") &&
!filesDir.equals("/data/data/" + context.getPackageName() + "/files"))
@@ -99,7 +113,19 @@ public class AndroidUtils {
appendPropertyToMarkdown(markdownString, "BOARD", Build.BOARD);
appendPropertyToMarkdown(markdownString, "HARDWARE", Build.HARDWARE);
appendPropertyToMarkdown(markdownString, "DEVICE", Build.DEVICE);
appendPropertyToMarkdown(markdownString, "SUPPORTED_ABIS", Joiner.on(", ").skipNulls().join(Build.SUPPORTED_ABIS));
appendPropertyToMarkdown(markdownString, "SUPPORTED_32_BIT_ABIS", Joiner.on(", ").skipNulls().join(Build.SUPPORTED_32_BIT_ABIS));
appendPropertyToMarkdown(markdownString, "SUPPORTED_64_BIT_ABIS", Joiner.on(", ").skipNulls().join(Build.SUPPORTED_64_BIT_ABIS));
// If on Android >= 15
if (Build.VERSION.SDK_INT >= 35) {
try {
appendPropertyToMarkdownIfSet(markdownString, "PAGE_SIZE", Os.sysconf(OsConstants._SC_PAGESIZE));
} catch (Throwable t) {
// Ignore
}
}
markdownString.append("\n##\n");

View File

@@ -485,6 +485,12 @@ public final class TermuxConstants {
* Termux miscellaneous urls.
*/
/** Termux Site */
public static final String TERMUX_SITE = TERMUX_APP_NAME + " Site"; // Default: "Termux Site"
/** Termux Site url */
public static final String TERMUX_SITE_URL = "https://termux.dev"; // Default: "https://termux.dev"
/** Termux Wiki */
public static final String TERMUX_WIKI = TERMUX_APP_NAME + " Wiki"; // Default: "Termux Wiki"
@@ -499,10 +505,10 @@ public final class TermuxConstants {
/** Termux support email url */
public static final String TERMUX_SUPPORT_EMAIL_URL = "termuxreports@groups.io"; // Default: "termuxreports@groups.io"
public static final String TERMUX_SUPPORT_EMAIL_URL = "support@termux.dev"; // Default: "support@termux.dev"
/** Termux support email mailto url */
public static final String TERMUX_SUPPORT_EMAIL_MAILTO_URL = "mailto:" + TERMUX_SUPPORT_EMAIL_URL; // Default: "mailto:termuxreports@groups.io"
public static final String TERMUX_SUPPORT_EMAIL_MAILTO_URL = "mailto:" + TERMUX_SUPPORT_EMAIL_URL; // Default: "mailto:support@termux.dev"
/** Termux Reddit subreddit */
@@ -513,7 +519,7 @@ public final class TermuxConstants {
/** Termux donate url */
public static final String TERMUX_DONATE_URL = TERMUX_PACKAGES_GITHUB_REPO_URL + "/wiki/Donate"; // Default: "https://github.com/termux/termux-packages/wiki/Donate"
public static final String TERMUX_DONATE_URL = TERMUX_SITE_URL + "/donate"; // Default: "https://termux.dev/donate"