mirror of https://github.com/termux/termux-app
767 lines
33 KiB
Java
767 lines
33 KiB
Java
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.os.Environment;
|
|
import android.text.TextUtils;
|
|
import android.view.Gravity;
|
|
import android.view.InputDevice;
|
|
import android.view.KeyEvent;
|
|
import android.view.MotionEvent;
|
|
import android.view.View;
|
|
import android.widget.EditText;
|
|
import android.widget.ListView;
|
|
import android.widget.Toast;
|
|
|
|
import com.termux.R;
|
|
import com.termux.app.TermuxActivity;
|
|
import com.termux.shared.data.UrlUtils;
|
|
import com.termux.shared.file.FileUtils;
|
|
import com.termux.shared.interact.MessageDialogUtils;
|
|
import com.termux.shared.interact.ShareUtils;
|
|
import com.termux.shared.shell.ShellUtils;
|
|
import com.termux.shared.terminal.TermuxTerminalViewClientBase;
|
|
import com.termux.shared.terminal.io.extrakeys.SpecialButton;
|
|
import com.termux.shared.termux.AndroidUtils;
|
|
import com.termux.shared.termux.TermuxConstants;
|
|
import com.termux.shared.activities.ReportActivity;
|
|
import com.termux.shared.models.ReportInfo;
|
|
import com.termux.app.models.UserAction;
|
|
import com.termux.app.terminal.io.KeyboardShortcut;
|
|
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
|
import com.termux.shared.data.DataUtils;
|
|
import com.termux.shared.logger.Logger;
|
|
import com.termux.shared.markdown.MarkdownUtils;
|
|
import com.termux.shared.termux.TermuxUtils;
|
|
import com.termux.shared.view.KeyboardUtils;
|
|
import com.termux.shared.view.ViewUtils;
|
|
import com.termux.terminal.KeyHandler;
|
|
import com.termux.terminal.TerminalBuffer;
|
|
import com.termux.terminal.TerminalEmulator;
|
|
import com.termux.terminal.TerminalSession;
|
|
|
|
import java.util.Arrays;
|
|
import java.util.Collections;
|
|
import java.util.LinkedHashSet;
|
|
import java.util.List;
|
|
|
|
import androidx.drawerlayout.widget.DrawerLayout;
|
|
|
|
public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|
|
|
final TermuxActivity mActivity;
|
|
|
|
final TermuxTerminalSessionClient mTermuxTerminalSessionClient;
|
|
|
|
/** Keeping track of the special keys acting as Ctrl and Fn for the soft keyboard and other hardware keys. */
|
|
boolean mVirtualControlKeyDown, mVirtualFnKeyDown;
|
|
|
|
private Runnable mShowSoftKeyboardRunnable;
|
|
|
|
private boolean mShowSoftKeyboardIgnoreOnce;
|
|
private boolean mShowSoftKeyboardWithDelayOnce;
|
|
|
|
private boolean mTerminalCursorBlinkerStateAlreadySet;
|
|
|
|
private static final String LOG_TAG = "TermuxTerminalViewClient";
|
|
|
|
public TermuxTerminalViewClient(TermuxActivity activity, TermuxTerminalSessionClient termuxTerminalSessionClient) {
|
|
this.mActivity = activity;
|
|
this.mTermuxTerminalSessionClient = termuxTerminalSessionClient;
|
|
}
|
|
|
|
public TermuxActivity getActivity() {
|
|
return mActivity;
|
|
}
|
|
|
|
/**
|
|
* Should be called when mActivity.onCreate() is called
|
|
*/
|
|
public void onCreate() {
|
|
mActivity.getTerminalView().setTextSize(mActivity.getPreferences().getFontSize());
|
|
mActivity.getTerminalView().setKeepScreenOn(mActivity.getPreferences().shouldKeepScreenOn());
|
|
}
|
|
|
|
/**
|
|
* Should be called when mActivity.onStart() is called
|
|
*/
|
|
public void onStart() {
|
|
// Set {@link TerminalView#TERMINAL_VIEW_KEY_LOGGING_ENABLED} value
|
|
// Also required if user changed the preference from {@link TermuxSettings} activity and returns
|
|
boolean isTerminalViewKeyLoggingEnabled = mActivity.getPreferences().isTerminalViewKeyLoggingEnabled();
|
|
mActivity.getTerminalView().setIsTerminalViewKeyLoggingEnabled(isTerminalViewKeyLoggingEnabled);
|
|
|
|
// Piggyback on the terminal view key logging toggle for now, should add a separate toggle in future
|
|
mActivity.getTermuxActivityRootView().setIsRootViewLoggingEnabled(isTerminalViewKeyLoggingEnabled);
|
|
ViewUtils.setIsViewUtilsLoggingEnabled(isTerminalViewKeyLoggingEnabled);
|
|
}
|
|
|
|
/**
|
|
* Should be called when mActivity.onResume() is called
|
|
*/
|
|
public void onResume() {
|
|
// Show the soft keyboard if required
|
|
setSoftKeyboardState(true, false);
|
|
|
|
mTerminalCursorBlinkerStateAlreadySet = false;
|
|
|
|
if (mActivity.getTerminalView().mEmulator != null) {
|
|
// Start terminal cursor blinking if enabled
|
|
// If emulator is already set, then start blinker now, otherwise wait for onEmulatorSet()
|
|
// event to start it. This is needed since onEmulatorSet() may not be called after
|
|
// TermuxActivity is started after device display timeout with double tap and not power button.
|
|
setTerminalCursorBlinkerState(true);
|
|
mTerminalCursorBlinkerStateAlreadySet = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Should be called when mActivity.onStop() is called
|
|
*/
|
|
public void onStop() {
|
|
// Stop terminal cursor blinking if enabled
|
|
setTerminalCursorBlinkerState(false);
|
|
}
|
|
|
|
/**
|
|
* Should be called when mActivity.reloadActivityStyling() is called
|
|
*/
|
|
public void onReload() {
|
|
// Show the soft keyboard if required
|
|
setSoftKeyboardState(false, true);
|
|
|
|
// Start terminal cursor blinking if enabled
|
|
setTerminalCursorBlinkerState(true);
|
|
}
|
|
|
|
/**
|
|
* Should be called when {@link com.termux.view.TerminalView#mEmulator} is set
|
|
*/
|
|
@Override
|
|
public void onEmulatorSet() {
|
|
if (!mTerminalCursorBlinkerStateAlreadySet) {
|
|
// Start terminal cursor blinking if enabled
|
|
// We need to wait for the first session to be attached that's set in
|
|
// TermuxActivity.onServiceConnected() and then the multiple calls to TerminalView.updateSize()
|
|
// where the final one eventually sets the mEmulator when width/height is not 0. Otherwise
|
|
// blinker will not start again if TermuxActivity is started again after exiting it with
|
|
// double back press. Check TerminalView.setTerminalCursorBlinkerState().
|
|
setTerminalCursorBlinkerState(true);
|
|
mTerminalCursorBlinkerStateAlreadySet = true;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
public float onScale(float scale) {
|
|
if (scale < 0.9f || scale > 1.1f) {
|
|
boolean increase = scale > 1.f;
|
|
changeFontSize(increase);
|
|
return 1.0f;
|
|
}
|
|
return scale;
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
public void onSingleTapUp(MotionEvent e) {
|
|
TerminalEmulator term = mActivity.getCurrentSession().getEmulator();
|
|
|
|
if (mActivity.getProperties().shouldOpenTerminalTranscriptURLOnClick()) {
|
|
int[] columnAndRow = mActivity.getTerminalView().getColumnAndRow(e, true);
|
|
String wordAtTap = term.getScreen().getWordAtLocation(columnAndRow[0], columnAndRow[1]);
|
|
LinkedHashSet<CharSequence> urlSet = UrlUtils.extractUrls(wordAtTap);
|
|
|
|
if (!urlSet.isEmpty()) {
|
|
String url = (String) urlSet.iterator().next();
|
|
ShareUtils.openURL(mActivity, url);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!term.isMouseTrackingActive() && !e.isFromSource(InputDevice.SOURCE_MOUSE)) {
|
|
if (!KeyboardUtils.areDisableSoftKeyboardFlagsSet(mActivity))
|
|
KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
|
|
else
|
|
Logger.logVerbose(LOG_TAG, "Not showing soft keyboard onSingleTapUp since its disabled");
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean shouldBackButtonBeMappedToEscape() {
|
|
return mActivity.getProperties().isBackKeyTheEscapeKey();
|
|
}
|
|
|
|
@Override
|
|
public boolean shouldEnforceCharBasedInput() {
|
|
return mActivity.getProperties().isEnforcingCharBasedInput();
|
|
}
|
|
|
|
@Override
|
|
public boolean shouldUseCtrlSpaceWorkaround() {
|
|
return mActivity.getProperties().isUsingCtrlSpaceWorkaround();
|
|
}
|
|
|
|
@Override
|
|
public boolean isTerminalViewSelected() {
|
|
return mActivity.getTerminalToolbarViewPager() == null || mActivity.isTerminalViewSelected();
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
public void copyModeChanged(boolean copyMode) {
|
|
// Disable drawer while copying.
|
|
mActivity.getDrawer().setDrawerLockMode(copyMode ? DrawerLayout.LOCK_MODE_LOCKED_CLOSED : DrawerLayout.LOCK_MODE_UNLOCKED);
|
|
}
|
|
|
|
|
|
|
|
@SuppressLint("RtlHardcoded")
|
|
@Override
|
|
public boolean onKeyDown(int keyCode, KeyEvent e, TerminalSession currentSession) {
|
|
if (handleVirtualKeys(keyCode, e, true)) return true;
|
|
|
|
if (keyCode == KeyEvent.KEYCODE_ENTER && !currentSession.isRunning()) {
|
|
mTermuxTerminalSessionClient.removeFinishedSession(currentSession);
|
|
return true;
|
|
} else if (!mActivity.getProperties().areHardwareKeyboardShortcutsDisabled() &&
|
|
e.isCtrlPressed() && e.isAltPressed()) {
|
|
// Get the unmodified code point:
|
|
int unicodeChar = e.getUnicodeChar(0);
|
|
|
|
if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN || unicodeChar == 'n'/* next */) {
|
|
mTermuxTerminalSessionClient.switchToSession(true);
|
|
} else if (keyCode == KeyEvent.KEYCODE_DPAD_UP || unicodeChar == 'p' /* previous */) {
|
|
mTermuxTerminalSessionClient.switchToSession(false);
|
|
} else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
|
|
mActivity.getDrawer().openDrawer(Gravity.LEFT);
|
|
} else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
|
|
mActivity.getDrawer().closeDrawers();
|
|
} else if (unicodeChar == 'k'/* keyboard */) {
|
|
onToggleSoftKeyboardRequest();
|
|
} else if (unicodeChar == 'm'/* menu */) {
|
|
mActivity.getTerminalView().showContextMenu();
|
|
} else if (unicodeChar == 'r'/* rename */) {
|
|
mTermuxTerminalSessionClient.renameSession(currentSession);
|
|
} else if (unicodeChar == 'c'/* create */) {
|
|
mTermuxTerminalSessionClient.addNewSession(false, null);
|
|
} else if (unicodeChar == 'u' /* urls */) {
|
|
showUrlSelection();
|
|
} else if (unicodeChar == 'v') {
|
|
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
|
|
changeFontSize(true);
|
|
} else if (unicodeChar == '-') {
|
|
changeFontSize(false);
|
|
} else if (unicodeChar >= '1' && unicodeChar <= '9') {
|
|
int index = unicodeChar - '1';
|
|
mTermuxTerminalSessionClient.switchToSession(index);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
public boolean onKeyUp(int keyCode, KeyEvent e) {
|
|
// If emulator is not set, like if bootstrap installation failed and user dismissed the error
|
|
// dialog, then just exit the activity, otherwise they will be stuck in a broken state.
|
|
if (keyCode == KeyEvent.KEYCODE_BACK && mActivity.getTerminalView().mEmulator == null) {
|
|
mActivity.finishActivityIfNotFinishing();
|
|
return true;
|
|
}
|
|
|
|
return handleVirtualKeys(keyCode, e, false);
|
|
}
|
|
|
|
/** Handle dedicated volume buttons as virtual keys if applicable. */
|
|
private boolean handleVirtualKeys(int keyCode, KeyEvent event, boolean down) {
|
|
InputDevice inputDevice = event.getDevice();
|
|
if (mActivity.getProperties().areVirtualVolumeKeysDisabled()) {
|
|
return false;
|
|
} else if (inputDevice != null && inputDevice.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC) {
|
|
// Do not steal dedicated buttons from a full external keyboard.
|
|
return false;
|
|
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
|
|
mVirtualControlKeyDown = down;
|
|
return true;
|
|
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
|
|
mVirtualFnKeyDown = down;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
public boolean readControlKey() {
|
|
return readExtraKeysSpecialButton(SpecialButton.CTRL) || mVirtualControlKeyDown;
|
|
}
|
|
|
|
@Override
|
|
public boolean readAltKey() {
|
|
return readExtraKeysSpecialButton(SpecialButton.ALT);
|
|
}
|
|
|
|
@Override
|
|
public boolean readShiftKey() {
|
|
return readExtraKeysSpecialButton(SpecialButton.SHIFT);
|
|
}
|
|
|
|
@Override
|
|
public boolean readFnKey() {
|
|
return readExtraKeysSpecialButton(SpecialButton.FN);
|
|
}
|
|
|
|
public boolean readExtraKeysSpecialButton(SpecialButton specialButton) {
|
|
if (mActivity.getExtraKeysView() == null) return false;
|
|
Boolean state = mActivity.getExtraKeysView().readSpecialButton(specialButton, true);
|
|
if (state == null) {
|
|
Logger.logError(LOG_TAG,"Failed to read an unregistered " + specialButton + " special button value from extra keys.");
|
|
return false;
|
|
}
|
|
return state;
|
|
}
|
|
|
|
@Override
|
|
public boolean onLongPress(MotionEvent event) {
|
|
return false;
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
public boolean onCodePoint(final int codePoint, boolean ctrlDown, TerminalSession session) {
|
|
if (mVirtualFnKeyDown) {
|
|
int resultingKeyCode = -1;
|
|
int resultingCodePoint = -1;
|
|
boolean altDown = false;
|
|
int lowerCase = Character.toLowerCase(codePoint);
|
|
switch (lowerCase) {
|
|
// Arrow keys.
|
|
case 'w':
|
|
resultingKeyCode = KeyEvent.KEYCODE_DPAD_UP;
|
|
break;
|
|
case 'a':
|
|
resultingKeyCode = KeyEvent.KEYCODE_DPAD_LEFT;
|
|
break;
|
|
case 's':
|
|
resultingKeyCode = KeyEvent.KEYCODE_DPAD_DOWN;
|
|
break;
|
|
case 'd':
|
|
resultingKeyCode = KeyEvent.KEYCODE_DPAD_RIGHT;
|
|
break;
|
|
|
|
// Page up and down.
|
|
case 'p':
|
|
resultingKeyCode = KeyEvent.KEYCODE_PAGE_UP;
|
|
break;
|
|
case 'n':
|
|
resultingKeyCode = KeyEvent.KEYCODE_PAGE_DOWN;
|
|
break;
|
|
|
|
// Some special keys:
|
|
case 't':
|
|
resultingKeyCode = KeyEvent.KEYCODE_TAB;
|
|
break;
|
|
case 'i':
|
|
resultingKeyCode = KeyEvent.KEYCODE_INSERT;
|
|
break;
|
|
case 'h':
|
|
resultingCodePoint = '~';
|
|
break;
|
|
|
|
// Special characters to input.
|
|
case 'u':
|
|
resultingCodePoint = '_';
|
|
break;
|
|
case 'l':
|
|
resultingCodePoint = '|';
|
|
break;
|
|
|
|
// Function keys.
|
|
case '1':
|
|
case '2':
|
|
case '3':
|
|
case '4':
|
|
case '5':
|
|
case '6':
|
|
case '7':
|
|
case '8':
|
|
case '9':
|
|
resultingKeyCode = (codePoint - '1') + KeyEvent.KEYCODE_F1;
|
|
break;
|
|
case '0':
|
|
resultingKeyCode = KeyEvent.KEYCODE_F10;
|
|
break;
|
|
|
|
// Other special keys.
|
|
case 'e':
|
|
resultingCodePoint = /*Escape*/ 27;
|
|
break;
|
|
case '.':
|
|
resultingCodePoint = /*^.*/ 28;
|
|
break;
|
|
|
|
case 'b': // alt+b, jumping backward in readline.
|
|
case 'f': // alf+f, jumping forward in readline.
|
|
case 'x': // alt+x, common in emacs.
|
|
resultingCodePoint = lowerCase;
|
|
altDown = true;
|
|
break;
|
|
|
|
// Volume control.
|
|
case 'v':
|
|
resultingCodePoint = -1;
|
|
AudioManager audio = (AudioManager) mActivity.getSystemService(Context.AUDIO_SERVICE);
|
|
audio.adjustSuggestedStreamVolume(AudioManager.ADJUST_SAME, AudioManager.USE_DEFAULT_STREAM_TYPE, AudioManager.FLAG_SHOW_UI);
|
|
break;
|
|
|
|
// Writing mode:
|
|
case 'q':
|
|
case 'k':
|
|
mActivity.toggleTerminalToolbar();
|
|
mVirtualFnKeyDown=false; // force disable fn key down to restore keyboard input into terminal view, fixes termux/termux-app#1420
|
|
break;
|
|
}
|
|
|
|
if (resultingKeyCode != -1) {
|
|
TerminalEmulator term = session.getEmulator();
|
|
session.write(KeyHandler.getCode(resultingKeyCode, 0, term.isCursorKeysApplicationMode(), term.isKeypadApplicationMode()));
|
|
} else if (resultingCodePoint != -1) {
|
|
session.writeCodePoint(altDown, resultingCodePoint);
|
|
}
|
|
return true;
|
|
} else if (ctrlDown) {
|
|
if (codePoint == 106 /* Ctrl+j or \n */ && !session.isRunning()) {
|
|
mTermuxTerminalSessionClient.removeFinishedSession(session);
|
|
return true;
|
|
}
|
|
|
|
List<KeyboardShortcut> shortcuts = mActivity.getProperties().getSessionShortcuts();
|
|
if (shortcuts != null && !shortcuts.isEmpty()) {
|
|
int codePointLowerCase = Character.toLowerCase(codePoint);
|
|
for (int i = shortcuts.size() - 1; i >= 0; i--) {
|
|
KeyboardShortcut shortcut = shortcuts.get(i);
|
|
if (codePointLowerCase == shortcut.codePoint) {
|
|
switch (shortcut.shortcutAction) {
|
|
case TermuxPropertyConstants.ACTION_SHORTCUT_CREATE_SESSION:
|
|
mTermuxTerminalSessionClient.addNewSession(false, null);
|
|
return true;
|
|
case TermuxPropertyConstants.ACTION_SHORTCUT_NEXT_SESSION:
|
|
mTermuxTerminalSessionClient.switchToSession(true);
|
|
return true;
|
|
case TermuxPropertyConstants.ACTION_SHORTCUT_PREVIOUS_SESSION:
|
|
mTermuxTerminalSessionClient.switchToSession(false);
|
|
return true;
|
|
case TermuxPropertyConstants.ACTION_SHORTCUT_RENAME_SESSION:
|
|
mTermuxTerminalSessionClient.renameSession(mActivity.getCurrentSession());
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
|
|
public void changeFontSize(boolean increase) {
|
|
mActivity.getPreferences().changeFontSize(increase);
|
|
mActivity.getTerminalView().setTextSize(mActivity.getPreferences().getFontSize());
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Called when user requests the soft keyboard to be toggled via "KEYBOARD" toggle button in
|
|
* drawer or extra keys, or with ctrl+alt+k hardware keyboard shortcut.
|
|
*/
|
|
public void onToggleSoftKeyboardRequest() {
|
|
// If soft keyboard toggle behaviour is enable/disabled
|
|
if (mActivity.getProperties().shouldEnableDisableSoftKeyboardOnToggle()) {
|
|
// If soft keyboard is visible
|
|
if (!KeyboardUtils.areDisableSoftKeyboardFlagsSet(mActivity)) {
|
|
Logger.logVerbose(LOG_TAG, "Disabling soft keyboard on toggle");
|
|
mActivity.getPreferences().setSoftKeyboardEnabled(false);
|
|
KeyboardUtils.disableSoftKeyboard(mActivity, mActivity.getTerminalView());
|
|
} else {
|
|
// Show with a delay, otherwise pressing keyboard toggle won't show the keyboard after
|
|
// switching back from another app if keyboard was previously disabled by user.
|
|
// Also request focus, since it wouldn't have been requested at startup by
|
|
// setSoftKeyboardState if keyboard was disabled. #2112
|
|
Logger.logVerbose(LOG_TAG, "Enabling soft keyboard on toggle");
|
|
mActivity.getPreferences().setSoftKeyboardEnabled(true);
|
|
KeyboardUtils.clearDisableSoftKeyboardFlags(mActivity);
|
|
if(mShowSoftKeyboardWithDelayOnce) {
|
|
mShowSoftKeyboardWithDelayOnce = false;
|
|
mActivity.getTerminalView().postDelayed(getShowSoftKeyboardRunnable(), 500);
|
|
mActivity.getTerminalView().requestFocus();
|
|
} else
|
|
KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
|
|
}
|
|
}
|
|
// If soft keyboard toggle behaviour is show/hide
|
|
else {
|
|
// If soft keyboard is disabled by user for Termux
|
|
if (!mActivity.getPreferences().isSoftKeyboardEnabled()) {
|
|
Logger.logVerbose(LOG_TAG, "Maintaining disabled soft keyboard on toggle");
|
|
KeyboardUtils.disableSoftKeyboard(mActivity, mActivity.getTerminalView());
|
|
} else {
|
|
Logger.logVerbose(LOG_TAG, "Showing/Hiding soft keyboard on toggle");
|
|
KeyboardUtils.clearDisableSoftKeyboardFlags(mActivity);
|
|
KeyboardUtils.toggleSoftKeyboard(mActivity);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void setSoftKeyboardState(boolean isStartup, boolean isReloadTermuxProperties) {
|
|
boolean noShowKeyboard = false;
|
|
|
|
// Requesting terminal view focus is necessary regardless of if soft keyboard is to be
|
|
// disabled or hidden at startup, otherwise if hardware keyboard is attached and user
|
|
// starts typing on hardware keyboard without tapping on the terminal first, then a colour
|
|
// tint will be added to the terminal as highlight for the focussed view. Test with a light
|
|
// theme.
|
|
|
|
// If soft keyboard is disabled by user for Termux (check function docs for Termux behaviour info)
|
|
if (KeyboardUtils.shouldSoftKeyboardBeDisabled(mActivity,
|
|
mActivity.getPreferences().isSoftKeyboardEnabled(),
|
|
mActivity.getPreferences().isSoftKeyboardEnabledOnlyIfNoHardware())) {
|
|
Logger.logVerbose(LOG_TAG, "Maintaining disabled soft keyboard");
|
|
KeyboardUtils.disableSoftKeyboard(mActivity, mActivity.getTerminalView());
|
|
mActivity.getTerminalView().requestFocus();
|
|
noShowKeyboard = true;
|
|
// Delay is only required if onCreate() is called like when Termux app is exited with
|
|
// double back press, not when Termux app is switched back from another app and keyboard
|
|
// toggle is pressed to enable keyboard
|
|
if (isStartup && mActivity.isOnResumeAfterOnCreate())
|
|
mShowSoftKeyboardWithDelayOnce = true;
|
|
} else {
|
|
// Set flag to automatically push up TerminalView when keyboard is opened instead of showing over it
|
|
KeyboardUtils.setSoftInputModeAdjustResize(mActivity);
|
|
|
|
// Clear any previous flags to disable soft keyboard in case setting updated
|
|
KeyboardUtils.clearDisableSoftKeyboardFlags(mActivity);
|
|
|
|
// If soft keyboard is to be hidden on startup
|
|
if (isStartup && mActivity.getProperties().shouldSoftKeyboardBeHiddenOnStartup()) {
|
|
Logger.logVerbose(LOG_TAG, "Hiding soft keyboard on startup");
|
|
// Required to keep keyboard hidden when Termux app is switched back from another app
|
|
KeyboardUtils.setSoftKeyboardAlwaysHiddenFlags(mActivity);
|
|
|
|
KeyboardUtils.hideSoftKeyboard(mActivity, mActivity.getTerminalView());
|
|
mActivity.getTerminalView().requestFocus();
|
|
noShowKeyboard = true;
|
|
// Required to keep keyboard hidden on app startup
|
|
mShowSoftKeyboardIgnoreOnce = true;
|
|
}
|
|
}
|
|
|
|
mActivity.getTerminalView().setOnFocusChangeListener(new View.OnFocusChangeListener() {
|
|
@Override
|
|
public void onFocusChange(View view, boolean hasFocus) {
|
|
// Force show soft keyboard if TerminalView or toolbar text input view has
|
|
// focus and close it if they don't
|
|
boolean textInputViewHasFocus = false;
|
|
final EditText textInputView = mActivity.findViewById(R.id.terminal_toolbar_text_input);
|
|
if (textInputView != null) textInputViewHasFocus = textInputView.hasFocus();
|
|
|
|
if (hasFocus || textInputViewHasFocus) {
|
|
if (mShowSoftKeyboardIgnoreOnce) {
|
|
mShowSoftKeyboardIgnoreOnce = false; return;
|
|
}
|
|
Logger.logVerbose(LOG_TAG, "Showing soft keyboard on focus change");
|
|
} else {
|
|
Logger.logVerbose(LOG_TAG, "Hiding soft keyboard on focus change");
|
|
}
|
|
|
|
KeyboardUtils.setSoftKeyboardVisibility(getShowSoftKeyboardRunnable(), mActivity, mActivity.getTerminalView(), hasFocus || textInputViewHasFocus);
|
|
}
|
|
});
|
|
|
|
// Do not force show soft keyboard if termux-reload-settings command was run with hardware keyboard
|
|
// or soft keyboard is to be hidden or is disabled
|
|
if (!isReloadTermuxProperties && !noShowKeyboard) {
|
|
// Request focus for TerminalView
|
|
// Also show the keyboard, since onFocusChange will not be called if TerminalView already
|
|
// had focus on startup to show the keyboard, like when opening url with context menu
|
|
// "Select URL" long press and returning to Termux app with back button. This
|
|
// will also show keyboard even if it was closed before opening url. #2111
|
|
Logger.logVerbose(LOG_TAG, "Requesting TerminalView focus and showing soft keyboard");
|
|
mActivity.getTerminalView().requestFocus();
|
|
mActivity.getTerminalView().postDelayed(getShowSoftKeyboardRunnable(), 300);
|
|
}
|
|
}
|
|
|
|
private Runnable getShowSoftKeyboardRunnable() {
|
|
if (mShowSoftKeyboardRunnable == null) {
|
|
mShowSoftKeyboardRunnable = () -> {
|
|
KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
|
|
};
|
|
}
|
|
return mShowSoftKeyboardRunnable;
|
|
}
|
|
|
|
|
|
|
|
public void setTerminalCursorBlinkerState(boolean start) {
|
|
if (start) {
|
|
// If set/update the cursor blinking rate is successful, then enable cursor blinker
|
|
if (mActivity.getTerminalView().setTerminalCursorBlinkerRate(mActivity.getProperties().getTerminalCursorBlinkRate()))
|
|
mActivity.getTerminalView().setTerminalCursorBlinkerState(true, true);
|
|
else
|
|
Logger.logError(LOG_TAG,"Failed to start cursor blinker");
|
|
} else {
|
|
// Disable cursor blinker
|
|
mActivity.getTerminalView().setTerminalCursorBlinkerState(false, true);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public void shareSessionTranscript() {
|
|
TerminalSession session = mActivity.getCurrentSession();
|
|
if (session == null) return;
|
|
|
|
String transcriptText = ShellUtils.getTerminalSessionTranscriptText(session, false, true);
|
|
if (transcriptText == null) return;
|
|
|
|
try {
|
|
// See https://github.com/termux/termux-app/issues/1166.
|
|
Intent intent = new Intent(Intent.ACTION_SEND);
|
|
intent.setType("text/plain");
|
|
transcriptText = DataUtils.getTruncatedCommandOutput(transcriptText, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, true, false).trim();
|
|
intent.putExtra(Intent.EXTRA_TEXT, transcriptText);
|
|
intent.putExtra(Intent.EXTRA_SUBJECT, mActivity.getString(R.string.title_share_transcript));
|
|
mActivity.startActivity(Intent.createChooser(intent, mActivity.getString(R.string.title_share_transcript_with)));
|
|
} catch (Exception e) {
|
|
Logger.logStackTraceWithMessage(LOG_TAG,"Failed to get share session transcript of length " + transcriptText.length(), e);
|
|
}
|
|
}
|
|
|
|
public void showUrlSelection() {
|
|
TerminalSession session = mActivity.getCurrentSession();
|
|
if (session == null) return;
|
|
|
|
String text = ShellUtils.getTerminalSessionTranscriptText(session, true, true);
|
|
|
|
LinkedHashSet<CharSequence> urlSet = UrlUtils.extractUrls(text);
|
|
if (urlSet.isEmpty()) {
|
|
new AlertDialog.Builder(mActivity).setMessage(R.string.title_select_url_none_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.msg_select_url_copied_to_clipboard, Toast.LENGTH_LONG).show();
|
|
}).setTitle(R.string.title_select_url_dialog).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];
|
|
ShareUtils.openURL(mActivity, url);
|
|
return true;
|
|
});
|
|
});
|
|
|
|
dialog.show();
|
|
}
|
|
|
|
public void reportIssueFromTranscript() {
|
|
TerminalSession session = mActivity.getCurrentSession();
|
|
if (session == null) return;
|
|
|
|
final String transcriptText = ShellUtils.getTerminalSessionTranscriptText(session, false, true);
|
|
if (transcriptText == null) return;
|
|
|
|
MessageDialogUtils.showMessage(mActivity, TermuxConstants.TERMUX_APP_NAME + " Report Issue",
|
|
mActivity.getString(R.string.msg_add_termux_debug_info),
|
|
mActivity.getString(R.string.action_yes), (dialog, which) -> reportIssueFromTranscript(transcriptText, true),
|
|
mActivity.getString(R.string.action_no), (dialog, which) -> reportIssueFromTranscript(transcriptText, false),
|
|
null);
|
|
}
|
|
|
|
private void reportIssueFromTranscript(String transcriptText, boolean addTermuxDebugInfo) {
|
|
Logger.showToast(mActivity, mActivity.getString(R.string.msg_generating_report), true);
|
|
|
|
new Thread() {
|
|
@Override
|
|
public void run() {
|
|
StringBuilder reportString = new StringBuilder();
|
|
|
|
String title = TermuxConstants.TERMUX_APP_NAME + " Report Issue";
|
|
|
|
reportString.append("## Transcript\n");
|
|
reportString.append("\n").append(MarkdownUtils.getMarkdownCodeForString(transcriptText, true));
|
|
reportString.append("\n##\n");
|
|
|
|
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(mActivity, true));
|
|
reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(mActivity));
|
|
|
|
String termuxAptInfo = TermuxUtils.geAPTInfoMarkdownString(mActivity);
|
|
if (termuxAptInfo != null)
|
|
reportString.append("\n\n").append(termuxAptInfo);
|
|
|
|
if (addTermuxDebugInfo) {
|
|
String termuxDebugInfo = TermuxUtils.getTermuxDebugMarkdownString(mActivity);
|
|
if (termuxDebugInfo != null)
|
|
reportString.append("\n\n").append(termuxDebugInfo);
|
|
}
|
|
|
|
String userActionName = UserAction.REPORT_ISSUE_FROM_TRANSCRIPT.getName();
|
|
ReportActivity.startReportActivity(mActivity,
|
|
new ReportInfo(userActionName,
|
|
TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY_NAME, title, null,
|
|
reportString.toString(), "\n\n" + TermuxUtils.getReportIssueMarkdownString(mActivity),
|
|
false,
|
|
userActionName,
|
|
Environment.getExternalStorageDirectory() + "/" +
|
|
FileUtils.sanitizeFileName(TermuxConstants.TERMUX_APP_NAME + "-" + userActionName + ".log", true, true)));
|
|
}
|
|
}.start();
|
|
}
|
|
|
|
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());
|
|
}
|
|
|
|
}
|