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();