diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f45b30f69c..aceb349787 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -35,6 +35,7 @@
+
diff --git a/app/src/main/java/com/termux/app/TermuxActivity.java b/app/src/main/java/com/termux/app/TermuxActivity.java
index 0c9f74125b..4c941076a0 100644
--- a/app/src/main/java/com/termux/app/TermuxActivity.java
+++ b/app/src/main/java/com/termux/app/TermuxActivity.java
@@ -10,6 +10,7 @@
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.net.Uri;
+import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.view.ContextMenu;
@@ -22,6 +23,7 @@
import android.view.WindowManager;
import android.widget.EditText;
import android.widget.ImageButton;
+import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.RelativeLayout;
import android.widget.Toast;
@@ -175,6 +177,11 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo
private float mTerminalToolbarDefaultHeight;
+ /**
+ * Unique identifier for this activity instance. Used for session attachment tracking.
+ */
+ private final int mActivityId = System.identityHashCode(this);
+
private static final int CONTEXT_MENU_SELECT_URL_ID = 0;
private static final int CONTEXT_MENU_SHARE_TRANSCRIPT_ID = 1;
@@ -251,6 +258,8 @@ public void onCreate(Bundle savedInstanceState) {
setToggleKeyboardView();
+ setNewWindowButtonView();
+
registerForContextMenu(mTerminalView);
FileReceiverActivity.updateFileReceiverActivityComponentsState(this);
@@ -352,9 +361,22 @@ public void onDestroy() {
if (mIsInvalidState) return;
+ // Detach all sessions owned by this activity and reset their clients
if (mTermuxService != null) {
- // Do not leave service and session clients with references to activity.
- mTermuxService.unsetTermuxTerminalSessionClient();
+ TerminalSession currentSession = getCurrentSession();
+ if (currentSession != null) {
+ // Reset this session's client to the service client
+ mTermuxService.resetSessionClient(currentSession);
+ }
+ if (mTerminalView != null) {
+ mTerminalView.detachSession();
+ }
+
+ // Detach all sessions owned by this activity (single source of truth)
+ mTermuxService.detachAllSessionsForActivity(mActivityId);
+
+ // Remove this activity's client from the service's set
+ mTermuxService.removeTermuxTerminalSessionClient(mTermuxTerminalSessionActivityClient);
mTermuxService = null;
}
@@ -374,6 +396,23 @@ public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
savedInstanceState.putBoolean(ARG_ACTIVITY_RECREATED, true);
}
+ @Override
+ public void onMultiWindowModeChanged(boolean isInMultiWindowMode) {
+ super.onMultiWindowModeChanged(isInMultiWindowMode);
+ Logger.logDebug(LOG_TAG, "onMultiWindowModeChanged: " + isInMultiWindowMode);
+ }
+
+ /** Add a new window */
+ public void addNewWindow() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ Intent intent = new Intent(this, TermuxActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT |
+ Intent.FLAG_ACTIVITY_NEW_TASK |
+ Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
+ startActivity(intent);
+ }
+ }
+
@@ -417,11 +456,33 @@ public void onServiceConnected(ComponentName componentName, IBinder service) {
// then the original intent will be re-delivered, resulting in a new session being re-added
// each time.
if (!mIsActivityRecreated && intent != null && Intent.ACTION_RUN.equals(intent.getAction())) {
- // Android 7.1 app shortcut from res/xml/shortcuts.xml.
- boolean isFailSafe = intent.getBooleanExtra(TERMUX_ACTIVITY.EXTRA_FAILSAFE_SESSION, false);
- mTermuxTerminalSessionActivityClient.addNewSession(isFailSafe, null);
- } else {
- mTermuxTerminalSessionActivityClient.setCurrentSession(mTermuxTerminalSessionActivityClient.getCurrentStoredSessionOrLast());
+ if (intent.getBooleanExtra(TERMUX_ACTIVITY.EXTRA_NEW_WINDOW, false)) {
+ // Android 7.1 app shortcut: open a new window
+ addNewWindow();
+ } else {
+ // Android 7.1 app shortcut from res/xml/shortcuts.xml.
+ boolean isFailSafe = intent.getBooleanExtra(TERMUX_ACTIVITY.EXTRA_FAILSAFE_SESSION, false);
+ mTermuxTerminalSessionActivityClient.addNewSession(isFailSafe, null);
+ }
+ }
+
+ // Ensure this activity is attached to a session
+ if (getCurrentSession() == null) {
+ TerminalSession sessionToAttach = mTermuxService.claimFirstUnattachedSession(mActivityId);
+ if (sessionToAttach != null) {
+ mTermuxTerminalSessionActivityClient.setCurrentSession(sessionToAttach);
+ } else {
+ // All sessions are attached, create a new one or use stored session
+ TerminalSession storedSession = mTermuxTerminalSessionActivityClient.getCurrentStoredSessionOrLast();
+ if (storedSession != null && !mTermuxService.isSessionAttached(storedSession)) {
+ mTermuxTerminalSessionActivityClient.setCurrentSession(storedSession);
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInMultiWindowMode()) {
+ // In multi-window mode with all sessions attached, create a new session
+ mTermuxTerminalSessionActivityClient.addNewSession(false, null);
+ } else {
+ mTermuxTerminalSessionActivityClient.setCurrentSession(storedSession);
+ }
+ }
}
}
@@ -594,6 +655,21 @@ private void setToggleKeyboardView() {
});
}
+ private void setNewWindowButtonView() {
+ View newWindowButton = findViewById(R.id.new_window_button);
+ if (newWindowButton != null) {
+ newWindowButton.setOnClickListener(v -> addNewWindow());
+ // Show if multi-window API is available (N+)
+ newWindowButton.setVisibility(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ? View.VISIBLE : View.GONE);
+ }
+
+ // Use vertical orientation for drawer buttons if multi-window is available
+ LinearLayout drawerButtons = findViewById(R.id.drawer_buttons);
+ if (drawerButtons != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ drawerButtons.setOrientation(LinearLayout.VERTICAL);
+ }
+ }
+
@@ -877,6 +953,10 @@ public TermuxService getTermuxService() {
return mTermuxService;
}
+ public int getActivityId() {
+ return mActivityId;
+ }
+
public TerminalView getTerminalView() {
return mTerminalView;
}
diff --git a/app/src/main/java/com/termux/app/TermuxService.java b/app/src/main/java/com/termux/app/TermuxService.java
index 8025d0bd2c..3cccd600d3 100644
--- a/app/src/main/java/com/termux/app/TermuxService.java
+++ b/app/src/main/java/com/termux/app/TermuxService.java
@@ -50,7 +50,11 @@
import com.termux.terminal.TerminalSessionClient;
import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
/**
* A service holding a list of {@link TermuxSession} in {@link TermuxShellManager#mTermuxSessions} and background {@link AppShell}
@@ -77,11 +81,18 @@ class LocalBinder extends Binder {
private final Handler mHandler = new Handler();
- /** 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.
+ /** The full implementations of the {@link TerminalSessionClient} interface to be used by {@link TerminalSession}
+ * that hold activity references for activity related functions. In multi-window mode, multiple
+ * activities may be bound simultaneously. Note that the service may often outlive the activities,
+ * so need to clear these references when activities are destroyed.
*/
- private TermuxTerminalSessionActivityClient mTermuxTerminalSessionActivityClient;
+ private final Set mActivityClients = new HashSet<>();
+
+ /**
+ * Single source of truth for session attachment state. Maps session handle to the activity
+ * instance ID that owns it. If a session handle is not in this map, it is unattached.
+ */
+ private final Map mSessionAttachments = new HashMap<>();
/** The basic implementation of the {@link TerminalSessionClient} interface to be used by {@link TerminalSession}
* that does not hold activity references and only a service reference.
@@ -195,7 +206,7 @@ public boolean onUnbind(Intent intent) {
// Since we cannot rely on {@link TermuxActivity.onDestroy()} to always complete,
// we unset clients here as well if it failed, so that we do not leave service and session
// clients with references to the activity.
- if (mTermuxTerminalSessionActivityClient != null)
+ if (!mActivityClients.isEmpty())
unsetTermuxTerminalSessionClient();
return false;
}
@@ -612,10 +623,8 @@ public synchronized TermuxSession createTermuxSession(ExecutionCommand execution
if (executionCommand.isPluginExecutionCommand)
mShellManager.mPendingPluginExecutionCommands.remove(executionCommand);
- // Notify {@link TermuxSessionsListViewController} that sessions list has been updated if
- // activity in is foreground
- if (mTermuxTerminalSessionActivityClient != null)
- mTermuxTerminalSessionActivityClient.termuxSessionListNotifyUpdated();
+ // Notify all activities that sessions list has been updated
+ notifyAllSessionListsUpdated();
updateNotification();
@@ -649,10 +658,8 @@ public void onTermuxSessionExited(final TermuxSession termuxSession) {
mShellManager.mTermuxSessions.remove(termuxSession);
- // Notify {@link TermuxSessionsListViewController} that sessions list has been updated if
- // activity in is foreground
- if (mTermuxTerminalSessionActivityClient != null)
- mTermuxTerminalSessionActivityClient.termuxSessionListNotifyUpdated();
+ // Notify all activities that sessions list has been updated
+ notifyAllSessionListsUpdated();
}
updateNotification();
@@ -687,8 +694,8 @@ private void handleSessionAction(int sessionAction, TerminalSession newTerminalS
switch (sessionAction) {
case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_OPEN_ACTIVITY:
setCurrentStoredTerminalSession(newTerminalSession);
- if (mTermuxTerminalSessionActivityClient != null)
- mTermuxTerminalSessionActivityClient.setCurrentSession(newTerminalSession);
+ if (!mActivityClients.isEmpty())
+ mActivityClients.iterator().next().setCurrentSession(newTerminalSession);
startTermuxActivity();
break;
case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_KEEP_CURRENT_SESSION_AND_OPEN_ACTIVITY:
@@ -698,8 +705,8 @@ private void handleSessionAction(int sessionAction, TerminalSession newTerminalS
break;
case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_DONT_OPEN_ACTIVITY:
setCurrentStoredTerminalSession(newTerminalSession);
- if (mTermuxTerminalSessionActivityClient != null)
- mTermuxTerminalSessionActivityClient.setCurrentSession(newTerminalSession);
+ if (!mActivityClients.isEmpty())
+ mActivityClients.iterator().next().setCurrentSession(newTerminalSession);
break;
case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_KEEP_CURRENT_SESSION_AND_DONT_OPEN_ACTIVITY:
if (getTermuxSessionsSize() == 1)
@@ -731,37 +738,42 @@ private void startTermuxActivity() {
- /** If {@link TermuxActivity} has not bound to the {@link TermuxService} yet or is destroyed, then
+ /** If no {@link TermuxActivity} has bound to the {@link TermuxService} yet or all are destroyed, then
* interface functions requiring the activity should not be available to the terminal sessions,
* so we just return the {@link #mTermuxTerminalSessionServiceClient}. Once {@link TermuxActivity} bind
- * callback is received, it should call {@link #setTermuxTerminalSessionClient} to set the
- * {@link TermuxService#mTermuxTerminalSessionActivityClient} so that further terminal sessions are directly
- * passed the {@link TermuxTerminalSessionActivityClient} object which fully implements the
+ * callback is received, it should call {@link #setTermuxTerminalSessionClient} to add its client
+ * to {@link TermuxService#mActivityClients} so that further terminal sessions are directly
+ * passed a {@link TermuxTerminalSessionActivityClient} object which fully implements the
* {@link TerminalSessionClient} interface.
*
- * @return Returns the {@link TermuxTerminalSessionActivityClient} if {@link TermuxActivity} has bound with
- * {@link TermuxService}, otherwise {@link TermuxTerminalSessionServiceClient}.
+ * @return Returns the first available {@link TermuxTerminalSessionActivityClient} if any {@link TermuxActivity}
+ * has bound with {@link TermuxService}, otherwise {@link TermuxTerminalSessionServiceClient}.
*/
public synchronized TermuxTerminalSessionClientBase getTermuxTerminalSessionClient() {
- if (mTermuxTerminalSessionActivityClient != null)
- return mTermuxTerminalSessionActivityClient;
+ if (!mActivityClients.isEmpty())
+ return mActivityClients.iterator().next();
else
return mTermuxTerminalSessionServiceClient;
}
- /** This should be called when {@link TermuxActivity#onServiceConnected} is called to set the
- * {@link TermuxService#mTermuxTerminalSessionActivityClient} variable and update the {@link TerminalSession}
- * and {@link TerminalEmulator} clients in case they were passed {@link TermuxTerminalSessionServiceClient}
- * earlier.
+ /** This should be called when {@link TermuxActivity#onServiceConnected} is called to add
+ * the activity's client to {@link TermuxService#mActivityClients}. In multi-window mode,
+ * multiple activities may be bound simultaneously.
*
* @param termuxTerminalSessionActivityClient The {@link TermuxTerminalSessionActivityClient} object that fully
* implements the {@link TerminalSessionClient} interface.
*/
public synchronized void setTermuxTerminalSessionClient(TermuxTerminalSessionActivityClient termuxTerminalSessionActivityClient) {
- mTermuxTerminalSessionActivityClient = termuxTerminalSessionActivityClient;
+ mActivityClients.add(termuxTerminalSessionActivityClient);
- for (int i = 0; i < mShellManager.mTermuxSessions.size(); i++)
- mShellManager.mTermuxSessions.get(i).getTerminalSession().updateTerminalSessionClient(mTermuxTerminalSessionActivityClient);
+ // Don't update all sessions' clients here - in multi-window mode, each activity
+ // should only set the client for its own attached session. The client is set
+ // when setCurrentSession() is called in TermuxTerminalSessionActivityClient.
+ }
+
+ /** Remove an activity client when its activity is destroyed. */
+ public synchronized void removeTermuxTerminalSessionClient(TermuxTerminalSessionActivityClient client) {
+ mActivityClients.remove(client);
}
/** This should be called when {@link TermuxActivity} has been destroyed and in {@link #onUnbind(Intent)}
@@ -772,11 +784,129 @@ public synchronized void unsetTermuxTerminalSessionClient() {
for (int i = 0; i < mShellManager.mTermuxSessions.size(); i++)
mShellManager.mTermuxSessions.get(i).getTerminalSession().updateTerminalSessionClient(mTermuxTerminalSessionServiceClient);
- mTermuxTerminalSessionActivityClient = null;
+ mActivityClients.clear();
+ }
+
+ /** Reset a specific session's client to the service client. Used when an activity is destroyed
+ * in multi-window mode to avoid resetting all sessions' clients. */
+ public synchronized void resetSessionClient(TerminalSession session) {
+ if (session != null) {
+ session.updateTerminalSessionClient(mTermuxTerminalSessionServiceClient);
+ }
+ }
+
+ /** Notify all bound activities to update their session lists. Used when session attachment
+ * state changes in multi-window mode. */
+ public synchronized void notifyAllSessionListsUpdated() {
+ for (TermuxTerminalSessionActivityClient client : mActivityClients) {
+ client.termuxSessionListNotifyUpdated();
+ }
+ }
+
+ /**
+ * Check if a session is attached to any activity.
+ * @param session The session to check
+ * @return true if the session is attached to any activity
+ */
+ public synchronized boolean isSessionAttached(TerminalSession session) {
+ if (session == null) return false;
+ return mSessionAttachments.containsKey(session.mHandle);
+ }
+
+ /**
+ * Check if a session is attached to a different activity than the one specified.
+ * @param session The session to check
+ * @param activityId The activity ID to compare against
+ * @return true if the session is attached to a different activity
+ */
+ public synchronized boolean isSessionAttachedToOther(TerminalSession session, int activityId) {
+ if (session == null) return false;
+ Integer owner = mSessionAttachments.get(session.mHandle);
+ return owner != null && owner != activityId;
+ }
+
+ /**
+ * Bring the activity that owns a session to the foreground.
+ * @param session The session whose owning activity should be focused
+ * @return true if the activity was found and brought to front
+ */
+ @SuppressLint("MissingPermission")
+ public synchronized boolean focusActivityForSession(TerminalSession session) {
+ if (session == null) return false;
+ Integer ownerId = mSessionAttachments.get(session.mHandle);
+ if (ownerId == null) return false;
+
+ for (TermuxTerminalSessionActivityClient client : mActivityClients) {
+ TermuxActivity activity = client.getActivity();
+ if (activity != null && activity.getActivityId() == ownerId) {
+ // Bring the activity's task to front
+ android.app.ActivityManager am = (android.app.ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
+ if (am != null) {
+ am.moveTaskToFront(activity.getTaskId(), 0);
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Attempt to attach a session to an activity. Fails if already attached elsewhere.
+ * @param session The session to attach
+ * @param activityId The activity ID claiming the session
+ * @return true if successfully attached, false if already attached elsewhere
+ */
+ public synchronized boolean attachSession(TerminalSession session, int activityId) {
+ if (session == null) return false;
+ Integer currentOwner = mSessionAttachments.get(session.mHandle);
+ if (currentOwner != null && currentOwner != activityId) {
+ return false; // Already attached to a different activity
+ }
+ mSessionAttachments.put(session.mHandle, activityId);
+ notifyAllSessionListsUpdated();
+ return true;
}
+ /**
+ * Detach a session from an activity. Only succeeds if the activity owns the session.
+ * @param session The session to detach
+ * @param activityId The activity ID releasing the session
+ */
+ public synchronized void detachSession(TerminalSession session, int activityId) {
+ if (session == null) return;
+ Integer owner = mSessionAttachments.get(session.mHandle);
+ if (owner != null && owner == activityId) {
+ mSessionAttachments.remove(session.mHandle);
+ notifyAllSessionListsUpdated();
+ }
+ }
+ /**
+ * Detach all sessions owned by an activity. Called when activity is destroyed.
+ * @param activityId The activity ID whose sessions should be detached
+ */
+ public synchronized void detachAllSessionsForActivity(int activityId) {
+ mSessionAttachments.entrySet().removeIf(entry -> entry.getValue() == activityId);
+ notifyAllSessionListsUpdated();
+ }
+ /**
+ * Atomically claim an unattached session by marking it as attached.
+ * @param activityId The activity ID claiming the session
+ * @return The claimed session, or null if no unattached sessions are available
+ */
+ @Nullable
+ public synchronized TerminalSession claimFirstUnattachedSession(int activityId) {
+ for (int i = 0; i < mShellManager.mTermuxSessions.size(); i++) {
+ TerminalSession session = mShellManager.mTermuxSessions.get(i).getTerminalSession();
+ if (!mSessionAttachments.containsKey(session.mHandle)) {
+ mSessionAttachments.put(session.mHandle, activityId);
+ notifyAllSessionListsUpdated();
+ return session;
+ }
+ }
+ return null;
+ }
private Notification buildNotification() {
diff --git a/app/src/main/java/com/termux/app/terminal/TermuxSessionsListViewController.java b/app/src/main/java/com/termux/app/terminal/TermuxSessionsListViewController.java
index bf914b977b..a13c01b354 100644
--- a/app/src/main/java/com/termux/app/terminal/TermuxSessionsListViewController.java
+++ b/app/src/main/java/com/termux/app/terminal/TermuxSessionsListViewController.java
@@ -58,11 +58,15 @@ public View getView(int position, View convertView, @NonNull ViewGroup parent) {
}
boolean shouldEnableDarkTheme = ThemeUtils.shouldEnableDarkTheme(mActivity, NightMode.getAppNightMode().getName());
+ boolean isCurrentSession = sessionAtRow == mActivity.getCurrentSession();
- if (shouldEnableDarkTheme) {
- sessionTitleView.setBackground(
- ContextCompat.getDrawable(mActivity, R.drawable.session_background_black_selected)
- );
+ // Set background based on whether this is the current session
+ if (isCurrentSession) {
+ sessionTitleView.setBackground(ContextCompat.getDrawable(mActivity,
+ shouldEnableDarkTheme ? R.drawable.current_session_black : R.drawable.current_session));
+ } else {
+ sessionTitleView.setBackground(ContextCompat.getDrawable(mActivity,
+ shouldEnableDarkTheme ? R.drawable.session_background_black_selected : R.drawable.session_background_selected));
}
String name = sessionAtRow.mSessionName;
@@ -89,13 +93,34 @@ public View getView(int position, View convertView, @NonNull ViewGroup parent) {
int defaultColor = shouldEnableDarkTheme ? Color.WHITE : Color.BLACK;
int color = sessionRunning || sessionAtRow.getExitStatus() == 0 ? defaultColor : Color.RED;
sessionTitleView.setTextColor(color);
+
+ // Gray out sessions attached to other windows
+ boolean isAttachedToOtherWindow = mActivity.getTermuxService() != null &&
+ mActivity.getTermuxService().isSessionAttachedToOther(sessionAtRow, mActivity.getActivityId());
+ if (isAttachedToOtherWindow) {
+ sessionTitleView.setAlpha(0.5f);
+ } else {
+ sessionTitleView.setAlpha(1.0f);
+ }
+
return sessionRowView;
}
@Override
public void onItemClick(AdapterView> parent, View view, int position, long id) {
TermuxSession clickedSession = getItem(position);
- mActivity.getTermuxTerminalSessionClient().setCurrentSession(clickedSession.getTerminalSession());
+ TerminalSession session = clickedSession.getTerminalSession();
+ if (mActivity.getTermuxService() == null) return;
+
+ if (mActivity.getTermuxService().isSessionAttachedToOther(session, mActivity.getActivityId())) {
+ // Session is attached to another window - focus that window instead
+ if (!mActivity.getTermuxService().focusActivityForSession(session)) {
+ mActivity.showToast(mActivity.getString(R.string.msg_failed_to_focus_window), true);
+ }
+ } else {
+ // Session is unattached or attached to this window - switch to it
+ mActivity.getTermuxTerminalSessionClient().setCurrentSession(session);
+ }
mActivity.getDrawer().closeDrawers();
}
diff --git a/app/src/main/java/com/termux/app/terminal/TermuxTerminalSessionActivityClient.java b/app/src/main/java/com/termux/app/terminal/TermuxTerminalSessionActivityClient.java
index bd789145f2..a856725555 100644
--- a/app/src/main/java/com/termux/app/terminal/TermuxTerminalSessionActivityClient.java
+++ b/app/src/main/java/com/termux/app/terminal/TermuxTerminalSessionActivityClient.java
@@ -54,6 +54,10 @@ public TermuxTerminalSessionActivityClient(TermuxActivity activity) {
this.mActivity = activity;
}
+ public TermuxActivity getActivity() {
+ return mActivity;
+ }
+
/**
* Should be called when mActivity.onCreate() is called
*/
@@ -67,10 +71,13 @@ public void onCreate() {
*/
public void onStart() {
// The service has connected, but data may have changed since we were last in the foreground.
- // Get the session stored in shared preferences stored by {@link #onStop} if its valid,
- // otherwise get the last session currently running.
if (mActivity.getTermuxService() != null) {
- setCurrentSession(getCurrentStoredSessionOrLast());
+ TerminalSession currentSession = mActivity.getCurrentSession();
+ // In multi-window mode, keep the current attached session if it's still valid.
+ // Only restore from preferences if we don't have a valid session attached.
+ if (currentSession == null || mActivity.getTermuxService().getTermuxSessionForTerminalSession(currentSession) == null) {
+ setCurrentSession(getCurrentStoredSessionOrLast());
+ }
termuxSessionListNotifyUpdated();
}
@@ -293,14 +300,30 @@ private synchronized void releaseBellSoundPool() {
public void setCurrentSession(TerminalSession session) {
if (session == null) return;
- if (mActivity.getTerminalView().attachSession(session)) {
+ TermuxService service = mActivity.getTermuxService();
+ TerminalSession previousSession = mActivity.getTerminalView().attachSession(session);
+ if (previousSession != session) {
+ // Session changed - update attachment state through the service
+ if (service != null) {
+ // Detach the previous session from this activity
+ if (previousSession != null) {
+ service.detachSession(previousSession, mActivity.getActivityId());
+ }
+ // Attach the new session to this activity
+ service.attachSession(session, mActivity.getActivityId());
+ }
+
// notify about switched session if not already displaying the session
notifyOfSessionChange();
}
+ // Set this activity's client on the session so it receives callbacks (render updates, etc.)
+ // This is important for multi-window support where each window needs its own client.
+ session.updateTerminalSessionClient(this);
+
// We call the following even when the session is already being displayed since config may
// be stale, like current session not selected or scrolled to.
- checkAndScrollToSession(session);
+ scrollToSession(session);
updateBackgroundColor();
}
@@ -318,17 +341,31 @@ public void switchToSession(boolean forward) {
if (service == null) return;
TerminalSession currentTerminalSession = mActivity.getCurrentSession();
- int index = service.getIndexOfSession(currentTerminalSession);
+ int currentIndex = service.getIndexOfSession(currentTerminalSession);
int size = service.getTermuxSessionsSize();
- if (forward) {
- if (++index >= size) index = 0;
- } else {
- if (--index < 0) index = size - 1;
- }
+ int activityId = mActivity.getActivityId();
- TermuxSession termuxSession = service.getTermuxSession(index);
- if (termuxSession != null)
- setCurrentSession(termuxSession.getTerminalSession());
+ // Find the next session in the given direction
+ int index = currentIndex;
+ for (int i = 0; i < size; i++) {
+ if (forward) {
+ if (++index >= size) index = 0;
+ } else {
+ if (--index < 0) index = size - 1;
+ }
+
+ TermuxSession termuxSession = service.getTermuxSession(index);
+ if (termuxSession != null) {
+ TerminalSession session = termuxSession.getTerminalSession();
+ if (service.isSessionAttachedToOther(session, activityId)) {
+ // Session is attached to another window - focus that window
+ service.focusActivityForSession(session);
+ } else {
+ setCurrentSession(session);
+ }
+ return;
+ }
+ }
}
public void switchToSession(int index) {
@@ -336,8 +373,15 @@ public void switchToSession(int index) {
if (service == null) return;
TermuxSession termuxSession = service.getTermuxSession(index);
- if (termuxSession != null)
- setCurrentSession(termuxSession.getTerminalSession());
+ if (termuxSession != null) {
+ TerminalSession session = termuxSession.getTerminalSession();
+ if (service.isSessionAttachedToOther(session, mActivity.getActivityId())) {
+ // Session is attached to another window - focus that window
+ service.focusActivityForSession(session);
+ } else {
+ setCurrentSession(session);
+ }
+ }
}
@SuppressLint("InflateParams")
@@ -442,12 +486,15 @@ public void removeFinishedSession(TerminalSession finishedSession) {
// There are no sessions to show, so finish the activity.
mActivity.finishActivityIfNotFinishing();
} else {
- if (index >= size) {
- index = size - 1;
+ // Try to atomically claim an unattached session to switch to
+ TerminalSession unattachedSession = service.claimFirstUnattachedSession(mActivity.getActivityId());
+ if (unattachedSession != null) {
+ setCurrentSession(unattachedSession);
+ } else {
+ // All remaining sessions are attached to other windows.
+ // Close this activity instead of stealing a session from another window.
+ mActivity.finishActivityIfNotFinishing();
}
- TermuxSession termuxSession = service.getTermuxSession(index);
- if (termuxSession != null)
- setCurrentSession(termuxSession.getTerminalSession());
}
}
@@ -455,7 +502,7 @@ public void termuxSessionListNotifyUpdated() {
mActivity.termuxSessionListNotifyUpdated();
}
- public void checkAndScrollToSession(TerminalSession session) {
+ public void scrollToSession(TerminalSession session) {
if (!mActivity.isVisible()) return;
TermuxService service = mActivity.getTermuxService();
if (service == null) return;
@@ -465,7 +512,6 @@ public void checkAndScrollToSession(TerminalSession session) {
final ListView termuxSessionsListView = mActivity.findViewById(R.id.terminal_sessions_list);
if (termuxSessionsListView == null) return;
- termuxSessionsListView.setItemChecked(indexOfSession, true);
// Delay is necessary otherwise sometimes scroll to newly added session does not happen
termuxSessionsListView.postDelayed(() -> termuxSessionsListView.smoothScrollToPosition(indexOfSession), 1000);
}
diff --git a/app/src/main/java/com/termux/app/terminal/TermuxTerminalViewClient.java b/app/src/main/java/com/termux/app/terminal/TermuxTerminalViewClient.java
index 700c5e5098..a6e36cc105 100644
--- a/app/src/main/java/com/termux/app/terminal/TermuxTerminalViewClient.java
+++ b/app/src/main/java/com/termux/app/terminal/TermuxTerminalViewClient.java
@@ -269,6 +269,8 @@ public boolean onKeyDown(int keyCode, KeyEvent e, TerminalSession currentSession
showUrlSelection();
} else if (unicodeChar == 'v') {
doPaste();
+ } else if (unicodeChar == 'w'/* new window */) {
+ mActivity.addNewWindow();
} 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
diff --git a/app/src/main/res/layout/activity_termux.xml b/app/src/main/res/layout/activity_termux.xml
index 831ea7cfb8..863534a3c2 100644
--- a/app/src/main/res/layout/activity_termux.xml
+++ b/app/src/main/res/layout/activity_termux.xml
@@ -65,10 +65,11 @@
android:layout_height="0dp"
android:layout_gravity="top"
android:layout_weight="1"
- android:choiceMode="singleChoice"
+ android:choiceMode="none"
android:longClickable="true" />
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index cbd2992ba1..2bfba5a505 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -43,9 +43,11 @@
New session
+ New window
Failsafe
Max terminals reached
Close down existing ones before creating new.
+ Failed to focus window
Set session name
Set
diff --git a/app/src/main/res/xml/shortcuts.xml b/app/src/main/res/xml/shortcuts.xml
index dcd8d341cc..894d7d9c47 100644
--- a/app/src/main/res/xml/shortcuts.xml
+++ b/app/src/main/res/xml/shortcuts.xml
@@ -23,6 +23,21 @@
android:name="android.shortcut.conversation"/>
+
+
+
+
+
+