mirror of
https://github.com/fankes/termux-app.git
synced 2025-09-04 17:55:36 +08:00
Refactor TermuxActivity
This commit majorly refactors `TermuxActivity` and moves its view components and functions into dedicated classes. - The view layouts and ids have been given meaningful names, like `termux_activity.xml`. - The `TerminalToolbarViewPager` class has been created to handle the now called toolbar that shows on the bottom of the terminal view. It currently contains extra keys view defined by `terminal_toolbar_extra_keys_view.xml` file and a text input view defined by `terminal_toolbar_text_input_view.xml` file when user can switch to by swiping left. The input text will now be preserved if android destroys the activity or its recreated. - The `TermuxSessionsListViewController` class has been created to handle view related functionality of the termux sessions list shown in the left drawer, namely view creation, `onItemClick()`, `onItemLongClick()`, etc. Its list view is defined by `termux_activity.xml` file and each item's layout is defined by the `terminal_sessions_list_item.xml` file. - The `TextDataUtils` class has been added to the `com.termux.app.utils` package for text utils. - The design for the `SessionChangedCallback` interface for `TerminalSession` has been majorly changed. Firstly, it has been renamed and moved from `TerminalSession` to the dedicated `TerminalSessionClient` class file. The interface now also supports the termux app centralized logging framework so that `TerminalSession` and `TerminalEmulator` can use them. Previously, `TermuxService` was implementing a wrapper interface, which would then call the real interface defined by the `TermuxActivity` if it was currently bound to the service. This cluttered and partially duplicated the code. Now, the implementation is defined by the `TermuxSessionClientBase` and `TermuxSessionClient` classes. The `TermuxSessionClientBase` implements the `TerminalSessionClient` interface but the definition of the activity related functions do not do anything, only the background ones like the logging functions are fully implemented. The `TermuxSessionClient` class inherits from the `TermuxSessionClientBase` class and provides the implementation for the activity related functions. The design for how this works is that if the `TermuxService` is not bound to `TermuxActivity`, it just passes the `TermuxSessionClientBase` implementation to `TerminalSession`. If the activity is bound at some point, then in `onServiceConnected()` it replaces/updates the client objects stored in `TerminalSession` and `TerminalEmulator` with `TermuxSessionClient`, and then replaces them back with `TermuxSessionClientBase` in `onDestroy()`. This seems to be working for now without an issue.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -24,9 +24,12 @@ import com.termux.R;
|
||||
import com.termux.app.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY;
|
||||
import com.termux.app.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
||||
import com.termux.app.settings.preferences.TermuxSharedPreferences;
|
||||
import com.termux.app.terminal.TermuxSessionClient;
|
||||
import com.termux.app.terminal.TermuxSessionClientBase;
|
||||
import com.termux.app.utils.Logger;
|
||||
import com.termux.terminal.TerminalEmulator;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
import com.termux.terminal.TerminalSession.SessionChangedCallback;
|
||||
import com.termux.terminal.TerminalSessionClient;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
@@ -44,13 +47,11 @@ import java.util.List;
|
||||
* Optionally may hold a wake and a wifi lock, in which case that is shown in the notification - see
|
||||
* {@link #buildNotification()}.
|
||||
*/
|
||||
public final class TermuxService extends Service implements SessionChangedCallback {
|
||||
public final class TermuxService extends Service {
|
||||
|
||||
private static final String NOTIFICATION_CHANNEL_ID = "termux_notification_channel";
|
||||
private static final int NOTIFICATION_ID = 1337;
|
||||
|
||||
private static final String LOG_TAG = "TermuxService";
|
||||
|
||||
/** This service is only bound from inside the same process and never uses IPC. */
|
||||
class LocalBinder extends Binder {
|
||||
public final TermuxService service = TermuxService.this;
|
||||
@@ -63,15 +64,23 @@ public final class TermuxService extends Service implements SessionChangedCallba
|
||||
/**
|
||||
* The terminal sessions which this service manages.
|
||||
* <p/>
|
||||
* Note that this list is observed by {@link TermuxActivity#mListViewAdapter}, so any changes must be made on the UI
|
||||
* Note that this list is observed by {@link TermuxActivity#mTermuxSessionListViewController}, so any changes must be made on the UI
|
||||
* thread and followed by a call to {@link ArrayAdapter#notifyDataSetChanged()} }.
|
||||
*/
|
||||
final List<TerminalSession> mTerminalSessions = new ArrayList<>();
|
||||
|
||||
final List<BackgroundJob> mBackgroundTasks = new ArrayList<>();
|
||||
|
||||
/** Note that the service may often outlive the activity, so need to clear this reference. */
|
||||
SessionChangedCallback mSessionChangeCallback;
|
||||
/** The full implementation of the {@link TerminalSessionClient} interface to be used by {@link TerminalSession}
|
||||
* that holds activity references for activity related functions.
|
||||
* Note that the service may often outlive the activity, so need to clear this reference.
|
||||
*/
|
||||
TermuxSessionClient mTermuxSessionClient;
|
||||
|
||||
/** The basic implementation of the {@link TerminalSessionClient} interface to be used by {@link TerminalSession}
|
||||
* that does not hold activity references.
|
||||
*/
|
||||
final TermuxSessionClientBase mTermuxSessionClientBase = new TermuxSessionClientBase();;
|
||||
|
||||
/** The wake lock and wifi lock are always acquired and released together. */
|
||||
private PowerManager.WakeLock mWakeLock;
|
||||
@@ -80,6 +89,8 @@ public final class TermuxService extends Service implements SessionChangedCallba
|
||||
/** If the user has executed the {@link TermuxConstants.TERMUX_APP.TERMUX_SERVICE#ACTION_STOP_SERVICE} intent. */
|
||||
boolean mWantsToStop = false;
|
||||
|
||||
private static final String LOG_TAG = "TermuxService";
|
||||
|
||||
@SuppressLint("Wakelock")
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
@@ -266,7 +277,7 @@ public final class TermuxService extends Service implements SessionChangedCallba
|
||||
return mTerminalSessions;
|
||||
}
|
||||
|
||||
TerminalSession createTermSession(String executablePath, String[] arguments, String cwd, boolean failSafe) {
|
||||
public TerminalSession createTermSession(String executablePath, String[] arguments, String cwd, boolean failSafe) {
|
||||
TermuxConstants.TERMUX_HOME_DIR.mkdirs();
|
||||
|
||||
if (cwd == null || cwd.isEmpty()) cwd = TermuxConstants.TERMUX_HOME_DIR_PATH;
|
||||
@@ -302,7 +313,7 @@ public final class TermuxService extends Service implements SessionChangedCallba
|
||||
args[0] = processName;
|
||||
if (processArgs.length > 1) System.arraycopy(processArgs, 1, args, 1, processArgs.length - 1);
|
||||
|
||||
TerminalSession session = new TerminalSession(executablePath, cwd, args, env, this);
|
||||
TerminalSession session = new TerminalSession(executablePath, cwd, args, env, getTermuxSessionClient());
|
||||
mTerminalSessions.add(session);
|
||||
updateNotification();
|
||||
|
||||
@@ -327,35 +338,48 @@ public final class TermuxService extends Service implements SessionChangedCallba
|
||||
return indexOfRemoved;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTitleChanged(TerminalSession changedSession) {
|
||||
if (mSessionChangeCallback != null) mSessionChangeCallback.onTitleChanged(changedSession);
|
||||
/** If {@link TermuxActivity} has not bound to the {@link TermuxService} yet or is destroyed, then
|
||||
* interface functions requiring the activity should not be available to the terminal sessions,
|
||||
* so we just return the {@link #mTermuxSessionClientBase}. Once {@link TermuxActivity} bind
|
||||
* callback is received, it should call {@link #setTermuxSessionClient} to set the
|
||||
* {@link TermuxService#mTermuxSessionClient} so that further terminal sessions are directly
|
||||
* passed the {@link TermuxSessionClient} object which fully implements the
|
||||
* {@link TerminalSessionClient} interface.
|
||||
*
|
||||
* @return Returns the {@link TermuxSessionClient} if {@link TermuxActivity} has bound with
|
||||
* {@link TermuxService}, otherwise {@link TermuxSessionClientBase}.
|
||||
*/
|
||||
public TermuxSessionClientBase getTermuxSessionClient() {
|
||||
if (mTermuxSessionClient != null)
|
||||
return mTermuxSessionClient;
|
||||
else
|
||||
return mTermuxSessionClientBase;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSessionFinished(final TerminalSession finishedSession) {
|
||||
if (mSessionChangeCallback != null)
|
||||
mSessionChangeCallback.onSessionFinished(finishedSession);
|
||||
/** This should be called when {@link TermuxActivity#onServiceConnected} is called to set the
|
||||
* {@link TermuxService#mTermuxSessionClient} variable and update the {@link TerminalSession}
|
||||
* and {@link TerminalEmulator} clients in case they were passed {@link TermuxSessionClientBase}
|
||||
* earlier.
|
||||
*
|
||||
* @param termuxSessionClient The {@link TermuxSessionClient} object that fully
|
||||
* implements the {@link TerminalSessionClient} interface.
|
||||
*/
|
||||
public void setTermuxSessionClient(TermuxSessionClient termuxSessionClient) {
|
||||
mTermuxSessionClient = termuxSessionClient;
|
||||
|
||||
for (int i = 0; i < mTerminalSessions.size(); i++)
|
||||
mTerminalSessions.get(i).updateTerminalSessionClient(mTermuxSessionClient);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(TerminalSession changedSession) {
|
||||
if (mSessionChangeCallback != null) mSessionChangeCallback.onTextChanged(changedSession);
|
||||
}
|
||||
/** This should be called when {@link TermuxActivity} has been destroyed so that the
|
||||
* {@link TermuxService} and {@link TerminalSession} and {@link TerminalEmulator} clients do not
|
||||
* hold an activity references.
|
||||
*/
|
||||
public void unsetTermuxSessionClient() {
|
||||
mTermuxSessionClient = null;
|
||||
|
||||
@Override
|
||||
public void onClipboardText(TerminalSession session, String text) {
|
||||
if (mSessionChangeCallback != null) mSessionChangeCallback.onClipboardText(session, text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBell(TerminalSession session) {
|
||||
if (mSessionChangeCallback != null) mSessionChangeCallback.onBell(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onColorsChanged(TerminalSession session) {
|
||||
if (mSessionChangeCallback != null) mSessionChangeCallback.onColorsChanged(session);
|
||||
for (int i = 0; i < mTerminalSessions.size(); i++)
|
||||
mTerminalSessions.get(i).updateTerminalSessionClient(mTermuxSessionClientBase);
|
||||
}
|
||||
|
||||
public void onBackgroundJobExited(final BackgroundJob task) {
|
||||
|
@@ -26,9 +26,9 @@ import com.termux.app.TermuxConstants;
|
||||
*/
|
||||
public final class TermuxPreferenceConstants {
|
||||
|
||||
/** Defines the key for whether to show extra keys in termux terminal view */
|
||||
public static final String KEY_SHOW_EXTRA_KEYS = "show_extra_keys";
|
||||
public static final boolean DEFAULT_VALUE_SHOW_EXTRA_KEYS = true;
|
||||
/** Defines the key for whether to show terminal toolbar containing extra keys and text input field */
|
||||
public static final String KEY_SHOW_TERMINAL_TOOLBAR = "show_extra_keys";
|
||||
public static final boolean DEFAULT_VALUE_SHOW_TERMINAL_TOOLBAR = true;
|
||||
|
||||
|
||||
|
||||
|
@@ -33,17 +33,17 @@ public class TermuxSharedPreferences {
|
||||
|
||||
|
||||
|
||||
public boolean getShowExtraKeys() {
|
||||
return mSharedPreferences.getBoolean(TermuxPreferenceConstants.KEY_SHOW_EXTRA_KEYS, TermuxPreferenceConstants.DEFAULT_VALUE_SHOW_EXTRA_KEYS);
|
||||
public boolean getShowTerminalToolbar() {
|
||||
return mSharedPreferences.getBoolean(TermuxPreferenceConstants.KEY_SHOW_TERMINAL_TOOLBAR, TermuxPreferenceConstants.DEFAULT_VALUE_SHOW_TERMINAL_TOOLBAR);
|
||||
}
|
||||
|
||||
public void setShowExtraKeys(boolean value) {
|
||||
mSharedPreferences.edit().putBoolean(TermuxPreferenceConstants.KEY_SHOW_EXTRA_KEYS, value).apply();
|
||||
public void setShowTerminalToolbar(boolean value) {
|
||||
mSharedPreferences.edit().putBoolean(TermuxPreferenceConstants.KEY_SHOW_TERMINAL_TOOLBAR, value).apply();
|
||||
}
|
||||
|
||||
public boolean toggleShowExtraKeys() {
|
||||
boolean currentValue = getShowExtraKeys();
|
||||
setShowExtraKeys(!currentValue);
|
||||
public boolean toogleShowTerminalToolbar() {
|
||||
boolean currentValue = getShowTerminalToolbar();
|
||||
setShowTerminalToolbar(!currentValue);
|
||||
return !currentValue;
|
||||
}
|
||||
|
||||
@@ -108,13 +108,6 @@ public class TermuxSharedPreferences {
|
||||
setFontSize(Integer.toString(fontSize));
|
||||
}
|
||||
|
||||
/**
|
||||
* If value is not in the range [min, max], set it to either min or max.
|
||||
*/
|
||||
static int clamp(int value, int min, int max) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public String getCurrentSession() {
|
||||
@@ -152,4 +145,13 @@ public class TermuxSharedPreferences {
|
||||
mSharedPreferences.edit().putBoolean(TermuxPreferenceConstants.KEY_TERMINAL_VIEW_KEY_LOGGING_ENABLED, value).apply();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* If value is not in the range [min, max], set it to either min or max.
|
||||
*/
|
||||
static int clamp(int value, int min, int max) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,315 @@
|
||||
package com.termux.app.terminal;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Typeface;
|
||||
import android.media.AudioAttributes;
|
||||
import android.media.SoundPool;
|
||||
import android.text.TextUtils;
|
||||
import android.widget.ListView;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.app.utils.DialogUtils;
|
||||
import com.termux.app.TermuxActivity;
|
||||
import com.termux.app.TermuxConstants;
|
||||
import com.termux.app.TermuxService;
|
||||
import com.termux.app.settings.properties.TermuxPropertyConstants;
|
||||
import com.termux.app.terminal.io.BellHandler;
|
||||
import com.termux.app.utils.Logger;
|
||||
import com.termux.terminal.TerminalColors;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
import com.termux.terminal.TextStyle;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
public class TermuxSessionClient extends TermuxSessionClientBase {
|
||||
|
||||
final TermuxActivity mActivity;
|
||||
|
||||
private static final int MAX_SESSIONS = 8;
|
||||
|
||||
final SoundPool mBellSoundPool = new SoundPool.Builder().setMaxStreams(1).setAudioAttributes(
|
||||
new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||
.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build()).build();
|
||||
|
||||
int mBellSoundId;
|
||||
|
||||
private static final String LOG_TAG = "TermuxSessionClient";
|
||||
|
||||
public TermuxSessionClient(TermuxActivity activity) {
|
||||
this.mActivity = activity;
|
||||
|
||||
mBellSoundId = mBellSoundPool.load(activity, R.raw.bell, 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(TerminalSession changedSession) {
|
||||
if (!mActivity.isVisible()) return;
|
||||
|
||||
if (mActivity.getCurrentSession() == changedSession) mActivity.getTerminalView().onScreenUpdated();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTitleChanged(TerminalSession updatedSession) {
|
||||
if (!mActivity.isVisible()) return;
|
||||
|
||||
if (updatedSession != mActivity.getCurrentSession()) {
|
||||
// Only show toast for other sessions than the current one, since the user
|
||||
// probably consciously caused the title change to change in the current session
|
||||
// and don't want an annoying toast for that.
|
||||
mActivity.showToast(toToastTitle(updatedSession), true);
|
||||
}
|
||||
|
||||
mActivity.terminalSessionListNotifyUpdated();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSessionFinished(final TerminalSession finishedSession) {
|
||||
if (mActivity.getTermuxService().wantsToStop()) {
|
||||
// The service wants to stop as soon as possible.
|
||||
mActivity.finishActivityIfNotFinishing();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mActivity.isVisible() && finishedSession != mActivity.getCurrentSession()) {
|
||||
// Show toast for non-current sessions that exit.
|
||||
int indexOfSession = mActivity.getTermuxService().getSessions().indexOf(finishedSession);
|
||||
// Verify that session was not removed before we got told about it finishing:
|
||||
if (indexOfSession >= 0)
|
||||
mActivity.showToast(toToastTitle(finishedSession) + " - exited", true);
|
||||
}
|
||||
|
||||
if (mActivity.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
|
||||
// On Android TV devices we need to use older behaviour because we may
|
||||
// not be able to have multiple launcher icons.
|
||||
if (mActivity.getTermuxService().getSessions().size() > 1) {
|
||||
removeFinishedSession(finishedSession);
|
||||
}
|
||||
} else {
|
||||
// Once we have a separate launcher icon for the failsafe session, it
|
||||
// should be safe to auto-close session on exit code '0' or '130'.
|
||||
if (finishedSession.getExitStatus() == 0 || finishedSession.getExitStatus() == 130) {
|
||||
removeFinishedSession(finishedSession);
|
||||
}
|
||||
}
|
||||
|
||||
mActivity.terminalSessionListNotifyUpdated();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClipboardText(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)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBell(TerminalSession session) {
|
||||
if (!mActivity.isVisible()) return;
|
||||
|
||||
switch (mActivity.getProperties().getBellBehaviour()) {
|
||||
case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_VIBRATE:
|
||||
BellHandler.getInstance(mActivity).doBell();
|
||||
break;
|
||||
case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_BEEP:
|
||||
mBellSoundPool.play(mBellSoundId, 1.f, 1.f, 1, 0, 1.f);
|
||||
break;
|
||||
case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_IGNORE:
|
||||
// Ignore the bell character.
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onColorsChanged(TerminalSession changedSession) {
|
||||
if (mActivity.getCurrentSession() == changedSession)
|
||||
updateBackgroundColor();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** Try switching to session and note about it, but do nothing if already displaying the session. */
|
||||
public void setCurrentSession(TerminalSession session) {
|
||||
if (mActivity.getTerminalView().attachSession(session)) {
|
||||
noteSessionInfo();
|
||||
updateBackgroundColor();
|
||||
}
|
||||
}
|
||||
|
||||
void noteSessionInfo() {
|
||||
if (!mActivity.isVisible()) return;
|
||||
|
||||
TerminalSession session = mActivity.getCurrentSession();
|
||||
final int indexOfSession = mActivity.getTermuxService().getSessions().indexOf(session);
|
||||
mActivity.showToast(toToastTitle(session), false);
|
||||
mActivity.terminalSessionListNotifyUpdated();
|
||||
final ListView lv = mActivity.findViewById(R.id.terminal_sessions_list);
|
||||
lv.setItemChecked(indexOfSession, true);
|
||||
lv.smoothScrollToPosition(indexOfSession);
|
||||
}
|
||||
|
||||
public void switchToSession(boolean forward) {
|
||||
TermuxService service = mActivity.getTermuxService();
|
||||
|
||||
TerminalSession currentSession = mActivity.getCurrentSession();
|
||||
int index = service.getSessions().indexOf(currentSession);
|
||||
if (forward) {
|
||||
if (++index >= service.getSessions().size()) index = 0;
|
||||
} else {
|
||||
if (--index < 0) index = service.getSessions().size() - 1;
|
||||
}
|
||||
setCurrentSession(service.getSessions().get(index));
|
||||
}
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
public void renameSession(final TerminalSession sessionToRename) {
|
||||
if (sessionToRename == null) return;
|
||||
|
||||
DialogUtils.textInput(mActivity, R.string.session_rename_title, sessionToRename.mSessionName, R.string.session_rename_positive_button, text -> {
|
||||
sessionToRename.mSessionName = text;
|
||||
mActivity.terminalSessionListNotifyUpdated();
|
||||
}, -1, null, -1, null, null);
|
||||
}
|
||||
|
||||
public void addNewSession(boolean failSafe, String sessionName) {
|
||||
if (mActivity.getTermuxService().getSessions().size() >= MAX_SESSIONS) {
|
||||
new AlertDialog.Builder(mActivity).setTitle(R.string.max_terminals_reached_title).setMessage(R.string.max_terminals_reached_message)
|
||||
.setPositiveButton(android.R.string.ok, null).show();
|
||||
} else {
|
||||
TerminalSession currentSession = mActivity.getCurrentSession();
|
||||
|
||||
String workingDirectory;
|
||||
if (currentSession == null) {
|
||||
workingDirectory = mActivity.getProperties().getDefaultWorkingDirectory();
|
||||
} else {
|
||||
workingDirectory = currentSession.getCwd();
|
||||
}
|
||||
|
||||
TerminalSession newSession = mActivity.getTermuxService().createTermSession(null, null, workingDirectory, failSafe);
|
||||
if (sessionName != null) {
|
||||
newSession.mSessionName = sessionName;
|
||||
}
|
||||
setCurrentSession(newSession);
|
||||
mActivity.getDrawer().closeDrawers();
|
||||
}
|
||||
}
|
||||
|
||||
public void setCurrentStoredSession() {
|
||||
TerminalSession currentSession = mActivity.getCurrentSession();
|
||||
if (currentSession != null)
|
||||
mActivity.getPreferences().setCurrentSession(currentSession.mHandle);
|
||||
else
|
||||
mActivity.getPreferences().setCurrentSession(null);
|
||||
}
|
||||
|
||||
/** The current session as stored or the last one if that does not exist. */
|
||||
public TerminalSession getCurrentStoredSessionOrLast() {
|
||||
TerminalSession stored = getCurrentStoredSession(mActivity);
|
||||
|
||||
if (stored != null) {
|
||||
// If a stored session is in the list of currently running sessions, then return it
|
||||
return stored;
|
||||
} else {
|
||||
// Else return the last session currently running
|
||||
List<TerminalSession> sessions = mActivity.getTermuxService().getSessions();
|
||||
return sessions.isEmpty() ? null : sessions.get(sessions.size() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
private TerminalSession getCurrentStoredSession(TermuxActivity context) {
|
||||
String sessionHandle = mActivity.getPreferences().getCurrentSession();
|
||||
|
||||
// If no session is stored in shared preferences
|
||||
if(sessionHandle == null)
|
||||
return null;
|
||||
|
||||
// Check if the session handle found matches one of the currently running sessions
|
||||
List<TerminalSession> sessions = context.getTermuxService().getSessions();
|
||||
for (int i = 0, len = sessions.size(); i < len; i++) {
|
||||
TerminalSession session = sessions.get(i);
|
||||
if (session.mHandle.equals(sessionHandle))
|
||||
return session;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void removeFinishedSession(TerminalSession finishedSession) {
|
||||
// Return pressed with finished session - remove it.
|
||||
TermuxService service = mActivity.getTermuxService();
|
||||
|
||||
int index = service.removeTermSession(finishedSession);
|
||||
mActivity.terminalSessionListNotifyUpdated();
|
||||
if (mActivity.getTermuxService().getSessions().isEmpty()) {
|
||||
// There are no sessions to show, so finish the activity.
|
||||
mActivity.finishActivityIfNotFinishing();
|
||||
} else {
|
||||
if (index >= service.getSessions().size()) {
|
||||
index = service.getSessions().size() - 1;
|
||||
}
|
||||
setCurrentSession(service.getSessions().get(index));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
String toToastTitle(TerminalSession session) {
|
||||
final int indexOfSession = mActivity.getTermuxService().getSessions().indexOf(session);
|
||||
StringBuilder toastTitle = new StringBuilder("[" + (indexOfSession + 1) + "]");
|
||||
if (!TextUtils.isEmpty(session.mSessionName)) {
|
||||
toastTitle.append(" ").append(session.mSessionName);
|
||||
}
|
||||
String title = session.getTitle();
|
||||
if (!TextUtils.isEmpty(title)) {
|
||||
// Space to "[${NR}] or newline after session name:
|
||||
toastTitle.append(session.mSessionName == null ? " " : "\n");
|
||||
toastTitle.append(title);
|
||||
}
|
||||
return toastTitle.toString();
|
||||
}
|
||||
|
||||
|
||||
public void checkForFontAndColors() {
|
||||
try {
|
||||
File colorsFile = TermuxConstants.TERMUX_COLOR_PROPERTIES_FILE;
|
||||
File fontFile = TermuxConstants.TERMUX_FONT_FILE;
|
||||
|
||||
final Properties props = new Properties();
|
||||
if (colorsFile.isFile()) {
|
||||
try (InputStream in = new FileInputStream(colorsFile)) {
|
||||
props.load(in);
|
||||
}
|
||||
}
|
||||
|
||||
TerminalColors.COLOR_SCHEME.updateWith(props);
|
||||
TerminalSession session = mActivity.getCurrentSession();
|
||||
if (session != null && session.getEmulator() != null) {
|
||||
session.getEmulator().mColors.reset();
|
||||
}
|
||||
updateBackgroundColor();
|
||||
|
||||
final Typeface newTypeface = (fontFile.exists() && fontFile.length() > 0) ? Typeface.createFromFile(fontFile) : Typeface.MONOSPACE;
|
||||
mActivity.getTerminalView().setTypeface(newTypeface);
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Error in checkForFontAndColors()", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void updateBackgroundColor() {
|
||||
TerminalSession session = mActivity.getCurrentSession();
|
||||
if (session != null && session.getEmulator() != null) {
|
||||
mActivity.getWindow().getDecorView().setBackgroundColor(session.getEmulator().mColors.mCurrentColors[TextStyle.COLOR_INDEX_BACKGROUND]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,71 @@
|
||||
package com.termux.app.terminal;
|
||||
|
||||
import com.termux.app.utils.Logger;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
import com.termux.terminal.TerminalSessionClient;
|
||||
|
||||
public class TermuxSessionClientBase implements TerminalSessionClient {
|
||||
|
||||
public TermuxSessionClientBase() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(TerminalSession changedSession) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTitleChanged(TerminalSession updatedSession) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSessionFinished(final TerminalSession finishedSession) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClipboardText(TerminalSession session, String text) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBell(TerminalSession session) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onColorsChanged(TerminalSession changedSession) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logError(String tag, String message) {
|
||||
Logger.logError(tag, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logWarn(String tag, String message) {
|
||||
Logger.logWarn(tag, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logInfo(String tag, String message) {
|
||||
Logger.logInfo(tag, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logDebug(String tag, String message) {
|
||||
Logger.logDebug(tag, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logVerbose(String tag, String message) {
|
||||
Logger.logVerbose(tag, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logStackTraceWithMessage(String tag, String message, Exception e) {
|
||||
Logger.logStackTraceWithMessage(tag, message, e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logStackTrace(String tag, Exception e) {
|
||||
Logger.logStackTrace(tag, e);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,107 @@
|
||||
package com.termux.app.terminal;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Typeface;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.StyleSpan;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.app.TermuxActivity;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class TermuxSessionsListViewController extends ArrayAdapter<TerminalSession> implements AdapterView.OnItemClickListener, AdapterView.OnItemLongClickListener {
|
||||
|
||||
final TermuxActivity mActivity;
|
||||
|
||||
final StyleSpan boldSpan = new StyleSpan(Typeface.BOLD);
|
||||
final StyleSpan italicSpan = new StyleSpan(Typeface.ITALIC);
|
||||
|
||||
public TermuxSessionsListViewController(TermuxActivity activity, List<TerminalSession> sessionList) {
|
||||
super(activity.getApplicationContext(), R.layout.terminal_sessions_list_item, sessionList);
|
||||
this.mActivity = activity;
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
@NonNull
|
||||
@Override
|
||||
public View getView(int position, View convertView, @NonNull ViewGroup parent) {
|
||||
View sessionRowView = convertView;
|
||||
if (sessionRowView == null) {
|
||||
LayoutInflater inflater = mActivity.getLayoutInflater();
|
||||
sessionRowView = inflater.inflate(R.layout.terminal_sessions_list_item, parent, false);
|
||||
}
|
||||
|
||||
TextView sessionTitleView = sessionRowView.findViewById(R.id.session_title);
|
||||
|
||||
TerminalSession sessionAtRow = getItem(position);
|
||||
if (sessionAtRow == null) {
|
||||
sessionTitleView.setText("null session");
|
||||
return sessionRowView;
|
||||
}
|
||||
|
||||
boolean isUsingBlackUI = mActivity.getProperties().isUsingBlackUI();
|
||||
|
||||
if (isUsingBlackUI) {
|
||||
sessionTitleView.setBackground(
|
||||
ContextCompat.getDrawable(mActivity, R.drawable.selected_session_background_black)
|
||||
);
|
||||
}
|
||||
|
||||
String name = sessionAtRow.mSessionName;
|
||||
String sessionTitle = sessionAtRow.getTitle();
|
||||
|
||||
String numberPart = "[" + (position + 1) + "] ";
|
||||
String sessionNamePart = (TextUtils.isEmpty(name) ? "" : name);
|
||||
String sessionTitlePart = (TextUtils.isEmpty(sessionTitle) ? "" : ((sessionNamePart.isEmpty() ? "" : "\n") + sessionTitle));
|
||||
|
||||
String fullSessionTitle = numberPart + sessionNamePart + sessionTitlePart;
|
||||
SpannableString fullSessionTitleStyled = new SpannableString(fullSessionTitle);
|
||||
fullSessionTitleStyled.setSpan(boldSpan, 0, numberPart.length() + sessionNamePart.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
fullSessionTitleStyled.setSpan(italicSpan, numberPart.length() + sessionNamePart.length(), fullSessionTitle.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
sessionTitleView.setText(fullSessionTitleStyled);
|
||||
|
||||
boolean sessionRunning = sessionAtRow.isRunning();
|
||||
|
||||
if (sessionRunning) {
|
||||
sessionTitleView.setPaintFlags(sessionTitleView.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG);
|
||||
} else {
|
||||
sessionTitleView.setPaintFlags(sessionTitleView.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
|
||||
}
|
||||
int defaultColor = isUsingBlackUI ? Color.WHITE : Color.BLACK;
|
||||
int color = sessionRunning || sessionAtRow.getExitStatus() == 0 ? defaultColor : Color.RED;
|
||||
sessionTitleView.setTextColor(color);
|
||||
return sessionRowView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
TerminalSession clickedSession = getItem(position);
|
||||
mActivity.getTermuxSessionClient().setCurrentSession(clickedSession);
|
||||
mActivity.getDrawer().closeDrawers();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
final TerminalSession selectedSession = getItem(position);
|
||||
mActivity.getTermuxSessionClient().renameSession(selectedSession);
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
@@ -1,45 +1,62 @@
|
||||
package com.termux.app.terminal;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.AlertDialog;
|
||||
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.text.TextUtils;
|
||||
import android.view.Gravity;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.ListView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.app.TermuxActivity;
|
||||
import com.termux.app.TermuxService;
|
||||
import com.termux.app.terminal.extrakeys.ExtraKeysView;
|
||||
import com.termux.app.terminal.io.KeyboardShortcut;
|
||||
import com.termux.app.terminal.io.extrakeys.ExtraKeysView;
|
||||
import com.termux.app.settings.properties.TermuxPropertyConstants;
|
||||
import com.termux.app.utils.TextDataUtils;
|
||||
import com.termux.app.utils.Logger;
|
||||
import com.termux.terminal.KeyHandler;
|
||||
import com.termux.terminal.TerminalEmulator;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
import com.termux.view.TerminalViewClient;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
|
||||
public final class TermuxViewClient implements TerminalViewClient {
|
||||
public class TermuxViewClient implements TerminalViewClient {
|
||||
|
||||
final TermuxActivity mActivity;
|
||||
|
||||
final TermuxSessionClient mTermuxSessionClient;
|
||||
|
||||
/** Keeping track of the special keys acting as Ctrl and Fn for the soft keyboard and other hardware keys. */
|
||||
boolean mVirtualControlKeyDown, mVirtualFnKeyDown;
|
||||
|
||||
public TermuxViewClient(TermuxActivity activity) {
|
||||
public TermuxViewClient(TermuxActivity activity, TermuxSessionClient termuxSessionClient) {
|
||||
this.mActivity = activity;
|
||||
this.mTermuxSessionClient = termuxSessionClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float onScale(float scale) {
|
||||
if (scale < 0.9f || scale > 1.1f) {
|
||||
boolean increase = scale > 1.f;
|
||||
mActivity.changeFontSize(increase);
|
||||
changeFontSize(increase);
|
||||
return 1.0f;
|
||||
}
|
||||
return scale;
|
||||
@@ -84,16 +101,16 @@ public final class TermuxViewClient implements TerminalViewClient {
|
||||
if (handleVirtualKeys(keyCode, e, true)) return true;
|
||||
|
||||
if (keyCode == KeyEvent.KEYCODE_ENTER && !currentSession.isRunning()) {
|
||||
mActivity.removeFinishedSession(currentSession);
|
||||
mTermuxSessionClient.removeFinishedSession(currentSession);
|
||||
return true;
|
||||
} else if (e.isCtrlPressed() && e.isAltPressed()) {
|
||||
// Get the unmodified code point:
|
||||
int unicodeChar = e.getUnicodeChar(0);
|
||||
|
||||
if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN || unicodeChar == 'n'/* next */) {
|
||||
mActivity.switchToSession(true);
|
||||
mTermuxSessionClient.switchToSession(true);
|
||||
} else if (keyCode == KeyEvent.KEYCODE_DPAD_UP || unicodeChar == 'p' /* previous */) {
|
||||
mActivity.switchToSession(false);
|
||||
mTermuxSessionClient.switchToSession(false);
|
||||
} else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
|
||||
mActivity.getDrawer().openDrawer(Gravity.LEFT);
|
||||
} else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
|
||||
@@ -104,24 +121,24 @@ public final class TermuxViewClient implements TerminalViewClient {
|
||||
} else if (unicodeChar == 'm'/* menu */) {
|
||||
mActivity.getTerminalView().showContextMenu();
|
||||
} else if (unicodeChar == 'r'/* rename */) {
|
||||
mActivity.renameSession(currentSession);
|
||||
mTermuxSessionClient.renameSession(currentSession);
|
||||
} else if (unicodeChar == 'c'/* create */) {
|
||||
mActivity.addNewSession(false, null);
|
||||
mTermuxSessionClient.addNewSession(false, null);
|
||||
} else if (unicodeChar == 'u' /* urls */) {
|
||||
mActivity.showUrlSelection();
|
||||
showUrlSelection();
|
||||
} else if (unicodeChar == 'v') {
|
||||
mActivity.doPaste();
|
||||
doPaste();
|
||||
} else if (unicodeChar == '+' || e.getUnicodeChar(KeyEvent.META_SHIFT_ON) == '+') {
|
||||
// We also check for the shifted char here since shift may be required to produce '+',
|
||||
// see https://github.com/termux/termux-api/issues/2
|
||||
mActivity.changeFontSize(true);
|
||||
changeFontSize(true);
|
||||
} else if (unicodeChar == '-') {
|
||||
mActivity.changeFontSize(false);
|
||||
changeFontSize(false);
|
||||
} else if (unicodeChar >= '1' && unicodeChar <= '9') {
|
||||
int num = unicodeChar - '1';
|
||||
TermuxService service = mActivity.getTermService();
|
||||
TermuxService service = mActivity.getTermuxService();
|
||||
if (service.getSessions().size() > num)
|
||||
mActivity.switchToSession(service.getSessions().get(num));
|
||||
mTermuxSessionClient.setCurrentSession(service.getSessions().get(num));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -262,7 +279,7 @@ public final class TermuxViewClient implements TerminalViewClient {
|
||||
// Writing mode:
|
||||
case 'q':
|
||||
case 'k':
|
||||
mActivity.toggleShowExtraKeys();
|
||||
mActivity.toggleTerminalToolbar();
|
||||
mVirtualFnKeyDown=false; // force disable fn key down to restore keyboard input into terminal view, fixes termux/termux-app#1420
|
||||
break;
|
||||
}
|
||||
@@ -276,7 +293,7 @@ public final class TermuxViewClient implements TerminalViewClient {
|
||||
return true;
|
||||
} else if (ctrlDown) {
|
||||
if (codePoint == 106 /* Ctrl+j or \n */ && !session.isRunning()) {
|
||||
mActivity.removeFinishedSession(session);
|
||||
mTermuxSessionClient.removeFinishedSession(session);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -288,16 +305,16 @@ public final class TermuxViewClient implements TerminalViewClient {
|
||||
if (codePointLowerCase == shortcut.codePoint) {
|
||||
switch (shortcut.shortcutAction) {
|
||||
case TermuxPropertyConstants.ACTION_SHORTCUT_CREATE_SESSION:
|
||||
mActivity.addNewSession(false, null);
|
||||
mTermuxSessionClient.addNewSession(false, null);
|
||||
return true;
|
||||
case TermuxPropertyConstants.ACTION_SHORTCUT_NEXT_SESSION:
|
||||
mActivity.switchToSession(true);
|
||||
mTermuxSessionClient.switchToSession(true);
|
||||
return true;
|
||||
case TermuxPropertyConstants.ACTION_SHORTCUT_PREVIOUS_SESSION:
|
||||
mActivity.switchToSession(false);
|
||||
mTermuxSessionClient.switchToSession(false);
|
||||
return true;
|
||||
case TermuxPropertyConstants.ACTION_SHORTCUT_RENAME_SESSION:
|
||||
mActivity.renameSession(mActivity.getCurrentTermSession());
|
||||
mTermuxSessionClient.renameSession(mActivity.getCurrentSession());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -310,6 +327,90 @@ public final class TermuxViewClient implements TerminalViewClient {
|
||||
|
||||
|
||||
|
||||
public void changeFontSize(boolean increase) {
|
||||
mActivity.getPreferences().changeFontSize(mActivity, increase);
|
||||
mActivity.getTerminalView().setTextSize(mActivity.getPreferences().getFontSize());
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void shareSessionTranscript() {
|
||||
TerminalSession session = mActivity.getCurrentSession();
|
||||
if (session == null) return;
|
||||
|
||||
String transcriptText = session.getEmulator().getScreen().getTranscriptTextWithoutJoinedLines().trim();
|
||||
|
||||
try {
|
||||
// See https://github.com/termux/termux-app/issues/1166.
|
||||
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||
intent.setType("text/plain");
|
||||
transcriptText = TextDataUtils.getTruncatedCommandOutput(transcriptText, 100_000);
|
||||
intent.putExtra(Intent.EXTRA_TEXT, transcriptText);
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT, mActivity.getString(R.string.share_transcript_title));
|
||||
mActivity.startActivity(Intent.createChooser(intent, mActivity.getString(R.string.share_transcript_chooser_title)));
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage("Failed to get share session transcript of length " + transcriptText.length(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public void showUrlSelection() {
|
||||
TerminalSession session = mActivity.getCurrentSession();
|
||||
if (session == null) return;
|
||||
|
||||
String text = session.getEmulator().getScreen().getTranscriptTextWithFullLinesJoined();
|
||||
|
||||
LinkedHashSet<CharSequence> urlSet = TextDataUtils.extractUrls(text);
|
||||
if (urlSet.isEmpty()) {
|
||||
new AlertDialog.Builder(mActivity).setMessage(R.string.select_url_no_found).show();
|
||||
return;
|
||||
}
|
||||
|
||||
final CharSequence[] urls = urlSet.toArray(new CharSequence[0]);
|
||||
Collections.reverse(Arrays.asList(urls)); // Latest first.
|
||||
|
||||
// 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.select_url_copied_to_clipboard, Toast.LENGTH_LONG).show();
|
||||
}).setTitle(R.string.select_url_dialog_title).create();
|
||||
|
||||
// Long press to open URL:
|
||||
dialog.setOnShowListener(di -> {
|
||||
ListView lv = dialog.getListView(); // this is a ListView with your "buds" in it
|
||||
lv.setOnItemLongClickListener((parent, view, position, id) -> {
|
||||
dialog.dismiss();
|
||||
String url = (String) urls[position];
|
||||
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||
try {
|
||||
mActivity.startActivity(i, null);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
// If no applications match, Android displays a system message.
|
||||
mActivity.startActivity(Intent.createChooser(i, null));
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
public void doPaste() {
|
||||
TerminalSession session = mActivity.getCurrentSession();
|
||||
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());
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void logError(String tag, String message) {
|
||||
Logger.logError(tag, message);
|
||||
|
@@ -0,0 +1,114 @@
|
||||
package com.termux.app.terminal.io;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.viewpager.widget.PagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.app.TermuxActivity;
|
||||
import com.termux.app.terminal.io.extrakeys.ExtraKeysView;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
|
||||
public class TerminalToolbarViewPager {
|
||||
|
||||
public static class PageAdapter extends PagerAdapter {
|
||||
|
||||
final TermuxActivity mActivity;
|
||||
String mSavedTextInput;
|
||||
|
||||
public PageAdapter(TermuxActivity activity, String savedTextInput) {
|
||||
this.mActivity = activity;
|
||||
this.mSavedTextInput = savedTextInput;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
|
||||
return view == object;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Object instantiateItem(@NonNull ViewGroup collection, int position) {
|
||||
LayoutInflater inflater = LayoutInflater.from(mActivity);
|
||||
View layout;
|
||||
if (position == 0) {
|
||||
layout = inflater.inflate(R.layout.terminal_toolbar_extra_keys_view, collection, false);
|
||||
ExtraKeysView extraKeysView = (ExtraKeysView) layout;
|
||||
mActivity.setExtraKeysView(extraKeysView);
|
||||
extraKeysView.reload(mActivity.getProperties().getExtraKeysInfo());
|
||||
|
||||
// apply extra keys fix if enabled in prefs
|
||||
if (mActivity.getProperties().isUsingFullScreen() && mActivity.getProperties().isUsingFullScreenWorkAround()) {
|
||||
FullScreenWorkAround.apply(mActivity);
|
||||
}
|
||||
|
||||
} else {
|
||||
layout = inflater.inflate(R.layout.terminal_toolbar_text_input_view, collection, false);
|
||||
final EditText editText = layout.findViewById(R.id.terminal_toolbar_text_input);
|
||||
|
||||
if(mSavedTextInput != null) {
|
||||
editText.setText(mSavedTextInput);
|
||||
mSavedTextInput = null;
|
||||
}
|
||||
|
||||
editText.setOnEditorActionListener((v, actionId, event) -> {
|
||||
TerminalSession session = mActivity.getCurrentSession();
|
||||
if (session != null) {
|
||||
if (session.isRunning()) {
|
||||
String textToSend = editText.getText().toString();
|
||||
if (textToSend.length() == 0) textToSend = "\r";
|
||||
session.write(textToSend);
|
||||
} else {
|
||||
mActivity.getTermuxSessionClient().removeFinishedSession(session);
|
||||
}
|
||||
editText.setText("");
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
collection.addView(layout);
|
||||
return layout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyItem(@NonNull ViewGroup collection, int position, @NonNull Object view) {
|
||||
collection.removeView((View) view);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static class OnPageChangeListener extends ViewPager.SimpleOnPageChangeListener {
|
||||
|
||||
final TermuxActivity mActivity;
|
||||
final ViewPager mTerminalToolbarViewPager;
|
||||
|
||||
public OnPageChangeListener(TermuxActivity activity, ViewPager viewPager) {
|
||||
this.mActivity = activity;
|
||||
this.mTerminalToolbarViewPager = viewPager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageSelected(int position) {
|
||||
if (position == 0) {
|
||||
mActivity.getTerminalView().requestFocus();
|
||||
} else {
|
||||
final EditText editText = mTerminalToolbarViewPager.findViewById(R.id.terminal_toolbar_text_input);
|
||||
if (editText != null) editText.requestFocus();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -1,9 +1,14 @@
|
||||
package com.termux.app.utils;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ResolveInfo;
|
||||
|
||||
import com.termux.app.TermuxConstants;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class TermuxUtils {
|
||||
|
||||
public static Context getTermuxPackageContext(Context context) {
|
||||
@@ -15,4 +20,26 @@ public class TermuxUtils {
|
||||
return context;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the {@link TermuxConstants#BROADCAST_TERMUX_OPENED} broadcast to notify apps that Termux
|
||||
* app has been opened.
|
||||
*
|
||||
* @param context The Context to send the broadcast.
|
||||
*/
|
||||
public static void sendTermuxOpenedBroadcast(Context context) {
|
||||
Intent broadcast = new Intent(TermuxConstants.BROADCAST_TERMUX_OPENED);
|
||||
List<ResolveInfo> matches = context.getPackageManager().queryBroadcastReceivers(broadcast, 0);
|
||||
|
||||
// send broadcast to registered Termux receivers
|
||||
// this technique is needed to work around broadcast changes that Oreo introduced
|
||||
for (ResolveInfo info : matches) {
|
||||
Intent explicitBroadcast = new Intent(broadcast);
|
||||
ComponentName cname = new ComponentName(info.activityInfo.applicationInfo.packageName,
|
||||
info.activityInfo.name);
|
||||
explicitBroadcast.setComponent(cname);
|
||||
context.sendBroadcast(explicitBroadcast);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
111
app/src/main/java/com/termux/app/utils/TextDataUtils.java
Normal file
111
app/src/main/java/com/termux/app/utils/TextDataUtils.java
Normal file
@@ -0,0 +1,111 @@
|
||||
package com.termux.app.utils;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class TextDataUtils {
|
||||
|
||||
// https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:media2/media2-session/src/main/java/androidx/media2/session/MediaUtils.java
|
||||
public static final int TRANSACTION_SIZE_LIMIT_IN_BYTES = 256 * 1024; // 256KB
|
||||
|
||||
public static String getTruncatedCommandOutput(String text, int maxLength) {
|
||||
if (text.length() > maxLength) {
|
||||
int cutOffIndex = text.length() - maxLength;
|
||||
int nextNewlineIndex = text.indexOf('\n', cutOffIndex);
|
||||
if (nextNewlineIndex != -1 && nextNewlineIndex != text.length() - 1) {
|
||||
cutOffIndex = nextNewlineIndex + 1;
|
||||
}
|
||||
text = text.substring(cutOffIndex).trim();
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
public static LinkedHashSet<CharSequence> extractUrls(String text) {
|
||||
|
||||
StringBuilder regex_sb = new StringBuilder();
|
||||
|
||||
regex_sb.append("("); // Begin first matching group.
|
||||
regex_sb.append("(?:"); // Begin scheme group.
|
||||
regex_sb.append("dav|"); // The DAV proto.
|
||||
regex_sb.append("dict|"); // The DICT proto.
|
||||
regex_sb.append("dns|"); // The DNS proto.
|
||||
regex_sb.append("file|"); // File path.
|
||||
regex_sb.append("finger|"); // The Finger proto.
|
||||
regex_sb.append("ftp(?:s?)|"); // The FTP proto.
|
||||
regex_sb.append("git|"); // The Git proto.
|
||||
regex_sb.append("gopher|"); // The Gopher proto.
|
||||
regex_sb.append("http(?:s?)|"); // The HTTP proto.
|
||||
regex_sb.append("imap(?:s?)|"); // The IMAP proto.
|
||||
regex_sb.append("irc(?:[6s]?)|"); // The IRC proto.
|
||||
regex_sb.append("ip[fn]s|"); // The IPFS proto.
|
||||
regex_sb.append("ldap(?:s?)|"); // The LDAP proto.
|
||||
regex_sb.append("pop3(?:s?)|"); // The POP3 proto.
|
||||
regex_sb.append("redis(?:s?)|"); // The Redis proto.
|
||||
regex_sb.append("rsync|"); // The Rsync proto.
|
||||
regex_sb.append("rtsp(?:[su]?)|"); // The RTSP proto.
|
||||
regex_sb.append("sftp|"); // The SFTP proto.
|
||||
regex_sb.append("smb(?:s?)|"); // The SAMBA proto.
|
||||
regex_sb.append("smtp(?:s?)|"); // The SMTP proto.
|
||||
regex_sb.append("svn(?:(?:\\+ssh)?)|"); // The Subversion proto.
|
||||
regex_sb.append("tcp|"); // The TCP proto.
|
||||
regex_sb.append("telnet|"); // The Telnet proto.
|
||||
regex_sb.append("tftp|"); // The TFTP proto.
|
||||
regex_sb.append("udp|"); // The UDP proto.
|
||||
regex_sb.append("vnc|"); // The VNC proto.
|
||||
regex_sb.append("ws(?:s?)"); // The Websocket proto.
|
||||
regex_sb.append(")://"); // End scheme group.
|
||||
regex_sb.append(")"); // End first matching group.
|
||||
|
||||
|
||||
// Begin second matching group.
|
||||
regex_sb.append("(");
|
||||
|
||||
// User name and/or password in format 'user:pass@'.
|
||||
regex_sb.append("(?:\\S+(?::\\S*)?@)?");
|
||||
|
||||
// Begin host group.
|
||||
regex_sb.append("(?:");
|
||||
|
||||
// IP address (from http://www.regular-expressions.info/examples.html).
|
||||
regex_sb.append("(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|");
|
||||
|
||||
// Host name or domain.
|
||||
regex_sb.append("(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)(?:(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))?|");
|
||||
|
||||
// Just path. Used in case of 'file://' scheme.
|
||||
regex_sb.append("/(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)");
|
||||
|
||||
// End host group.
|
||||
regex_sb.append(")");
|
||||
|
||||
// Port number.
|
||||
regex_sb.append("(?::\\d{1,5})?");
|
||||
|
||||
// Resource path with optional query string.
|
||||
regex_sb.append("(?:/[a-zA-Z0-9:@%\\-._~!$&()*+,;=?/]*)?");
|
||||
|
||||
// Fragment.
|
||||
regex_sb.append("(?:#[a-zA-Z0-9:@%\\-._~!$&()*+,;=?/]*)?");
|
||||
|
||||
// End second matching group.
|
||||
regex_sb.append(")");
|
||||
|
||||
final Pattern urlPattern = Pattern.compile(
|
||||
regex_sb.toString(),
|
||||
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
|
||||
|
||||
LinkedHashSet<CharSequence> urlSet = new LinkedHashSet<>();
|
||||
Matcher matcher = urlPattern.matcher(text);
|
||||
|
||||
while (matcher.find()) {
|
||||
int matchStart = matcher.start(1);
|
||||
int matchEnd = matcher.end();
|
||||
String url = text.substring(matchStart, matchEnd);
|
||||
urlSet.add(url);
|
||||
}
|
||||
|
||||
return urlSet;
|
||||
}
|
||||
|
||||
}
|
9
app/src/main/res/layout/terminal_sessions_list_item.xml
Normal file
9
app/src/main/res/layout/terminal_sessions_list_item.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/session_title"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="?android:attr/listPreferredItemHeight"
|
||||
android:background="@drawable/selected_session_background"
|
||||
android:ellipsize="marquee"
|
||||
android:gravity="start|center_vertical"
|
||||
android:padding="6dip"
|
||||
android:textSize="14sp" />
|
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.termux.app.terminal.io.extrakeys.ExtraKeysView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/terminal_toolbar_extra_keys"
|
||||
style="?android:attr/buttonBarStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:orientation="horizontal" />
|
16
app/src/main/res/layout/terminal_toolbar_text_input_view.xml
Normal file
16
app/src/main/res/layout/terminal_toolbar_text_input_view.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<EditText xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/terminal_toolbar_text_input"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:imeOptions="actionSend|flagNoFullscreen"
|
||||
android:maxLines="1"
|
||||
android:inputType="text"
|
||||
android:importantForAutofill="no"
|
||||
android:textColor="@android:color/white"
|
||||
android:textColorHighlight="@android:color/darker_gray"
|
||||
android:paddingTop="0dp"
|
||||
android:textCursorDrawable="@null"
|
||||
android:paddingBottom="0dp"
|
||||
tools:ignore="LabelFor" />
|
80
app/src/main/res/layout/termux_activity.xml
Normal file
80
app/src/main/res/layout/termux_activity.xml
Normal file
@@ -0,0 +1,80 @@
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<androidx.drawerlayout.widget.DrawerLayout
|
||||
android:id="@+id/drawer_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_above="@+id/terminal_toolbar_view_pager"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.termux.view.TerminalView
|
||||
android:id="@+id/terminal_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginRight="3dp"
|
||||
android:layout_marginLeft="3dp"
|
||||
android:focusableInTouchMode="true"
|
||||
android:scrollbarThumbVertical="@drawable/terminal_scroll_shape"
|
||||
android:scrollbars="vertical"
|
||||
android:importantForAutofill="no"
|
||||
android:autofillHints="password" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/left_drawer"
|
||||
android:layout_width="240dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="start"
|
||||
android:background="@android:color/white"
|
||||
android:choiceMode="singleChoice"
|
||||
android:divider="@android:color/transparent"
|
||||
android:dividerHeight="0dp"
|
||||
android:descendantFocusability="blocksDescendants"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ListView
|
||||
android:id="@+id/terminal_sessions_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_gravity="top"
|
||||
android:layout_weight="1"
|
||||
android:choiceMode="singleChoice"
|
||||
android:longClickable="true" />
|
||||
|
||||
<LinearLayout
|
||||
style="?android:attr/buttonBarStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/toggle_keyboard_button"
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/toggle_soft_keyboard" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/new_session_button"
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/new_session" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.drawerlayout.widget.DrawerLayout>
|
||||
|
||||
<androidx.viewpager.widget.ViewPager
|
||||
android:id="@+id/terminal_toolbar_view_pager"
|
||||
android:visibility="gone"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="37.5dp"
|
||||
android:background="@android:drawable/screen_background_dark_transparent"
|
||||
android:layout_alignParentBottom="true" />
|
||||
</RelativeLayout>
|
@@ -1,5 +1,7 @@
|
||||
package com.termux.app;
|
||||
|
||||
import com.termux.app.utils.TextDataUtils;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
@@ -11,7 +13,7 @@ public class TermuxActivityTest {
|
||||
private void assertUrlsAre(String text, String... urls) {
|
||||
LinkedHashSet<String> expected = new LinkedHashSet<>();
|
||||
Collections.addAll(expected, urls);
|
||||
Assert.assertEquals(expected, TermuxActivity.extractUrls(text));
|
||||
Assert.assertEquals(expected, TextDataUtils.extractUrls(text));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@@ -1,7 +1,6 @@
|
||||
package com.termux.terminal;
|
||||
|
||||
import android.util.Base64;
|
||||
import android.util.Log;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
@@ -153,6 +152,8 @@ public final class TerminalEmulator {
|
||||
/** The terminal session this emulator is bound to. */
|
||||
private final TerminalOutput mSession;
|
||||
|
||||
TerminalSessionClient mClient;
|
||||
|
||||
/** Keeps track of the current argument of the current escape sequence. Ranges from 0 to MAX_ESCAPE_PARAMETERS-1. */
|
||||
private int mArgIndex;
|
||||
/** Holds the arguments of the current escape sequence. */
|
||||
@@ -227,6 +228,8 @@ public final class TerminalEmulator {
|
||||
|
||||
public final TerminalColors mColors = new TerminalColors();
|
||||
|
||||
private static final String LOG_TAG = "TerminalEmulator";
|
||||
|
||||
private boolean isDecsetInternalBitSet(int bit) {
|
||||
return (mCurrentDecSetFlags & bit) != 0;
|
||||
}
|
||||
@@ -279,16 +282,21 @@ public final class TerminalEmulator {
|
||||
}
|
||||
}
|
||||
|
||||
public TerminalEmulator(TerminalOutput session, int columns, int rows, int transcriptRows) {
|
||||
public TerminalEmulator(TerminalOutput session, int columns, int rows, int transcriptRows, TerminalSessionClient client) {
|
||||
mSession = session;
|
||||
mScreen = mMainBuffer = new TerminalBuffer(columns, transcriptRows, rows);
|
||||
mAltBuffer = new TerminalBuffer(columns, rows, rows);
|
||||
mClient = client;
|
||||
mRows = rows;
|
||||
mColumns = columns;
|
||||
mTabStop = new boolean[mColumns];
|
||||
reset();
|
||||
}
|
||||
|
||||
public void updateTerminalSessionClient(TerminalSessionClient client) {
|
||||
mClient = client;
|
||||
}
|
||||
|
||||
public TerminalBuffer getScreen() {
|
||||
return mScreen;
|
||||
}
|
||||
@@ -751,7 +759,7 @@ public final class TerminalEmulator {
|
||||
if (internalBit != -1) {
|
||||
value = isDecsetInternalBitSet(internalBit) ? 1 : 2; // 1=set, 2=reset.
|
||||
} else {
|
||||
Log.e(EmulatorDebug.LOG_TAG, "Got DECRQM for unrecognized private DEC mode=" + mode);
|
||||
mClient.logError(LOG_TAG, "Got DECRQM for unrecognized private DEC mode=" + mode);
|
||||
value = 0; // 0=not recognized, 3=permanently set, 4=permanently reset
|
||||
}
|
||||
}
|
||||
@@ -888,7 +896,7 @@ public final class TerminalEmulator {
|
||||
case "&8": // Undo key - ignore.
|
||||
break;
|
||||
default:
|
||||
Log.w(EmulatorDebug.LOG_TAG, "Unhandled termcap/terminfo name: '" + trans + "'");
|
||||
mClient.logWarn(LOG_TAG, "Unhandled termcap/terminfo name: '" + trans + "'");
|
||||
}
|
||||
// Respond with invalid request:
|
||||
mSession.write("\033P0+r" + part + "\033\\");
|
||||
@@ -900,12 +908,12 @@ public final class TerminalEmulator {
|
||||
mSession.write("\033P1+r" + part + "=" + hexEncoded + "\033\\");
|
||||
}
|
||||
} else {
|
||||
Log.e(EmulatorDebug.LOG_TAG, "Invalid device termcap/terminfo name of odd length: " + part);
|
||||
mClient.logError(LOG_TAG, "Invalid device termcap/terminfo name of odd length: " + part);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (LOG_ESCAPE_SEQUENCES)
|
||||
Log.e(EmulatorDebug.LOG_TAG, "Unrecognized device control string: " + dcs);
|
||||
mClient.logError(LOG_TAG, "Unrecognized device control string: " + dcs);
|
||||
}
|
||||
finishSequence();
|
||||
}
|
||||
@@ -995,7 +1003,7 @@ public final class TerminalEmulator {
|
||||
int externalBit = mArgs[i];
|
||||
int internalBit = mapDecSetBitToInternalBit(externalBit);
|
||||
if (internalBit == -1) {
|
||||
Log.w(EmulatorDebug.LOG_TAG, "Ignoring request to save/recall decset bit=" + externalBit);
|
||||
mClient.logWarn(LOG_TAG, "Ignoring request to save/recall decset bit=" + externalBit);
|
||||
} else {
|
||||
if (b == 's') {
|
||||
mSavedDecSetFlags |= internalBit;
|
||||
@@ -1182,7 +1190,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.
|
||||
Log.e(EmulatorDebug.LOG_TAG, "(ignored) CSI > MODIFY RESOURCE: " + getArg0(-1) + " to " + getArg1(-1));
|
||||
mClient.logError(LOG_TAG, "(ignored) CSI > MODIFY RESOURCE: " + getArg0(-1) + " to " + getArg1(-1));
|
||||
break;
|
||||
default:
|
||||
parseArg(b);
|
||||
@@ -1729,7 +1737,7 @@ public final class TerminalEmulator {
|
||||
int firstArg = mArgs[i + 1];
|
||||
if (firstArg == 2) {
|
||||
if (i + 4 > mArgIndex) {
|
||||
Log.w(EmulatorDebug.LOG_TAG, "Too few CSI" + code + ";2 RGB arguments");
|
||||
mClient.logWarn(LOG_TAG, "Too few CSI" + code + ";2 RGB arguments");
|
||||
} else {
|
||||
int red = mArgs[i + 2], green = mArgs[i + 3], blue = mArgs[i + 4];
|
||||
if (red < 0 || green < 0 || blue < 0 || red > 255 || green > 255 || blue > 255) {
|
||||
@@ -1754,7 +1762,7 @@ public final class TerminalEmulator {
|
||||
mBackColor = color;
|
||||
}
|
||||
} else {
|
||||
if (LOG_ESCAPE_SEQUENCES) Log.w(EmulatorDebug.LOG_TAG, "Invalid color index: " + color);
|
||||
if (LOG_ESCAPE_SEQUENCES) mClient.logWarn(LOG_TAG, "Invalid color index: " + color);
|
||||
}
|
||||
} else {
|
||||
finishSequenceAndLogError("Invalid ISO-8613-3 SGR first argument: " + firstArg);
|
||||
@@ -1771,7 +1779,7 @@ public final class TerminalEmulator {
|
||||
mBackColor = code - 100 + 8;
|
||||
} else {
|
||||
if (LOG_ESCAPE_SEQUENCES)
|
||||
Log.w(EmulatorDebug.LOG_TAG, String.format("SGR unknown code %d", code));
|
||||
mClient.logWarn(LOG_TAG, String.format("SGR unknown code %d", code));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1905,7 +1913,7 @@ public final class TerminalEmulator {
|
||||
String clipboardText = new String(Base64.decode(textParameter.substring(startIndex), 0), StandardCharsets.UTF_8);
|
||||
mSession.clipboardText(clipboardText);
|
||||
} catch (Exception e) {
|
||||
Log.e(EmulatorDebug.LOG_TAG, "OSC Manipulate selection, invalid string '" + textParameter + "");
|
||||
mClient.logError(LOG_TAG, "OSC Manipulate selection, invalid string '" + textParameter + "");
|
||||
}
|
||||
break;
|
||||
case 104:
|
||||
@@ -2101,7 +2109,7 @@ public final class TerminalEmulator {
|
||||
}
|
||||
|
||||
private void finishSequenceAndLogError(String error) {
|
||||
if (LOG_ESCAPE_SEQUENCES) Log.w(EmulatorDebug.LOG_TAG, error);
|
||||
if (LOG_ESCAPE_SEQUENCES) mClient.logWarn(LOG_TAG, error);
|
||||
finishSequence();
|
||||
}
|
||||
|
||||
|
@@ -6,7 +6,6 @@ import android.os.Message;
|
||||
import android.system.ErrnoException;
|
||||
import android.system.Os;
|
||||
import android.system.OsConstants;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileDescriptor;
|
||||
@@ -31,41 +30,6 @@ import java.util.UUID;
|
||||
*/
|
||||
public final class TerminalSession extends TerminalOutput {
|
||||
|
||||
/** Callback to be invoked when a {@link TerminalSession} changes. */
|
||||
public interface SessionChangedCallback {
|
||||
void onTextChanged(TerminalSession changedSession);
|
||||
|
||||
void onTitleChanged(TerminalSession changedSession);
|
||||
|
||||
void onSessionFinished(TerminalSession finishedSession);
|
||||
|
||||
void onClipboardText(TerminalSession session, String text);
|
||||
|
||||
void onBell(TerminalSession session);
|
||||
|
||||
void onColorsChanged(TerminalSession session);
|
||||
|
||||
}
|
||||
|
||||
private static FileDescriptor wrapFileDescriptor(int fileDescriptor) {
|
||||
FileDescriptor result = new FileDescriptor();
|
||||
try {
|
||||
Field descriptorField;
|
||||
try {
|
||||
descriptorField = FileDescriptor.class.getDeclaredField("descriptor");
|
||||
} catch (NoSuchFieldException e) {
|
||||
// For desktop java:
|
||||
descriptorField = FileDescriptor.class.getDeclaredField("fd");
|
||||
}
|
||||
descriptorField.setAccessible(true);
|
||||
descriptorField.set(result, fileDescriptor);
|
||||
} catch (NoSuchFieldException | IllegalAccessException | IllegalArgumentException e) {
|
||||
Log.wtf(EmulatorDebug.LOG_TAG, "Error accessing FileDescriptor#descriptor private field", e);
|
||||
System.exit(1);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static final int MSG_NEW_INPUT = 1;
|
||||
private static final int MSG_PROCESS_EXITED = 4;
|
||||
|
||||
@@ -87,7 +51,7 @@ public final class TerminalSession extends TerminalOutput {
|
||||
private final byte[] mUtf8InputBuffer = new byte[5];
|
||||
|
||||
/** Callback which gets notified when a session finishes or changes title. */
|
||||
final SessionChangedCallback mChangeCallback;
|
||||
TerminalSessionClient mClient;
|
||||
|
||||
/** The pid of the shell process. 0 if not started and -1 if finished running. */
|
||||
int mShellPid;
|
||||
@@ -104,52 +68,32 @@ public final class TerminalSession extends TerminalOutput {
|
||||
/** Set by the application for user identification of session, not by terminal. */
|
||||
public String mSessionName;
|
||||
|
||||
@SuppressLint("HandlerLeak")
|
||||
final Handler mMainThreadHandler = new Handler() {
|
||||
final byte[] mReceiveBuffer = new byte[4 * 1024];
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
int bytesRead = mProcessToTerminalIOQueue.read(mReceiveBuffer, false);
|
||||
if (bytesRead > 0) {
|
||||
mEmulator.append(mReceiveBuffer, bytesRead);
|
||||
notifyScreenUpdate();
|
||||
}
|
||||
|
||||
if (msg.what == MSG_PROCESS_EXITED) {
|
||||
int exitCode = (Integer) msg.obj;
|
||||
cleanupResources(exitCode);
|
||||
mChangeCallback.onSessionFinished(TerminalSession.this);
|
||||
|
||||
String exitDescription = "\r\n[Process completed";
|
||||
if (exitCode > 0) {
|
||||
// Non-zero process exit.
|
||||
exitDescription += " (code " + exitCode + ")";
|
||||
} else if (exitCode < 0) {
|
||||
// Negated signal.
|
||||
exitDescription += " (signal " + (-exitCode) + ")";
|
||||
}
|
||||
exitDescription += " - press Enter]";
|
||||
|
||||
byte[] bytesToWrite = exitDescription.getBytes(StandardCharsets.UTF_8);
|
||||
mEmulator.append(bytesToWrite, bytesToWrite.length);
|
||||
notifyScreenUpdate();
|
||||
}
|
||||
}
|
||||
};
|
||||
final Handler mMainThreadHandler = new MainThreadHandler();
|
||||
|
||||
private final String mShellPath;
|
||||
private final String mCwd;
|
||||
private final String[] mArgs;
|
||||
private final String[] mEnv;
|
||||
|
||||
public TerminalSession(String shellPath, String cwd, String[] args, String[] env, SessionChangedCallback changeCallback) {
|
||||
mChangeCallback = changeCallback;
|
||||
private static final String LOG_TAG = "TerminalSession";
|
||||
|
||||
public TerminalSession(String shellPath, String cwd, String[] args, String[] env, TerminalSessionClient client) {
|
||||
this.mShellPath = shellPath;
|
||||
this.mCwd = cwd;
|
||||
this.mArgs = args;
|
||||
this.mEnv = env;
|
||||
this.mClient = client;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param client The {@link TerminalSessionClient} interface implementation to allow
|
||||
* for communication between {@link TerminalSession} and its client.
|
||||
*/
|
||||
public void updateTerminalSessionClient(TerminalSessionClient client) {
|
||||
mClient = client;
|
||||
|
||||
if (mEmulator != null)
|
||||
mEmulator.updateTerminalSessionClient(client);
|
||||
}
|
||||
|
||||
/** Inform the attached pty of the new size and reflow or initialize the emulator. */
|
||||
@@ -174,13 +118,13 @@ public final class TerminalSession extends TerminalOutput {
|
||||
* @param rows The number of rows in the terminal window.
|
||||
*/
|
||||
public void initializeEmulator(int columns, int rows) {
|
||||
mEmulator = new TerminalEmulator(this, columns, rows, /* transcript= */2000);
|
||||
mEmulator = new TerminalEmulator(this, columns, rows, /* transcript= */2000, mClient);
|
||||
|
||||
int[] processId = new int[1];
|
||||
mTerminalFileDescriptor = JNI.createSubprocess(mShellPath, mCwd, mArgs, mEnv, processId, rows, columns);
|
||||
mShellPid = processId[0];
|
||||
|
||||
final FileDescriptor terminalFileDescriptorWrapped = wrapFileDescriptor(mTerminalFileDescriptor);
|
||||
final FileDescriptor terminalFileDescriptorWrapped = wrapFileDescriptor(mTerminalFileDescriptor, mClient);
|
||||
|
||||
new Thread("TermSessionInputReader[pid=" + mShellPid + "]") {
|
||||
@Override
|
||||
@@ -246,23 +190,23 @@ public final class TerminalSession extends TerminalOutput {
|
||||
} else if (codePoint <= /* 11 bits */0b11111111111) {
|
||||
/* 110xxxxx leading byte with leading 5 bits */
|
||||
mUtf8InputBuffer[bufferPosition++] = (byte) (0b11000000 | (codePoint >> 6));
|
||||
/* 10xxxxxx continuation byte with following 6 bits */
|
||||
/* 10xxxxxx continuation byte with following 6 bits */
|
||||
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | (codePoint & 0b111111));
|
||||
} else if (codePoint <= /* 16 bits */0b1111111111111111) {
|
||||
/* 1110xxxx leading byte with leading 4 bits */
|
||||
/* 1110xxxx leading byte with leading 4 bits */
|
||||
mUtf8InputBuffer[bufferPosition++] = (byte) (0b11100000 | (codePoint >> 12));
|
||||
/* 10xxxxxx continuation byte with following 6 bits */
|
||||
/* 10xxxxxx continuation byte with following 6 bits */
|
||||
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | ((codePoint >> 6) & 0b111111));
|
||||
/* 10xxxxxx continuation byte with following 6 bits */
|
||||
/* 10xxxxxx continuation byte with following 6 bits */
|
||||
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | (codePoint & 0b111111));
|
||||
} else { /* We have checked codePoint <= 1114111 above, so we have max 21 bits = 0b111111111111111111111 */
|
||||
/* 11110xxx leading byte with leading 3 bits */
|
||||
/* 11110xxx leading byte with leading 3 bits */
|
||||
mUtf8InputBuffer[bufferPosition++] = (byte) (0b11110000 | (codePoint >> 18));
|
||||
/* 10xxxxxx continuation byte with following 6 bits */
|
||||
/* 10xxxxxx continuation byte with following 6 bits */
|
||||
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | ((codePoint >> 12) & 0b111111));
|
||||
/* 10xxxxxx continuation byte with following 6 bits */
|
||||
/* 10xxxxxx continuation byte with following 6 bits */
|
||||
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | ((codePoint >> 6) & 0b111111));
|
||||
/* 10xxxxxx continuation byte with following 6 bits */
|
||||
/* 10xxxxxx continuation byte with following 6 bits */
|
||||
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | (codePoint & 0b111111));
|
||||
}
|
||||
write(mUtf8InputBuffer, 0, bufferPosition);
|
||||
@@ -272,9 +216,9 @@ public final class TerminalSession extends TerminalOutput {
|
||||
return mEmulator;
|
||||
}
|
||||
|
||||
/** Notify the {@link #mChangeCallback} that the screen has changed. */
|
||||
/** Notify the {@link #mClient} that the screen has changed. */
|
||||
protected void notifyScreenUpdate() {
|
||||
mChangeCallback.onTextChanged(this);
|
||||
mClient.onTextChanged(this);
|
||||
}
|
||||
|
||||
/** Reset state for terminal emulator state. */
|
||||
@@ -289,7 +233,7 @@ public final class TerminalSession extends TerminalOutput {
|
||||
try {
|
||||
Os.kill(mShellPid, OsConstants.SIGKILL);
|
||||
} catch (ErrnoException e) {
|
||||
Log.w("termux", "Failed sending SIGKILL: " + e.getMessage());
|
||||
mClient.logWarn(LOG_TAG, "Failed sending SIGKILL: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -309,7 +253,7 @@ public final class TerminalSession extends TerminalOutput {
|
||||
|
||||
@Override
|
||||
public void titleChanged(String oldTitle, String newTitle) {
|
||||
mChangeCallback.onTitleChanged(this);
|
||||
mClient.onTitleChanged(this);
|
||||
}
|
||||
|
||||
public synchronized boolean isRunning() {
|
||||
@@ -323,17 +267,17 @@ public final class TerminalSession extends TerminalOutput {
|
||||
|
||||
@Override
|
||||
public void clipboardText(String text) {
|
||||
mChangeCallback.onClipboardText(this, text);
|
||||
mClient.onClipboardText(this, text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBell() {
|
||||
mChangeCallback.onBell(this);
|
||||
mClient.onBell(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onColorsChanged() {
|
||||
mChangeCallback.onColorsChanged(this);
|
||||
mClient.onColorsChanged(this);
|
||||
}
|
||||
|
||||
public int getPid() {
|
||||
@@ -356,10 +300,64 @@ public final class TerminalSession extends TerminalOutput {
|
||||
return outputPath;
|
||||
}
|
||||
} catch (IOException | SecurityException e) {
|
||||
Log.e(EmulatorDebug.LOG_TAG, "Error getting current directory", e);
|
||||
mClient.logStackTraceWithMessage(LOG_TAG, "Error getting current directory", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static FileDescriptor wrapFileDescriptor(int fileDescriptor, TerminalSessionClient client) {
|
||||
FileDescriptor result = new FileDescriptor();
|
||||
try {
|
||||
Field descriptorField;
|
||||
try {
|
||||
descriptorField = FileDescriptor.class.getDeclaredField("descriptor");
|
||||
} catch (NoSuchFieldException e) {
|
||||
// For desktop java:
|
||||
descriptorField = FileDescriptor.class.getDeclaredField("fd");
|
||||
}
|
||||
descriptorField.setAccessible(true);
|
||||
descriptorField.set(result, fileDescriptor);
|
||||
} catch (NoSuchFieldException | IllegalAccessException | IllegalArgumentException e) {
|
||||
client.logStackTraceWithMessage(LOG_TAG, "Error accessing FileDescriptor#descriptor private field", e);
|
||||
System.exit(1);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@SuppressLint("HandlerLeak")
|
||||
class MainThreadHandler extends Handler {
|
||||
|
||||
final byte[] mReceiveBuffer = new byte[4 * 1024];
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
int bytesRead = mProcessToTerminalIOQueue.read(mReceiveBuffer, false);
|
||||
if (bytesRead > 0) {
|
||||
mEmulator.append(mReceiveBuffer, bytesRead);
|
||||
notifyScreenUpdate();
|
||||
}
|
||||
|
||||
if (msg.what == MSG_PROCESS_EXITED) {
|
||||
int exitCode = (Integer) msg.obj;
|
||||
cleanupResources(exitCode);
|
||||
mClient.onSessionFinished(TerminalSession.this);
|
||||
|
||||
String exitDescription = "\r\n[Process completed";
|
||||
if (exitCode > 0) {
|
||||
// Non-zero process exit.
|
||||
exitDescription += " (code " + exitCode + ")";
|
||||
} else if (exitCode < 0) {
|
||||
// Negated signal.
|
||||
exitDescription += " (signal " + (-exitCode) + ")";
|
||||
}
|
||||
exitDescription += " - press Enter]";
|
||||
|
||||
byte[] bytesToWrite = exitDescription.getBytes(StandardCharsets.UTF_8);
|
||||
mEmulator.append(bytesToWrite, bytesToWrite.length);
|
||||
notifyScreenUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,37 @@
|
||||
package com.termux.terminal;
|
||||
|
||||
/**
|
||||
* The interface for communication between {@link TerminalSession} and its client. It is used to
|
||||
* send callbacks to the client when {@link TerminalSession} changes or for sending other
|
||||
* back data to the client like logs.
|
||||
*/
|
||||
public interface TerminalSessionClient {
|
||||
|
||||
void onTextChanged(TerminalSession changedSession);
|
||||
|
||||
void onTitleChanged(TerminalSession changedSession);
|
||||
|
||||
void onSessionFinished(TerminalSession finishedSession);
|
||||
|
||||
void onClipboardText(TerminalSession session, String text);
|
||||
|
||||
void onBell(TerminalSession session);
|
||||
|
||||
void onColorsChanged(TerminalSession session);
|
||||
|
||||
|
||||
void logError(String tag, String message);
|
||||
|
||||
void logWarn(String tag, String message);
|
||||
|
||||
void logInfo(String tag, String message);
|
||||
|
||||
void logDebug(String tag, String message);
|
||||
|
||||
void logVerbose(String tag, String message);
|
||||
|
||||
void logStackTraceWithMessage(String tag, String message, Exception e);
|
||||
|
||||
void logStackTrace(String tag, Exception e);
|
||||
|
||||
}
|
Reference in New Issue
Block a user