result = new ArrayList<>();
38 | result.add(executable);
39 | if (arguments != null) Collections.addAll(result, arguments);
40 | return result.toArray(new String[0]);
41 | }
42 |
43 | /** Get basename for executable. */
44 | @Nullable
45 | public static String getExecutableBasename(@Nullable String executable) {
46 | return FileUtils.getFileBasename(executable);
47 | }
48 |
49 |
50 |
51 | /** Get transcript for {@link TerminalSession}. */
52 | public static String getTerminalSessionTranscriptText(TerminalSession terminalSession, boolean linesJoined, boolean trim) {
53 | if (terminalSession == null) return null;
54 |
55 | TerminalEmulator terminalEmulator = terminalSession.getEmulator();
56 | if (terminalEmulator == null) return null;
57 |
58 | TerminalBuffer terminalBuffer = terminalEmulator.getScreen();
59 | if (terminalBuffer == null) return null;
60 |
61 | String transcriptText;
62 |
63 | if (linesJoined)
64 | transcriptText = terminalBuffer.getTranscriptTextWithFullLinesJoined();
65 | else
66 | transcriptText = terminalBuffer.getTranscriptTextWithoutJoinedLines();
67 |
68 | if (transcriptText == null) return null;
69 |
70 | if (trim)
71 | transcriptText = transcriptText.trim();
72 |
73 | return transcriptText;
74 | }
75 |
76 | }
77 |
--------------------------------------------------------------------------------
/app/src/main/java/com/termux/app/fragments/settings/termux/TerminalViewPreferencesFragment.java:
--------------------------------------------------------------------------------
1 | package com.termux.app.fragments.settings.termux;
2 |
3 | import android.content.Context;
4 | import android.os.Bundle;
5 |
6 | import androidx.annotation.Keep;
7 | import androidx.preference.PreferenceDataStore;
8 | import androidx.preference.PreferenceFragmentCompat;
9 | import androidx.preference.PreferenceManager;
10 |
11 | import com.termux.R;
12 | import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences;
13 |
14 | @Keep
15 | public class TerminalViewPreferencesFragment extends PreferenceFragmentCompat {
16 |
17 | @Override
18 | public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
19 | Context context = getContext();
20 | if (context == null) return;
21 |
22 | PreferenceManager preferenceManager = getPreferenceManager();
23 | preferenceManager.setPreferenceDataStore(TerminalViewPreferencesDataStore.getInstance(context));
24 |
25 | setPreferencesFromResource(R.xml.termux_terminal_view_preferences, rootKey);
26 | }
27 |
28 | }
29 |
30 | class TerminalViewPreferencesDataStore extends PreferenceDataStore {
31 |
32 | private final Context mContext;
33 | private final TermuxAppSharedPreferences mPreferences;
34 |
35 | private static TerminalViewPreferencesDataStore mInstance;
36 |
37 | private TerminalViewPreferencesDataStore(Context context) {
38 | mContext = context;
39 | mPreferences = TermuxAppSharedPreferences.build(context, true);
40 | }
41 |
42 | public static synchronized TerminalViewPreferencesDataStore getInstance(Context context) {
43 | if (mInstance == null) {
44 | mInstance = new TerminalViewPreferencesDataStore(context);
45 | }
46 | return mInstance;
47 | }
48 |
49 |
50 |
51 | @Override
52 | public void putBoolean(String key, boolean value) {
53 | if (mPreferences == null) return;
54 | if (key == null) return;
55 |
56 | switch (key) {
57 | case "terminal_margin_adjustment":
58 | mPreferences.setTerminalMarginAdjustment(value);
59 | break;
60 | default:
61 | break;
62 | }
63 | }
64 |
65 | @Override
66 | public boolean getBoolean(String key, boolean defValue) {
67 | if (mPreferences == null) return false;
68 |
69 | switch (key) {
70 | case "terminal_margin_adjustment":
71 | return mPreferences.isTerminalMarginAdjustmentEnabled();
72 | default:
73 | return false;
74 | }
75 | }
76 |
77 | }
78 |
--------------------------------------------------------------------------------
/termux-shared/src/main/java/com/termux/shared/termux/terminal/TermuxTerminalSessionClientBase.java:
--------------------------------------------------------------------------------
1 | package com.termux.shared.termux.terminal;
2 |
3 | import androidx.annotation.NonNull;
4 | import androidx.annotation.Nullable;
5 |
6 | import com.termux.shared.logger.Logger;
7 | import com.termux.terminal.TerminalSession;
8 | import com.termux.terminal.TerminalSessionClient;
9 |
10 | public class TermuxTerminalSessionClientBase implements TerminalSessionClient {
11 |
12 | public TermuxTerminalSessionClientBase() {
13 | }
14 |
15 | @Override
16 | public void onTextChanged(@NonNull TerminalSession changedSession) {
17 | }
18 |
19 | @Override
20 | public void onTitleChanged(@NonNull TerminalSession updatedSession) {
21 | }
22 |
23 | @Override
24 | public void onSessionFinished(@NonNull TerminalSession finishedSession) {
25 | }
26 |
27 | @Override
28 | public void onCopyTextToClipboard(@NonNull TerminalSession session, String text) {
29 | }
30 |
31 | @Override
32 | public void onPasteTextFromClipboard(@Nullable TerminalSession session) {
33 | }
34 |
35 | @Override
36 | public void onBell(@NonNull TerminalSession session) {
37 | }
38 |
39 | @Override
40 | public void onColorsChanged(@NonNull TerminalSession changedSession) {
41 | }
42 |
43 | @Override
44 | public void onTerminalCursorStateChange(boolean state) {
45 | }
46 |
47 | @Override
48 | public void setTerminalShellPid(@NonNull TerminalSession session, int pid) {
49 | }
50 |
51 |
52 | @Override
53 | public Integer getTerminalCursorStyle() {
54 | return null;
55 | }
56 |
57 |
58 |
59 | @Override
60 | public void logError(String tag, String message) {
61 | Logger.logError(tag, message);
62 | }
63 |
64 | @Override
65 | public void logWarn(String tag, String message) {
66 | Logger.logWarn(tag, message);
67 | }
68 |
69 | @Override
70 | public void logInfo(String tag, String message) {
71 | Logger.logInfo(tag, message);
72 | }
73 |
74 | @Override
75 | public void logDebug(String tag, String message) {
76 | Logger.logDebug(tag, message);
77 | }
78 |
79 | @Override
80 | public void logVerbose(String tag, String message) {
81 | Logger.logVerbose(tag, message);
82 | }
83 |
84 | @Override
85 | public void logStackTraceWithMessage(String tag, String message, Exception e) {
86 | Logger.logStackTraceWithMessage(tag, message, e);
87 | }
88 |
89 | @Override
90 | public void logStackTrace(String tag, Exception e) {
91 | Logger.logStackTrace(tag, e);
92 | }
93 |
94 | }
95 |
--------------------------------------------------------------------------------
/termux-shared/src/main/java/com/termux/shared/file/filesystem/FileKey.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2008, 2009, Oracle and/or its affiliates. All rights reserved.
3 | * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 | *
5 | * This code is free software; you can redistribute it and/or modify it
6 | * under the terms of the GNU General Public License version 2 only, as
7 | * published by the Free Software Foundation. Oracle designates this
8 | * particular file as subject to the "Classpath" exception as provided
9 | * by Oracle in the LICENSE file that accompanied this code.
10 | *
11 | * This code is distributed in the hope that it will be useful, but WITHOUT
12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 | * version 2 for more details (a copy is included in the LICENSE file that
15 | * accompanied this code).
16 | *
17 | * You should have received a copy of the GNU General Public License version
18 | * 2 along with this work; if not, write to the Free Software Foundation,
19 | * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 | *
21 | * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 | * or visit www.oracle.com if you need additional information or have any
23 | * questions.
24 | */
25 |
26 | package com.termux.shared.file.filesystem;
27 |
28 | /**
29 | * Container for device/inode to uniquely identify file.
30 | * https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/src/main/java/sun/nio/fs/UnixFileKey.java
31 | */
32 |
33 | public class FileKey {
34 | private final long st_dev;
35 | private final long st_ino;
36 |
37 | FileKey(long st_dev, long st_ino) {
38 | this.st_dev = st_dev;
39 | this.st_ino = st_ino;
40 | }
41 |
42 | @Override
43 | public int hashCode() {
44 | return (int)(st_dev ^ (st_dev >>> 32)) +
45 | (int)(st_ino ^ (st_ino >>> 32));
46 | }
47 |
48 | @Override
49 | public boolean equals(Object obj) {
50 | if (obj == this)
51 | return true;
52 | if (!(obj instanceof FileKey))
53 | return false;
54 | FileKey other = (FileKey)obj;
55 | return (this.st_dev == other.st_dev) && (this.st_ino == other.st_ino);
56 | }
57 |
58 | @Override
59 | public String toString() {
60 | StringBuilder sb = new StringBuilder();
61 | sb.append("(dev=")
62 | .append(Long.toHexString(st_dev))
63 | .append(",ino=")
64 | .append(st_ino)
65 | .append(')');
66 | return sb.toString();
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/termux-shared/src/main/java/com/termux/shared/file/filesystem/FilePermission.java:
--------------------------------------------------------------------------------
1 |
2 | /*
3 | * Copyright (c) 2007, 2011, Oracle and/or its affiliates. All rights reserved.
4 | * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
5 | *
6 | * This code is free software; you can redistribute it and/or modify it
7 | * under the terms of the GNU General Public License version 2 only, as
8 | * published by the Free Software Foundation. Oracle designates this
9 | * particular file as subject to the "Classpath" exception as provided
10 | * by Oracle in the LICENSE file that accompanied this code.
11 | *
12 | * This code is distributed in the hope that it will be useful, but WITHOUT
13 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
15 | * version 2 for more details (a copy is included in the LICENSE file that
16 | * accompanied this code).
17 | *
18 | * You should have received a copy of the GNU General Public License version
19 | * 2 along with this work; if not, write to the Free Software Foundation,
20 | * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
21 | *
22 | * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
23 | * or visit www.oracle.com if you need additional information or have any
24 | * questions.
25 | */
26 |
27 | package com.termux.shared.file.filesystem;
28 |
29 | /**
30 | * Defines the bits for use with the {@link FileAttributes#permissions()
31 | * permissions} attribute.
32 | *
33 | * The {@link FileAttributes} class defines methods for manipulating
34 | * set of permissions.
35 | *
36 | * https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/src/main/java/java/nio/file/attribute/PosixFilePermission.java
37 | *
38 | * @since 1.7
39 | */
40 |
41 | public enum FilePermission {
42 |
43 | /**
44 | * Read permission, owner.
45 | */
46 | OWNER_READ,
47 |
48 | /**
49 | * Write permission, owner.
50 | */
51 | OWNER_WRITE,
52 |
53 | /**
54 | * Execute/search permission, owner.
55 | */
56 | OWNER_EXECUTE,
57 |
58 | /**
59 | * Read permission, group.
60 | */
61 | GROUP_READ,
62 |
63 | /**
64 | * Write permission, group.
65 | */
66 | GROUP_WRITE,
67 |
68 | /**
69 | * Execute/search permission, group.
70 | */
71 | GROUP_EXECUTE,
72 |
73 | /**
74 | * Read permission, others.
75 | */
76 | OTHERS_READ,
77 |
78 | /**
79 | * Write permission, others.
80 | */
81 | OTHERS_WRITE,
82 |
83 | /**
84 | * Execute/search permission, others.
85 | */
86 | OTHERS_EXECUTE
87 |
88 | }
89 |
--------------------------------------------------------------------------------
/termux-shared/src/main/java/com/termux/shared/android/ProcessUtils.java:
--------------------------------------------------------------------------------
1 | package com.termux.shared.android;
2 |
3 | import android.app.ActivityManager;
4 | import android.content.Context;
5 |
6 | import androidx.annotation.NonNull;
7 | import androidx.annotation.Nullable;
8 |
9 | import com.termux.shared.logger.Logger;
10 |
11 | import java.util.List;
12 |
13 | public class ProcessUtils {
14 |
15 | public static final String LOG_TAG = "ProcessUtils";
16 |
17 | /**
18 | * Get the app process name for a pid with a call to {@link ActivityManager#getRunningAppProcesses()}.
19 | *
20 | * This will not return child process names. Android did not keep track of them before android 12
21 | * phantom process addition, but there is no API via IActivityManager to get them.
22 | *
23 | * To get process name for pids of own app's child processes, check `get_process_name_from_cmdline()`
24 | * in `local-socket.cpp`.
25 | *
26 | * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/core/java/android/app/ActivityManager.java;l=3362
27 | * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java;l=8434
28 | * https://cs.android.com/android/_/android/platform/frameworks/base/+/refs/tags/android-12.0.0_r32:services/core/java/com/android/server/am/PhantomProcessList.java
29 | * https://cs.android.com/android/_/android/platform/frameworks/base/+/refs/tags/android-12.0.0_r32:services/core/java/com/android/server/am/PhantomProcessRecord.java
30 | *
31 | * @param context The {@link Context} for operations.
32 | * @param pid The pid of the process.
33 | * @return Returns the app process name if found, otherwise {@code null}.
34 | */
35 | @Nullable
36 | public static String getAppProcessNameForPid(@NonNull Context context, int pid) {
37 | if (pid < 0) return null;
38 |
39 | ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
40 | if (activityManager == null) return null;
41 | try {
42 | List runningApps = activityManager.getRunningAppProcesses();
43 | if (runningApps == null) {
44 | return null;
45 | }
46 | for (ActivityManager.RunningAppProcessInfo procInfo : runningApps) {
47 | if (procInfo.pid == pid) {
48 | return procInfo.processName;
49 | }
50 | }
51 | } catch (Exception e) {
52 | Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get app process name for pid " + pid, e);
53 | }
54 |
55 | return null;
56 | }
57 |
58 | }
59 |
--------------------------------------------------------------------------------
/app/src/main/java/com/termux/app/terminal/io/FullScreenWorkAround.java:
--------------------------------------------------------------------------------
1 | package com.termux.app.terminal.io;
2 |
3 | import android.graphics.Rect;
4 | import android.view.View;
5 | import android.view.ViewGroup;
6 |
7 | import com.termux.app.TermuxActivity;
8 |
9 | /**
10 | * Work around for fullscreen mode in Termux to fix ExtraKeysView not being visible.
11 | * This class is derived from:
12 | * https://stackoverflow.com/questions/7417123/android-how-to-adjust-layout-in-full-screen-mode-when-softkeyboard-is-visible
13 | * and has some additional tweaks
14 | * ---
15 | * For more information, see https://issuetracker.google.com/issues/36911528
16 | */
17 | public class FullScreenWorkAround {
18 | private final View mChildOfContent;
19 | private int mUsableHeightPrevious;
20 | private final ViewGroup.LayoutParams mViewGroupLayoutParams;
21 |
22 | private final int mNavBarHeight;
23 |
24 |
25 | public static void apply(TermuxActivity activity) {
26 | new FullScreenWorkAround(activity);
27 | }
28 |
29 | private FullScreenWorkAround(TermuxActivity activity) {
30 | ViewGroup content = activity.findViewById(android.R.id.content);
31 | mChildOfContent = content.getChildAt(0);
32 | mViewGroupLayoutParams = mChildOfContent.getLayoutParams();
33 | mNavBarHeight = activity.getNavBarHeight();
34 | mChildOfContent.getViewTreeObserver().addOnGlobalLayoutListener(this::possiblyResizeChildOfContent);
35 | }
36 |
37 | private void possiblyResizeChildOfContent() {
38 | int usableHeightNow = computeUsableHeight();
39 | if (usableHeightNow != mUsableHeightPrevious) {
40 | int usableHeightSansKeyboard = mChildOfContent.getRootView().getHeight();
41 | int heightDifference = usableHeightSansKeyboard - usableHeightNow;
42 | if (heightDifference > (usableHeightSansKeyboard / 4)) {
43 | // keyboard probably just became visible
44 |
45 | // ensures that usable layout space does not extend behind the
46 | // soft keyboard, causing the extra keys to not be visible
47 | mViewGroupLayoutParams.height = (usableHeightSansKeyboard - heightDifference) + getNavBarHeight();
48 | } else {
49 | // keyboard probably just became hidden
50 | mViewGroupLayoutParams.height = usableHeightSansKeyboard;
51 | }
52 | mChildOfContent.requestLayout();
53 | mUsableHeightPrevious = usableHeightNow;
54 | }
55 | }
56 |
57 | private int getNavBarHeight() {
58 | return mNavBarHeight;
59 | }
60 |
61 | private int computeUsableHeight() {
62 | Rect r = new Rect();
63 | mChildOfContent.getWindowVisibleDisplayFrame(r);
64 | return (r.bottom - r.top);
65 | }
66 |
67 | }
68 |
69 |
--------------------------------------------------------------------------------
/terminal-emulator/src/test/java/com/termux/terminal/TextStyleTest.java:
--------------------------------------------------------------------------------
1 | package com.termux.terminal;
2 |
3 | import junit.framework.TestCase;
4 |
5 | public class TextStyleTest extends TestCase {
6 |
7 | private static final int[] ALL_EFFECTS = new int[]{0, TextStyle.CHARACTER_ATTRIBUTE_BOLD, TextStyle.CHARACTER_ATTRIBUTE_ITALIC,
8 | TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE, TextStyle.CHARACTER_ATTRIBUTE_BLINK, TextStyle.CHARACTER_ATTRIBUTE_INVERSE,
9 | TextStyle.CHARACTER_ATTRIBUTE_INVISIBLE, TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH, TextStyle.CHARACTER_ATTRIBUTE_PROTECTED,
10 | TextStyle.CHARACTER_ATTRIBUTE_DIM};
11 |
12 | public void testEncodingSingle() {
13 | for (int fx : ALL_EFFECTS) {
14 | for (int fg = 0; fg < TextStyle.NUM_INDEXED_COLORS; fg++) {
15 | for (int bg = 0; bg < TextStyle.NUM_INDEXED_COLORS; bg++) {
16 | long encoded = TextStyle.encode(fg, bg, fx);
17 | assertEquals(fg, TextStyle.decodeForeColor(encoded));
18 | assertEquals(bg, TextStyle.decodeBackColor(encoded));
19 | assertEquals(fx, TextStyle.decodeEffect(encoded));
20 | }
21 | }
22 | }
23 | }
24 |
25 | public void testEncoding24Bit() {
26 | int[] values = {255, 240, 127, 1, 0};
27 | for (int red : values) {
28 | for (int green : values) {
29 | for (int blue : values) {
30 | int argb = 0xFF000000 | (red << 16) | (green << 8) | blue;
31 | long encoded = TextStyle.encode(argb, 0, 0);
32 | assertEquals(argb, TextStyle.decodeForeColor(encoded));
33 | encoded = TextStyle.encode(0, argb, 0);
34 | assertEquals(argb, TextStyle.decodeBackColor(encoded));
35 | }
36 | }
37 | }
38 | }
39 |
40 |
41 | public void testEncodingCombinations() {
42 | for (int f1 : ALL_EFFECTS) {
43 | for (int f2 : ALL_EFFECTS) {
44 | int combined = f1 | f2;
45 | assertEquals(combined, TextStyle.decodeEffect(TextStyle.encode(0, 0, combined)));
46 | }
47 | }
48 | }
49 |
50 | public void testEncodingStrikeThrough() {
51 | long encoded = TextStyle.encode(TextStyle.COLOR_INDEX_FOREGROUND, TextStyle.COLOR_INDEX_BACKGROUND,
52 | TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH);
53 | assertTrue((TextStyle.decodeEffect(encoded) & TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH) != 0);
54 | }
55 |
56 | public void testEncodingProtected() {
57 | long encoded = TextStyle.encode(TextStyle.COLOR_INDEX_FOREGROUND, TextStyle.COLOR_INDEX_BACKGROUND,
58 | TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH);
59 | assertEquals(0, (TextStyle.decodeEffect(encoded) & TextStyle.CHARACTER_ATTRIBUTE_PROTECTED));
60 | encoded = TextStyle.encode(TextStyle.COLOR_INDEX_FOREGROUND, TextStyle.COLOR_INDEX_BACKGROUND,
61 | TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH | TextStyle.CHARACTER_ATTRIBUTE_PROTECTED);
62 | assertTrue((TextStyle.decodeEffect(encoded) & TextStyle.CHARACTER_ATTRIBUTE_PROTECTED) != 0);
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/terminal-emulator/src/main/java/com/termux/terminal/Logger.java:
--------------------------------------------------------------------------------
1 | package com.termux.terminal;
2 |
3 | import android.util.Log;
4 |
5 | import java.io.IOException;
6 | import java.io.PrintWriter;
7 | import java.io.StringWriter;
8 |
9 | public class Logger {
10 |
11 | public static void logError(TerminalSessionClient client, String logTag, String message) {
12 | if (client != null)
13 | client.logError(logTag, message);
14 | else
15 | Log.e(logTag, message);
16 | }
17 |
18 | public static void logWarn(TerminalSessionClient client, String logTag, String message) {
19 | if (client != null)
20 | client.logWarn(logTag, message);
21 | else
22 | Log.w(logTag, message);
23 | }
24 |
25 | public static void logInfo(TerminalSessionClient client, String logTag, String message) {
26 | if (client != null)
27 | client.logInfo(logTag, message);
28 | else
29 | Log.i(logTag, message);
30 | }
31 |
32 | public static void logDebug(TerminalSessionClient client, String logTag, String message) {
33 | if (client != null)
34 | client.logDebug(logTag, message);
35 | else
36 | Log.d(logTag, message);
37 | }
38 |
39 | public static void logVerbose(TerminalSessionClient client, String logTag, String message) {
40 | if (client != null)
41 | client.logVerbose(logTag, message);
42 | else
43 | Log.v(logTag, message);
44 | }
45 |
46 | public static void logStackTraceWithMessage(TerminalSessionClient client, String tag, String message, Throwable throwable) {
47 | logError(client, tag, getMessageAndStackTraceString(message, throwable));
48 | }
49 |
50 | public static String getMessageAndStackTraceString(String message, Throwable throwable) {
51 | if (message == null && throwable == null)
52 | return null;
53 | else if (message != null && throwable != null)
54 | return message + ":\n" + getStackTraceString(throwable);
55 | else if (throwable == null)
56 | return message;
57 | else
58 | return getStackTraceString(throwable);
59 | }
60 |
61 | public static String getStackTraceString(Throwable throwable) {
62 | if (throwable == null) return null;
63 |
64 | String stackTraceString = null;
65 |
66 | try {
67 | StringWriter errors = new StringWriter();
68 | PrintWriter pw = new PrintWriter(errors);
69 | throwable.printStackTrace(pw);
70 | pw.close();
71 | stackTraceString = errors.toString();
72 | errors.close();
73 | } catch (IOException e) {
74 | e.printStackTrace();
75 | }
76 |
77 | return stackTraceString;
78 | }
79 |
80 | }
81 |
--------------------------------------------------------------------------------
/flake.lock:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": {
3 | "flake-utils": {
4 | "inputs": {
5 | "systems": "systems"
6 | },
7 | "locked": {
8 | "lastModified": 1731533236,
9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
10 | "owner": "numtide",
11 | "repo": "flake-utils",
12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
13 | "type": "github"
14 | },
15 | "original": {
16 | "owner": "numtide",
17 | "repo": "flake-utils",
18 | "type": "github"
19 | }
20 | },
21 | "gradle2nix": {
22 | "inputs": {
23 | "flake-utils": "flake-utils",
24 | "nixpkgs": "nixpkgs"
25 | },
26 | "locked": {
27 | "lastModified": 1743629487,
28 | "narHash": "sha256-MjnEgT9MhO2HknLhrx7GvBRVxdOzSKydIJMyzawe2Fk=",
29 | "owner": "tadfisher",
30 | "repo": "gradle2nix",
31 | "rev": "293ecbdc10d32d9d4bdc2d23213b9be09ce247ee",
32 | "type": "github"
33 | },
34 | "original": {
35 | "owner": "tadfisher",
36 | "ref": "v2",
37 | "repo": "gradle2nix",
38 | "type": "github"
39 | }
40 | },
41 | "nixpkgs": {
42 | "locked": {
43 | "lastModified": 1743448293,
44 | "narHash": "sha256-bmEPmSjJakAp/JojZRrUvNcDX2R5/nuX6bm+seVaGhs=",
45 | "owner": "NixOS",
46 | "repo": "nixpkgs",
47 | "rev": "77b584d61ff80b4cef9245829a6f1dfad5afdfa3",
48 | "type": "github"
49 | },
50 | "original": {
51 | "owner": "NixOS",
52 | "ref": "nixos-unstable",
53 | "repo": "nixpkgs",
54 | "type": "github"
55 | }
56 | },
57 | "nixpkgs_2": {
58 | "locked": {
59 | "lastModified": 1749794982,
60 | "narHash": "sha256-Kh9K4taXbVuaLC0IL+9HcfvxsSUx8dPB5s5weJcc9pc=",
61 | "owner": "nixos",
62 | "repo": "nixpkgs",
63 | "rev": "ee930f9755f58096ac6e8ca94a1887e0534e2d81",
64 | "type": "github"
65 | },
66 | "original": {
67 | "owner": "nixos",
68 | "ref": "nixos-unstable",
69 | "repo": "nixpkgs",
70 | "type": "github"
71 | }
72 | },
73 | "root": {
74 | "inputs": {
75 | "gradle2nix": "gradle2nix",
76 | "nixpkgs": "nixpkgs_2"
77 | }
78 | },
79 | "systems": {
80 | "locked": {
81 | "lastModified": 1681028828,
82 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
83 | "owner": "nix-systems",
84 | "repo": "default",
85 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
86 | "type": "github"
87 | },
88 | "original": {
89 | "owner": "nix-systems",
90 | "repo": "default",
91 | "type": "github"
92 | }
93 | }
94 | },
95 | "root": "root",
96 | "version": 7
97 | }
98 |
--------------------------------------------------------------------------------
/app/src/main/java/com/termux/app/fragments/settings/termux/TerminalIOPreferencesFragment.java:
--------------------------------------------------------------------------------
1 | package com.termux.app.fragments.settings.termux;
2 |
3 | import android.content.Context;
4 | import android.os.Bundle;
5 |
6 | import androidx.annotation.Keep;
7 | import androidx.preference.PreferenceDataStore;
8 | import androidx.preference.PreferenceFragmentCompat;
9 | import androidx.preference.PreferenceManager;
10 |
11 | import com.termux.R;
12 | import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences;
13 |
14 | @Keep
15 | public class TerminalIOPreferencesFragment extends PreferenceFragmentCompat {
16 |
17 | @Override
18 | public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
19 | Context context = getContext();
20 | if (context == null) return;
21 |
22 | PreferenceManager preferenceManager = getPreferenceManager();
23 | preferenceManager.setPreferenceDataStore(TerminalIOPreferencesDataStore.getInstance(context));
24 |
25 | setPreferencesFromResource(R.xml.termux_terminal_io_preferences, rootKey);
26 | }
27 |
28 | }
29 |
30 | class TerminalIOPreferencesDataStore extends PreferenceDataStore {
31 |
32 | private final Context mContext;
33 | private final TermuxAppSharedPreferences mPreferences;
34 |
35 | private static TerminalIOPreferencesDataStore mInstance;
36 |
37 | private TerminalIOPreferencesDataStore(Context context) {
38 | mContext = context;
39 | mPreferences = TermuxAppSharedPreferences.build(context, true);
40 | }
41 |
42 | public static synchronized TerminalIOPreferencesDataStore getInstance(Context context) {
43 | if (mInstance == null) {
44 | mInstance = new TerminalIOPreferencesDataStore(context);
45 | }
46 | return mInstance;
47 | }
48 |
49 |
50 |
51 | @Override
52 | public void putBoolean(String key, boolean value) {
53 | if (mPreferences == null) return;
54 | if (key == null) return;
55 |
56 | switch (key) {
57 | case "soft_keyboard_enabled":
58 | mPreferences.setSoftKeyboardEnabled(value);
59 | break;
60 | case "soft_keyboard_enabled_only_if_no_hardware":
61 | mPreferences.setSoftKeyboardEnabledOnlyIfNoHardware(value);
62 | break;
63 | default:
64 | break;
65 | }
66 | }
67 |
68 | @Override
69 | public boolean getBoolean(String key, boolean defValue) {
70 | if (mPreferences == null) return false;
71 |
72 | switch (key) {
73 | case "soft_keyboard_enabled":
74 | return mPreferences.isSoftKeyboardEnabled();
75 | case "soft_keyboard_enabled_only_if_no_hardware":
76 | return mPreferences.isSoftKeyboardEnabledOnlyIfNoHardware();
77 | default:
78 | return false;
79 | }
80 | }
81 |
82 | }
83 |
--------------------------------------------------------------------------------
/termux-shared/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
10 |
28 |
29 |
30 |
34 |
35 |
36 |
37 |
38 |
39 |
43 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/termux-shared/src/main/java/com/termux/shared/theme/NightMode.java:
--------------------------------------------------------------------------------
1 | package com.termux.shared.theme;
2 |
3 | import androidx.annotation.NonNull;
4 | import androidx.annotation.Nullable;
5 | import androidx.appcompat.app.AppCompatDelegate;
6 |
7 | import com.termux.shared.logger.Logger;
8 |
9 | /** The modes used by to decide night mode for themes. */
10 | public enum NightMode {
11 |
12 | /** Night theme should be enabled. */
13 | TRUE("true", AppCompatDelegate.MODE_NIGHT_YES),
14 |
15 | /** Dark theme should be enabled. */
16 | FALSE("false", AppCompatDelegate.MODE_NIGHT_NO),
17 |
18 | /**
19 | * Use night or dark theme depending on system night mode.
20 | * https://developer.android.com/guide/topics/resources/providing-resources#NightQualifier
21 | */
22 | SYSTEM("system", AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
23 |
24 | /** The current app wide night mode used by various libraries. Defaults to {@link #SYSTEM}. */
25 | private static NightMode APP_NIGHT_MODE;
26 |
27 | private static final String LOG_TAG = "NightMode";
28 |
29 | private final String name;
30 | private final @AppCompatDelegate.NightMode int mode;
31 |
32 | NightMode(final String name, int mode) {
33 | this.name = name;
34 | this.mode = mode;
35 | }
36 |
37 | public String getName() {
38 | return name;
39 | }
40 |
41 | public int getMode() {
42 | return mode;
43 | }
44 |
45 |
46 | /** Get {@link NightMode} for {@code name} if found, otherwise {@code null}. */
47 | @Nullable
48 | public static NightMode modeOf(String name) {
49 | for (NightMode v : NightMode.values()) {
50 | if (v.name.equals(name)) {
51 | return v;
52 | }
53 | }
54 |
55 | return null;
56 | }
57 |
58 | /** Get {@link NightMode} for {@code name} if found, otherwise {@code def}. */
59 | @NonNull
60 | public static NightMode modeOf(@Nullable String name, @NonNull NightMode def) {
61 | NightMode nightMode = modeOf(name);
62 | return nightMode != null ? nightMode : def;
63 | }
64 |
65 |
66 | /** Set {@link #APP_NIGHT_MODE}. */
67 | public static void setAppNightMode(@Nullable String name) {
68 | if (name == null || name.isEmpty()) {
69 | APP_NIGHT_MODE = SYSTEM;
70 | } else {
71 | NightMode nightMode = NightMode.modeOf(name);
72 | if (nightMode == null) {
73 | Logger.logError(LOG_TAG, "Invalid APP_NIGHT_MODE \"" + name + "\"");
74 | return;
75 | }
76 | APP_NIGHT_MODE = nightMode;
77 | }
78 |
79 | Logger.logVerbose(LOG_TAG, "Set APP_NIGHT_MODE to \"" + APP_NIGHT_MODE.getName() + "\"");
80 | }
81 |
82 | /** Get {@link #APP_NIGHT_MODE}. */
83 | @NonNull
84 | public static NightMode getAppNightMode() {
85 | if (APP_NIGHT_MODE == null)
86 | APP_NIGHT_MODE = SYSTEM;
87 |
88 | return APP_NIGHT_MODE;
89 | }
90 |
91 | }
92 |
--------------------------------------------------------------------------------
/app/src/main/java/com/termux/app/activities/HelpActivity.java:
--------------------------------------------------------------------------------
1 | package com.termux.app.activities;
2 |
3 | import android.content.ActivityNotFoundException;
4 | import android.content.Intent;
5 | import android.net.Uri;
6 | import android.os.Bundle;
7 | import android.view.ViewGroup;
8 | import android.webkit.WebSettings;
9 | import android.webkit.WebView;
10 | import android.webkit.WebViewClient;
11 | import android.widget.ProgressBar;
12 | import android.widget.RelativeLayout;
13 |
14 | import androidx.appcompat.app.AppCompatActivity;
15 |
16 | import com.termux.shared.termux.TermuxConstants;
17 |
18 | /** Basic embedded browser for viewing help pages. */
19 | public final class HelpActivity extends AppCompatActivity {
20 |
21 | WebView mWebView;
22 |
23 | @Override
24 | protected void onCreate(Bundle savedInstanceState) {
25 | super.onCreate(savedInstanceState);
26 |
27 | final RelativeLayout progressLayout = new RelativeLayout(this);
28 | RelativeLayout.LayoutParams lParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
29 | lParams.addRule(RelativeLayout.CENTER_IN_PARENT);
30 | ProgressBar progressBar = new ProgressBar(this);
31 | progressBar.setIndeterminate(true);
32 | progressBar.setLayoutParams(lParams);
33 | progressLayout.addView(progressBar);
34 |
35 | mWebView = new WebView(this);
36 | WebSettings settings = mWebView.getSettings();
37 | settings.setCacheMode(WebSettings.LOAD_NO_CACHE);
38 | settings.setAppCacheEnabled(false);
39 | setContentView(progressLayout);
40 | mWebView.clearCache(true);
41 |
42 | mWebView.setWebViewClient(new WebViewClient() {
43 | @Override
44 | public boolean shouldOverrideUrlLoading(WebView view, String url) {
45 | if (url.equals(TermuxConstants.TERMUX_WIKI_URL) || url.startsWith(TermuxConstants.TERMUX_WIKI_URL + "/")) {
46 | // Inline help.
47 | setContentView(progressLayout);
48 | return false;
49 | }
50 |
51 | try {
52 | startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
53 | } catch (ActivityNotFoundException e) {
54 | // Android TV does not have a system browser.
55 | setContentView(progressLayout);
56 | return false;
57 | }
58 | return true;
59 | }
60 |
61 | @Override
62 | public void onPageFinished(WebView view, String url) {
63 | setContentView(mWebView);
64 | }
65 | });
66 | mWebView.loadUrl(TermuxConstants.TERMUX_WIKI_URL);
67 | }
68 |
69 | @Override
70 | public void onBackPressed() {
71 | if (mWebView.canGoBack()) {
72 | mWebView.goBack();
73 | } else {
74 | super.onBackPressed();
75 | }
76 | }
77 |
78 | }
79 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/termux-shared/src/main/java/com/termux/shared/termux/terminal/io/BellHandler.java:
--------------------------------------------------------------------------------
1 | package com.termux.shared.termux.terminal.io;
2 |
3 | import android.content.Context;
4 | import android.os.Build;
5 | import android.os.Handler;
6 | import android.os.Looper;
7 | import android.os.SystemClock;
8 | import android.os.VibrationEffect;
9 | import android.os.Vibrator;
10 |
11 | import com.termux.shared.logger.Logger;
12 |
13 | public class BellHandler {
14 | private static BellHandler instance = null;
15 | private static final Object lock = new Object();
16 |
17 | private static final String LOG_TAG = "BellHandler";
18 |
19 | public static BellHandler getInstance(Context context) {
20 | if (instance == null) {
21 | synchronized (lock) {
22 | if (instance == null) {
23 | instance = new BellHandler((Vibrator) context.getApplicationContext().getSystemService(Context.VIBRATOR_SERVICE));
24 | }
25 | }
26 | }
27 |
28 | return instance;
29 | }
30 |
31 | private static final long DURATION = 50;
32 | private static final long MIN_PAUSE = 3 * DURATION;
33 |
34 | private final Handler handler = new Handler(Looper.getMainLooper());
35 | private long lastBell = 0;
36 | private final Runnable bellRunnable;
37 |
38 | private BellHandler(final Vibrator vibrator) {
39 | bellRunnable = new Runnable() {
40 | @Override
41 | public void run() {
42 | if (vibrator != null) {
43 | try {
44 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
45 | vibrator.vibrate(VibrationEffect.createOneShot(DURATION, VibrationEffect.DEFAULT_AMPLITUDE));
46 | } else {
47 | vibrator.vibrate(DURATION);
48 | }
49 | } catch (Exception e) {
50 | // Issue on samsung devices on android 8
51 | // java.lang.NullPointerException: Attempt to read from field 'android.os.VibrationEffect com.android.server.VibratorService$Vibration.mEffect' on a null object reference
52 | Logger.logStackTraceWithMessage(LOG_TAG, "Failed to run vibrator", e);
53 | }
54 | }
55 | }
56 | };
57 | }
58 |
59 | public synchronized void doBell() {
60 | long now = now();
61 | long timeSinceLastBell = now - lastBell;
62 |
63 | if (timeSinceLastBell < 0) {
64 | // there is a next bell pending; don't schedule another one
65 | } else if (timeSinceLastBell < MIN_PAUSE) {
66 | // there was a bell recently, schedule the next one
67 | handler.postDelayed(bellRunnable, MIN_PAUSE - timeSinceLastBell);
68 | lastBell = lastBell + MIN_PAUSE;
69 | } else {
70 | // the last bell was long ago, do it now
71 | bellRunnable.run();
72 | lastBell = now;
73 | }
74 | }
75 |
76 | private long now() {
77 | return SystemClock.uptimeMillis();
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/terminal-emulator/src/test/java/com/termux/terminal/ScreenBufferTest.java:
--------------------------------------------------------------------------------
1 | package com.termux.terminal;
2 |
3 | public class ScreenBufferTest extends TerminalTestCase {
4 |
5 | public void testBasics() {
6 | TerminalBuffer screen = new TerminalBuffer(5, 3, 3);
7 | assertEquals("", screen.getTranscriptText());
8 | screen.setChar(0, 0, 'a', 0);
9 | assertEquals("a", screen.getTranscriptText());
10 | screen.setChar(0, 0, 'b', 0);
11 | assertEquals("b", screen.getTranscriptText());
12 | screen.setChar(2, 0, 'c', 0);
13 | assertEquals("b c", screen.getTranscriptText());
14 | screen.setChar(2, 2, 'f', 0);
15 | assertEquals("b c\n\n f", screen.getTranscriptText());
16 | screen.blockSet(0, 0, 2, 2, 'X', 0);
17 | }
18 |
19 | public void testBlockSet() {
20 | TerminalBuffer screen = new TerminalBuffer(5, 3, 3);
21 | screen.blockSet(0, 0, 2, 2, 'X', 0);
22 | assertEquals("XX\nXX", screen.getTranscriptText());
23 | screen.blockSet(1, 1, 2, 2, 'Y', 0);
24 | assertEquals("XX\nXYY\n YY", screen.getTranscriptText());
25 | }
26 |
27 | public void testGetSelectedText() {
28 | withTerminalSized(5, 3).enterString("ABCDEFGHIJ").assertLinesAre("ABCDE", "FGHIJ", " ");
29 | assertEquals("AB", mTerminal.getSelectedText(0, 0, 1, 0));
30 | assertEquals("BC", mTerminal.getSelectedText(1, 0, 2, 0));
31 | assertEquals("CDE", mTerminal.getSelectedText(2, 0, 4, 0));
32 | assertEquals("FG", mTerminal.getSelectedText(0, 1, 1, 1));
33 | assertEquals("GH", mTerminal.getSelectedText(1, 1, 2, 1));
34 | assertEquals("HIJ", mTerminal.getSelectedText(2, 1, 4, 1));
35 |
36 | assertEquals("ABCDEFG", mTerminal.getSelectedText(0, 0, 1, 1));
37 | withTerminalSized(5, 3).enterString("ABCDE\r\nFGHIJ").assertLinesAre("ABCDE", "FGHIJ", " ");
38 | assertEquals("ABCDE\nFG", mTerminal.getSelectedText(0, 0, 1, 1));
39 | }
40 |
41 | public void testGetSelectedTextJoinFullLines() {
42 | withTerminalSized(5, 3).enterString("ABCDE\r\nFG");
43 | assertEquals("ABCDEFG", mTerminal.getScreen().getSelectedText(0, 0, 1, 1, true, true));
44 |
45 | withTerminalSized(5, 3).enterString("ABC\r\nFG");
46 | assertEquals("ABC\nFG", mTerminal.getScreen().getSelectedText(0, 0, 1, 1, true, true));
47 | }
48 |
49 | public void testGetWordAtLocation() {
50 | withTerminalSized(5, 3).enterString("ABCDEFGHIJ\r\nKLMNO");
51 | assertEquals("ABCDEFGHIJKLMNO", mTerminal.getScreen().getWordAtLocation(0, 0));
52 | assertEquals("ABCDEFGHIJKLMNO", mTerminal.getScreen().getWordAtLocation(4, 1));
53 | assertEquals("ABCDEFGHIJKLMNO", mTerminal.getScreen().getWordAtLocation(4, 2));
54 |
55 | withTerminalSized(5, 3).enterString("ABC DEF GHI ");
56 | assertEquals("ABC", mTerminal.getScreen().getWordAtLocation(0, 0));
57 | assertEquals("", mTerminal.getScreen().getWordAtLocation(3, 0));
58 | assertEquals("DEF", mTerminal.getScreen().getWordAtLocation(4, 0));
59 | assertEquals("DEF", mTerminal.getScreen().getWordAtLocation(0, 1));
60 | assertEquals("DEF", mTerminal.getScreen().getWordAtLocation(1, 1));
61 | assertEquals("GHI", mTerminal.getScreen().getWordAtLocation(0, 2));
62 | assertEquals("", mTerminal.getScreen().getWordAtLocation(1, 2));
63 | assertEquals("", mTerminal.getScreen().getWordAtLocation(2, 2));
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/terminal-view/src/main/java/com/termux/view/support/PopupWindowCompatGingerbread.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2015 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License
15 | */
16 | package com.termux.view.support;
17 |
18 | import android.util.Log;
19 | import android.widget.PopupWindow;
20 |
21 | import java.lang.reflect.Method;
22 |
23 | /**
24 | * Implementation of PopupWindow compatibility that can call Gingerbread APIs.
25 | * https://chromium.googlesource.com/android_tools/+/HEAD/sdk/extras/android/support/v4/src/gingerbread/android/support/v4/widget/PopupWindowCompatGingerbread.java
26 | */
27 | public class PopupWindowCompatGingerbread {
28 |
29 | private static Method sSetWindowLayoutTypeMethod;
30 | private static boolean sSetWindowLayoutTypeMethodAttempted;
31 | private static Method sGetWindowLayoutTypeMethod;
32 | private static boolean sGetWindowLayoutTypeMethodAttempted;
33 |
34 | public static void setWindowLayoutType(PopupWindow popupWindow, int layoutType) {
35 | if (!sSetWindowLayoutTypeMethodAttempted) {
36 | try {
37 | sSetWindowLayoutTypeMethod = PopupWindow.class.getDeclaredMethod(
38 | "setWindowLayoutType", int.class);
39 | sSetWindowLayoutTypeMethod.setAccessible(true);
40 | } catch (Exception e) {
41 | // Reflection method fetch failed. Oh well.
42 | }
43 | sSetWindowLayoutTypeMethodAttempted = true;
44 | }
45 | if (sSetWindowLayoutTypeMethod != null) {
46 | try {
47 | sSetWindowLayoutTypeMethod.invoke(popupWindow, layoutType);
48 | } catch (Exception e) {
49 | // Reflection call failed. Oh well.
50 | }
51 | }
52 | }
53 |
54 | public static int getWindowLayoutType(PopupWindow popupWindow) {
55 | if (!sGetWindowLayoutTypeMethodAttempted) {
56 | try {
57 | sGetWindowLayoutTypeMethod = PopupWindow.class.getDeclaredMethod(
58 | "getWindowLayoutType");
59 | sGetWindowLayoutTypeMethod.setAccessible(true);
60 | } catch (Exception e) {
61 | // Reflection method fetch failed. Oh well.
62 | }
63 | sGetWindowLayoutTypeMethodAttempted = true;
64 | }
65 | if (sGetWindowLayoutTypeMethod != null) {
66 | try {
67 | return (Integer) sGetWindowLayoutTypeMethod.invoke(popupWindow);
68 | } catch (Exception e) {
69 | // Reflection call failed. Oh well.
70 | }
71 | }
72 | return 0;
73 | }
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/termux-shared/src/main/java/com/termux/shared/termux/terminal/TermuxTerminalViewClientBase.java:
--------------------------------------------------------------------------------
1 | package com.termux.shared.termux.terminal;
2 |
3 | import android.view.KeyEvent;
4 | import android.view.MotionEvent;
5 |
6 | import com.termux.shared.logger.Logger;
7 | import com.termux.terminal.TerminalSession;
8 | import com.termux.view.TerminalViewClient;
9 |
10 | public class TermuxTerminalViewClientBase implements TerminalViewClient {
11 |
12 | public TermuxTerminalViewClientBase() {
13 | }
14 |
15 | @Override
16 | public float onScale(float scale) {
17 | return 1.0f;
18 | }
19 |
20 | @Override
21 | public void onSingleTapUp(MotionEvent e) {
22 | }
23 |
24 | public boolean shouldBackButtonBeMappedToEscape() {
25 | return false;
26 | }
27 |
28 | public boolean shouldEnforceCharBasedInput() {
29 | return false;
30 | }
31 |
32 | public boolean shouldUseCtrlSpaceWorkaround() {
33 | return false;
34 | }
35 |
36 | @Override
37 | public boolean isTerminalViewSelected() {
38 | return true;
39 | }
40 |
41 | @Override
42 | public void copyModeChanged(boolean copyMode) {
43 | }
44 |
45 | @Override
46 | public boolean onKeyDown(int keyCode, KeyEvent e, TerminalSession session) {
47 | return false;
48 | }
49 |
50 | @Override
51 | public boolean onKeyUp(int keyCode, KeyEvent e) {
52 | return false;
53 | }
54 |
55 | @Override
56 | public boolean onLongPress(MotionEvent event) {
57 | return false;
58 | }
59 |
60 | @Override
61 | public boolean readControlKey() {
62 | return false;
63 | }
64 |
65 | @Override
66 | public boolean readAltKey() {
67 | return false;
68 | }
69 |
70 | @Override
71 | public boolean readShiftKey() {
72 | return false;
73 | }
74 |
75 | @Override
76 | public boolean readFnKey() {
77 | return false;
78 | }
79 |
80 |
81 |
82 | @Override
83 | public boolean onCodePoint(int codePoint, boolean ctrlDown, TerminalSession session) {
84 | return false;
85 | }
86 |
87 | @Override
88 | public void onEmulatorSet() {
89 |
90 | }
91 |
92 | @Override
93 | public void logError(String tag, String message) {
94 | Logger.logError(tag, message);
95 | }
96 |
97 | @Override
98 | public void logWarn(String tag, String message) {
99 | Logger.logWarn(tag, message);
100 | }
101 |
102 | @Override
103 | public void logInfo(String tag, String message) {
104 | Logger.logInfo(tag, message);
105 | }
106 |
107 | @Override
108 | public void logDebug(String tag, String message) {
109 | Logger.logDebug(tag, message);
110 | }
111 |
112 | @Override
113 | public void logVerbose(String tag, String message) {
114 | Logger.logVerbose(tag, message);
115 | }
116 |
117 | @Override
118 | public void logStackTraceWithMessage(String tag, String message, Exception e) {
119 | Logger.logStackTraceWithMessage(tag, message, e);
120 | }
121 |
122 | @Override
123 | public void logStackTrace(String tag, Exception e) {
124 | Logger.logStackTrace(tag, e);
125 | }
126 |
127 | }
128 |
--------------------------------------------------------------------------------
/terminal-emulator/src/test/java/com/termux/terminal/DecSetTest.java:
--------------------------------------------------------------------------------
1 | package com.termux.terminal;
2 |
3 | /**
4 | *
5 | * "CSI ? Pm h", DEC Private Mode Set (DECSET)
6 | *
7 | *
8 | * and
9 | *
10 | *
11 | * "CSI ? Pm l", DEC Private Mode Reset (DECRST)
12 | *
13 | *
14 | * controls various aspects of the terminal
15 | */
16 | public class DecSetTest extends TerminalTestCase {
17 |
18 | /** DECSET 25, DECTCEM, controls visibility of the cursor. */
19 | public void testEnableDisableCursor() {
20 | withTerminalSized(3, 3);
21 | assertTrue("Initially the cursor should be enabled", mTerminal.isCursorEnabled());
22 | enterString("\033[?25l"); // Disable Cursor (DECTCEM).
23 | assertFalse(mTerminal.isCursorEnabled());
24 | enterString("\033[?25h"); // Enable Cursor (DECTCEM).
25 | assertTrue(mTerminal.isCursorEnabled());
26 |
27 | enterString("\033[?25l"); // Disable Cursor (DECTCEM), again.
28 | assertFalse(mTerminal.isCursorEnabled());
29 | mTerminal.reset();
30 | assertTrue("Resetting the terminal should enable the cursor", mTerminal.isCursorEnabled());
31 |
32 | enterString("\033[?25l");
33 | assertFalse(mTerminal.isCursorEnabled());
34 | enterString("\033c"); // RIS resetting should enabled cursor.
35 | assertTrue(mTerminal.isCursorEnabled());
36 | }
37 |
38 | /** DECSET 2004, controls bracketed paste mode. */
39 | public void testBracketedPasteMode() {
40 | withTerminalSized(3, 3);
41 |
42 | mTerminal.paste("a");
43 | assertEquals("Pasting 'a' should output 'a' when bracketed paste mode is disabled", "a", mOutput.getOutputAndClear());
44 |
45 | enterString("\033[?2004h"); // Enable bracketed paste mode.
46 | mTerminal.paste("a");
47 | assertEquals("Pasting when in bracketed paste mode should be bracketed", "\033[200~a\033[201~", mOutput.getOutputAndClear());
48 |
49 | enterString("\033[?2004l"); // Disable bracketed paste mode.
50 | mTerminal.paste("a");
51 | assertEquals("Pasting 'a' should output 'a' when bracketed paste mode is disabled", "a", mOutput.getOutputAndClear());
52 |
53 | enterString("\033[?2004h"); // Enable bracketed paste mode, again.
54 | mTerminal.paste("a");
55 | assertEquals("Pasting when in bracketed paste mode again should be bracketed", "\033[200~a\033[201~", mOutput.getOutputAndClear());
56 |
57 | mTerminal.paste("\033ab\033cd\033");
58 | assertEquals("Pasting an escape character should not input it", "\033[200~abcd\033[201~", mOutput.getOutputAndClear());
59 | mTerminal.paste("\u0081ab\u0081cd\u009F");
60 | assertEquals("Pasting C1 control codes should not input it", "\033[200~abcd\033[201~", mOutput.getOutputAndClear());
61 |
62 | mTerminal.reset();
63 | mTerminal.paste("a");
64 | assertEquals("Terminal reset() should disable bracketed paste mode", "a", mOutput.getOutputAndClear());
65 | }
66 |
67 | /** DECSET 7, DECAWM, controls wraparound mode. */
68 | public void testWrapAroundMode() {
69 | // Default with wraparound:
70 | withTerminalSized(3, 3).enterString("abcd").assertLinesAre("abc", "d ", " ");
71 | // With wraparound disabled:
72 | withTerminalSized(3, 3).enterString("\033[?7labcd").assertLinesAre("abd", " ", " ");
73 | enterString("efg").assertLinesAre("abg", " ", " ");
74 | // Re-enabling wraparound:
75 | enterString("\033[?7hhij").assertLinesAre("abh", "ij ", " ");
76 | }
77 |
78 | }
79 |
--------------------------------------------------------------------------------
/termux-shared/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
13 |
14 |
15 |
16 |
17 |
18 |
23 |
31 |
32 |
33 |
38 |
43 |
44 |
49 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/termux-shared/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'maven-publish'
3 |
4 | android {
5 | compileSdkVersion project.properties.compileSdkVersion.toInteger()
6 | ndkVersion = System.getenv("JITPACK_NDK_VERSION") ?: project.properties.ndkVersion
7 |
8 | dependencies {
9 | implementation "androidx.appcompat:appcompat:1.3.1"
10 | implementation "androidx.annotation:annotation:1.3.0"
11 | implementation "androidx.core:core:1.6.0"
12 | implementation "com.google.android.material:material:1.4.0"
13 | implementation "com.google.guava:guava:24.1-jre"
14 | implementation "io.noties.markwon:core:$markwonVersion"
15 | implementation "io.noties.markwon:ext-strikethrough:$markwonVersion"
16 | implementation "io.noties.markwon:linkify:$markwonVersion"
17 | implementation "io.noties.markwon:recycler:$markwonVersion"
18 | implementation "org.lsposed.hiddenapibypass:hiddenapibypass:6.1"
19 |
20 | // Do not increment version higher than 1.0.0-alpha09 since it will break ViewUtils and needs to be looked into
21 | // noinspection GradleDependency
22 | implementation "androidx.window:window:1.0.0-alpha09"
23 |
24 | // Do not increment version higher than 2.5 or there
25 | // will be runtime exceptions on android < 8
26 | // due to missing classes like java.nio.file.Path.
27 | implementation "commons-io:commons-io:2.5"
28 |
29 | implementation project(":terminal-view")
30 |
31 | implementation "com.termux:termux-am-library:v2.0.0"
32 | }
33 |
34 | defaultConfig {
35 | minSdkVersion project.properties.minSdkVersion.toInteger()
36 | targetSdkVersion project.properties.targetSdkVersion.toInteger()
37 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
38 | externalNativeBuild {
39 | ndkBuild {
40 | cppFlags ''
41 | }
42 | }
43 | }
44 |
45 | buildTypes {
46 | release {
47 | minifyEnabled false
48 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
49 | }
50 | }
51 |
52 | compileOptions {
53 | // Flag to enable support for the new language APIs
54 | coreLibraryDesugaringEnabled true
55 |
56 | sourceCompatibility JavaVersion.VERSION_1_8
57 | targetCompatibility JavaVersion.VERSION_1_8
58 | }
59 | externalNativeBuild {
60 | ndkBuild {
61 | path file('src/main/cpp/Android.mk')
62 | }
63 | }
64 | }
65 |
66 | dependencies {
67 | testImplementation "junit:junit:4.13.2"
68 | androidTestImplementation "androidx.test.ext:junit:1.1.3"
69 | androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0"
70 | coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.1.5"
71 | }
72 |
73 | task sourceJar(type: Jar) {
74 | from android.sourceSets.main.java.srcDirs
75 | classifier "sources"
76 | }
77 |
78 | afterEvaluate {
79 | publishing {
80 | publications {
81 | // Creates a Maven publication called "release".
82 | release(MavenPublication) {
83 | from components.release
84 | groupId = 'com.termux'
85 | artifactId = 'termux-shared'
86 | version = '0.118.0'
87 | artifact(sourceJar)
88 | }
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/termux-shared/src/main/java/com/termux/shared/termux/interact/TextInputDialogUtils.java:
--------------------------------------------------------------------------------
1 | package com.termux.shared.termux.interact;
2 |
3 | import android.app.Activity;
4 | import android.app.AlertDialog;
5 | import android.content.DialogInterface;
6 | import android.text.Selection;
7 | import android.util.TypedValue;
8 | import android.view.KeyEvent;
9 | import android.view.ViewGroup.LayoutParams;
10 | import android.widget.EditText;
11 | import android.widget.LinearLayout;
12 |
13 | public final class TextInputDialogUtils {
14 |
15 | public interface TextSetListener {
16 | void onTextSet(String text);
17 | }
18 |
19 | public static void textInput(Activity activity, int titleText, String initialText,
20 | int positiveButtonText, final TextSetListener onPositive,
21 | int neutralButtonText, final TextSetListener onNeutral,
22 | int negativeButtonText, final TextSetListener onNegative,
23 | final DialogInterface.OnDismissListener onDismiss) {
24 | final EditText input = new EditText(activity);
25 | input.setSingleLine();
26 | if (initialText != null) {
27 | input.setText(initialText);
28 | Selection.setSelection(input.getText(), initialText.length());
29 | }
30 |
31 | final AlertDialog[] dialogHolder = new AlertDialog[1];
32 | input.setImeActionLabel(activity.getResources().getString(positiveButtonText), KeyEvent.KEYCODE_ENTER);
33 | input.setOnEditorActionListener((v, actionId, event) -> {
34 | onPositive.onTextSet(input.getText().toString());
35 | dialogHolder[0].dismiss();
36 | return true;
37 | });
38 |
39 | float dipInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, activity.getResources().getDisplayMetrics());
40 | // https://www.google.com/design/spec/components/dialogs.html#dialogs-specs
41 | int paddingTopAndSides = Math.round(16 * dipInPixels);
42 | int paddingBottom = Math.round(24 * dipInPixels);
43 |
44 | LinearLayout layout = new LinearLayout(activity);
45 | layout.setOrientation(LinearLayout.VERTICAL);
46 | layout.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
47 | layout.setPadding(paddingTopAndSides, paddingTopAndSides, paddingTopAndSides, paddingBottom);
48 | layout.addView(input);
49 |
50 | AlertDialog.Builder builder = new AlertDialog.Builder(activity)
51 | .setTitle(titleText).setView(layout)
52 | .setPositiveButton(positiveButtonText, (d, whichButton) -> onPositive.onTextSet(input.getText().toString()));
53 |
54 | if (onNeutral != null) {
55 | builder.setNeutralButton(neutralButtonText, (dialog, which) -> onNeutral.onTextSet(input.getText().toString()));
56 | }
57 |
58 | if (onNegative == null) {
59 | builder.setNegativeButton(android.R.string.cancel, null);
60 | } else {
61 | builder.setNegativeButton(negativeButtonText, (dialog, which) -> onNegative.onTextSet(input.getText().toString()));
62 | }
63 |
64 | if (onDismiss != null)
65 | builder.setOnDismissListener(onDismiss);
66 |
67 | dialogHolder[0] = builder.create();
68 | dialogHolder[0].setCanceledOnTouchOutside(false);
69 | dialogHolder[0].show();
70 | }
71 |
72 | }
73 |
--------------------------------------------------------------------------------