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..951f711f5e 100644
--- a/app/src/main/java/com/termux/app/terminal/TermuxTerminalSessionActivityClient.java
+++ b/app/src/main/java/com/termux/app/terminal/TermuxTerminalSessionActivityClient.java
@@ -8,6 +8,8 @@
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Typeface;
+import android.app.Notification;
+import android.app.NotificationManager;
import android.media.AudioAttributes;
import android.media.SoundPool;
import android.text.TextUtils;
@@ -116,9 +118,12 @@ public void onReloadActivityStyling() {
@Override
public void onTextChanged(@NonNull TerminalSession changedSession) {
- if (!mActivity.isVisible()) return;
-
- if (mActivity.getCurrentSession() == changedSession) mActivity.getTerminalView().onScreenUpdated();
+ // IMPORTANT: Always update the screen, even if Activity is in background
+ // This prevents character overlay issues when app resumes from background.
+ // Background updates ensure the display is synchronized when app becomes visible again.
+ if (mActivity.getCurrentSession() == changedSession) {
+ mActivity.getTerminalView().onScreenUpdated();
+ }
}
@Override
@@ -196,21 +201,84 @@ public void onPasteTextFromClipboard(@Nullable TerminalSession session) {
mActivity.getTerminalView().mEmulator.paste(text);
}
+
+ /**
+ * Send a notification for the bell character event.
+ * BUGFIX P2: Implement notifications for SSH task completion signals
+ *
+ * @param session The terminal session that rang the bell
+ * @param includeVibration Whether to include vibration in the notification
+ */
+ private void sendBellNotification(@NonNull TerminalSession session, boolean includeVibration) {
+ try {
+ NotificationManager nm = (NotificationManager)
+ mActivity.getSystemService(Context.NOTIFICATION_SERVICE);
+
+ if (nm == null) return;
+
+ // Get a unique notification ID
+ int notificationId = com.termux.shared.termux.notification.TermuxNotificationUtils.getNextNotificationId(mActivity);
+
+ // Build the notification
+ Notification.Builder builder = com.termux.shared.termux.notification.TermuxNotificationUtils
+ .getTermuxOrPluginAppNotificationBuilder(
+ mActivity,
+ mActivity,
+ TermuxConstants.TERMUX_APP_NOTIFICATION_CHANNEL_ID,
+ Notification.PRIORITY_DEFAULT,
+ mActivity.getString(R.string.notification_bell_title),
+ mActivity.getString(R.string.notification_bell_text),
+ null,
+ null,
+ null,
+ 0);
+
+ if (builder != null) {
+ if (includeVibration) {
+ builder.setVibrate(new long[]{0, 250, 250, 250});
+ }
+ nm.notify(notificationId, builder.build());
+ Logger.logDebug(LOG_TAG, "Bell notification sent with ID: " + notificationId);
+ }
+ } catch (Exception e) {
+ Logger.logError(LOG_TAG, "Error sending bell notification: " + e.getMessage());
+ }
+ }
+
@Override
public void onBell(@NonNull 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:
- loadBellSoundPool();
- if (mBellSoundPool != null)
- mBellSoundPool.play(mBellSoundId, 1.f, 1.f, 1, 0, 1.f);
+ int bellBehaviour = mActivity.getProperties().getBellBehaviour();
+
+ // Handle vibrate/beep only when app is visible
+ if (mActivity.isVisible()) {
+ switch (bellBehaviour) {
+ case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_VIBRATE:
+ BellHandler.getInstance(mActivity).doBell();
+ break;
+ case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_BEEP:
+ loadBellSoundPool();
+ if (mBellSoundPool != null)
+ mBellSoundPool.play(mBellSoundId, 1.f, 1.f, 1, 0, 1.f);
+ break;
+ case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_VIBRATE_AND_NOTIFICATION:
+ BellHandler.getInstance(mActivity).doBell();
+ break;
+ case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_IGNORE:
+ // Ignore the bell character.
+ break;
+ }
+ }
+
+ // Handle notifications - these work both foreground and background
+ // BUGFIX P2: Notifications don't have the visibility restriction.
+ // For vibrate-and-notification, only add vibration to the notification when the app is
+ // not visible; when visible, BellHandler.doBell() already handles vibration above.
+ switch (bellBehaviour) {
+ case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_NOTIFICATION:
+ sendBellNotification(session, false);
break;
- case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_IGNORE:
- // Ignore the bell character.
+ case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_VIBRATE_AND_NOTIFICATION:
+ sendBellNotification(session, !mActivity.isVisible());
break;
}
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index cbd2992ba1..0d2c58a3b4 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -100,6 +100,8 @@
Exit
Acquire wakelock
Release wakelock
+ Command Complete
+ Remote command finished - Bell signal received
diff --git a/terminal-emulator/src/main/java/com/termux/terminal/TerminalBuffer.java b/terminal-emulator/src/main/java/com/termux/terminal/TerminalBuffer.java
index 21d6518785..cb6de31697 100644
--- a/terminal-emulator/src/main/java/com/termux/terminal/TerminalBuffer.java
+++ b/terminal-emulator/src/main/java/com/termux/terminal/TerminalBuffer.java
@@ -290,10 +290,18 @@ public void resize(int newColumns, int newRows, int newTotalRows, int[] cursor,
lastNonSpaceIndex = oldLine.getSpaceUsed();
if (cursorAtThisRow) justToCursor = true;
} else {
- for (int i = 0; i < oldLine.getSpaceUsed(); i++)
- // NEWLY INTRODUCED BUG! Should not index oldLine.mStyle with char indices
- if (oldLine.mText[i] != ' '/* || oldLine.mStyle[i] != currentStyle */)
- lastNonSpaceIndex = i + 1;
+ for (int i = 0; i < oldLine.getSpaceUsed(); i++) {
+ if (oldLine.mText[i] != ' ') {
+ // BUGFIX P1: mStyle is indexed by columns, not character indices
+ // Only count non-space characters to find last non-space index
+ char c = oldLine.mText[i];
+ int codePoint = (Character.isHighSurrogate(c)) ? Character.toCodePoint(c, oldLine.mText[++i]) : c;
+ int columnForChar = WcWidth.width(codePoint);
+ if (columnForChar > 0) {
+ lastNonSpaceIndex = i + 1;
+ }
+ }
+ }
}
int currentOldCol = 0;
diff --git a/terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java b/terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java
index b0be6f3440..f3c7665833 100644
--- a/terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java
+++ b/terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java
@@ -135,7 +135,7 @@ public final class TerminalEmulator {
private final Stack mTitleStack = new Stack<>();
/** The cursor position. Between (0,0) and (mRows-1, mColumns-1). */
- private int mCursorRow, mCursorCol;
+ private volatile int mCursorRow, mCursorCol;
/** The number of character rows and columns in the terminal screen. */
public int mRows, mColumns;
@@ -421,14 +421,23 @@ private void resizeScreen() {
mCursorRow = cursor[1];
}
- public int getCursorRow() {
+ public synchronized int getCursorRow() {
return mCursorRow;
}
- public int getCursorCol() {
+ public synchronized int getCursorCol() {
return mCursorCol;
}
+ /**
+ * Set cursor position safely with thread synchronization.
+ * BUGFIX P1: Thread-safe cursor updates
+ */
+ public synchronized void setCursorRowCol(int row, int col) {
+ mCursorRow = row;
+ mCursorCol = col;
+ }
+
/** Get the terminal cursor style. It will be one of {@link #TERMINAL_CURSOR_STYLES_LIST} */
public int getCursorStyle() {
return mCursorStyle;
diff --git a/termux-shared/src/main/java/com/termux/shared/termux/settings/properties/TermuxPropertyConstants.java b/termux-shared/src/main/java/com/termux/shared/termux/settings/properties/TermuxPropertyConstants.java
index dd935f3ab5..80f9b20291 100644
--- a/termux-shared/src/main/java/com/termux/shared/termux/settings/properties/TermuxPropertyConstants.java
+++ b/termux-shared/src/main/java/com/termux/shared/termux/settings/properties/TermuxPropertyConstants.java
@@ -173,11 +173,15 @@ public final class TermuxPropertyConstants {
public static final String VALUE_BELL_BEHAVIOUR_VIBRATE = "vibrate";
public static final String VALUE_BELL_BEHAVIOUR_BEEP = "beep";
public static final String VALUE_BELL_BEHAVIOUR_IGNORE = "ignore";
+ public static final String VALUE_BELL_BEHAVIOUR_NOTIFICATION = "notification";
+ public static final String VALUE_BELL_BEHAVIOUR_VIBRATE_AND_NOTIFICATION = "vibrate-and-notification";
public static final String DEFAULT_VALUE_BELL_BEHAVIOUR = VALUE_BELL_BEHAVIOUR_VIBRATE;
public static final int IVALUE_BELL_BEHAVIOUR_VIBRATE = 1;
public static final int IVALUE_BELL_BEHAVIOUR_BEEP = 2;
public static final int IVALUE_BELL_BEHAVIOUR_IGNORE = 3;
+ public static final int IVALUE_BELL_BEHAVIOUR_NOTIFICATION = 4;
+ public static final int IVALUE_BELL_BEHAVIOUR_VIBRATE_AND_NOTIFICATION = 5;
public static final int DEFAULT_IVALUE_BELL_BEHAVIOUR = IVALUE_BELL_BEHAVIOUR_VIBRATE;
/** Defines the bidirectional map for bell behaviour values and their internal values */
@@ -186,6 +190,8 @@ public final class TermuxPropertyConstants {
.put(VALUE_BELL_BEHAVIOUR_VIBRATE, IVALUE_BELL_BEHAVIOUR_VIBRATE)
.put(VALUE_BELL_BEHAVIOUR_BEEP, IVALUE_BELL_BEHAVIOUR_BEEP)
.put(VALUE_BELL_BEHAVIOUR_IGNORE, IVALUE_BELL_BEHAVIOUR_IGNORE)
+ .put(VALUE_BELL_BEHAVIOUR_NOTIFICATION, IVALUE_BELL_BEHAVIOUR_NOTIFICATION)
+ .put(VALUE_BELL_BEHAVIOUR_VIBRATE_AND_NOTIFICATION, IVALUE_BELL_BEHAVIOUR_VIBRATE_AND_NOTIFICATION)
.build();