Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19eb371d23 | ||
|
|
6caaae4fd6 | ||
|
|
ed544102bc | ||
|
|
8f1ab1bc17 | ||
|
|
50337cbf9d | ||
|
|
fa9ea2db5c | ||
|
|
7a659ebd21 | ||
|
|
54bc1ed791 | ||
|
|
fe4365c94b | ||
|
|
0c13ea1bd4 | ||
|
|
862b461a07 | ||
|
|
845976be0f | ||
|
|
60bdaa3bf6 | ||
|
|
eeb873f4e4 | ||
|
|
207ddf9fdc | ||
|
|
5ca82ea095 | ||
|
|
913c474d32 | ||
|
|
657c270d97 | ||
|
|
7763931035 | ||
|
|
50005bc794 | ||
|
|
96f5ed985a | ||
|
|
c3aa9d9662 | ||
|
|
47634ca237 | ||
|
|
d9ec1bf40b | ||
|
|
bc1b742a36 | ||
|
|
86e2945069 | ||
|
|
961e06379b | ||
|
|
468185efb3 | ||
|
|
e006e36dd0 | ||
|
|
dd38965c46 | ||
|
|
79d56b778d | ||
|
|
2326c52199 | ||
|
|
f907684ef2 | ||
|
|
9d37461ac7 | ||
|
|
4de0f98fa4 | ||
|
|
f153a72592 | ||
|
|
b0f4efb0bc | ||
|
|
5c03c2d77e |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -30,6 +30,7 @@ local.properties
|
||||
.idea/modules.xml
|
||||
.idea/scopes/scope_settings.xml
|
||||
.idea/vcs.xml
|
||||
.idea/dictionaries/
|
||||
*.iml
|
||||
|
||||
# OS-specific files
|
||||
|
||||
1
.idea/gradle.xml
generated
1
.idea/gradle.xml
generated
@@ -3,6 +3,7 @@
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="disableWrapperSourceDistributionNotification" value="true" />
|
||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="1.8" />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
Termux app
|
||||
==========
|
||||
[](https://travis-ci.org/termux/termux-app)
|
||||
[](https://gitter.im/termux/termux)
|
||||
|
||||
|
||||
Termux is an Android terminal app and Linux environment.
|
||||
|
||||
@@ -9,17 +9,17 @@ android {
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
jni.srcDirs = []
|
||||
}
|
||||
main {
|
||||
jni.srcDirs = []
|
||||
}
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.termux"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 23
|
||||
versionCode 22
|
||||
versionName "0.22"
|
||||
versionCode 26
|
||||
versionName "0.26"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
||||
@@ -39,12 +39,14 @@
|
||||
<activity
|
||||
android:name="com.termux.app.TermuxHelpActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/application_help" />
|
||||
android:theme="@android:style/Theme.Material.Light.DarkActionBar"
|
||||
android:parentActivityName=".app.TermuxActivity"
|
||||
android:label="@string/application_name" />
|
||||
|
||||
<activity
|
||||
android:name="com.termux.filepicker.TermuxFilePickerActivity"
|
||||
android:label="@string/application_name"
|
||||
android:theme="@android:style/Theme.Material"
|
||||
android:theme="@android:style/Theme.Material.Light.DarkActionBar"
|
||||
android:noHistory="true">
|
||||
<intent-filter>
|
||||
<!--
|
||||
|
||||
@@ -1,229 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<title>Termux Help</title>
|
||||
|
||||
<style>
|
||||
html { font-family: 'sans-serif-light', sans-serif; height: 100%; margin: auto; padding: 0; color: black; background-color: white; }
|
||||
.page { max-width: 820px; margin: auto; padding: 0 1em; }
|
||||
body { margin-left: auto; margin-right: auto; margin-top: 0; padding: 0; width: 100%; }
|
||||
p { font-size: 16px; line-height: 1.3em; }
|
||||
ul.index { padding-left: 0; }
|
||||
.index li { list-style-type: none; line-height: 1.8em; }
|
||||
dt { margin-left: 1em; list-style-type: bullet; }
|
||||
a, a:visited { color: #0000EE }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page help">
|
||||
<h1 id="index">Termux Help</h1>
|
||||
|
||||
<ul class="index">
|
||||
<li><a href="#introduction">Introduction</a></li>
|
||||
<li><a href="#user_interface">User interface</a></li>
|
||||
<li><a href="#touch_keyboard">Using a touch keyboard</a></li>
|
||||
<li><a href="#hardware_keyboard">Using a hardware keyboard</a></li>
|
||||
<li><a href="#package_management">Package management</a></li>
|
||||
<li><a href="#text_editing">Text editing</a></li>
|
||||
<li><a href="#using_ssh">Using SSH</a></li>
|
||||
<li><a href="#interactive_shells">Interactive shells</a></li>
|
||||
<li><a href="#termux_android">Termux and Android</a></li>
|
||||
<li><a href="#add_on_api">Add-on: API</a></li>
|
||||
<li><a href="#add_on_float">Add-on: Float</a></li>
|
||||
<li><a href="#add_on_styling">Add-on: Styling</a></li>
|
||||
<li><a href="#add_on_widget">Add-on: Widget</a></li>
|
||||
<li><a href="#source_and_licenses">Source and licenses</a>
|
||||
</ul>
|
||||
|
||||
<h2 id="introduction">Introduction</h2>
|
||||
<p>Termux is a terminal emulator for Android combined with a collection of packages for command line software. This help
|
||||
explains both the terminal interface and the packaging tool available from inside the terminal.</p>
|
||||
<p>Want to ask a question, report a bug or have an idea for a new package or feature?
|
||||
Visit the <a href="https://plus.google.com/communities/101692629528551299417">Google+ Termux Community</a>!</p>
|
||||
|
||||
<h2 id="user_interface">User interface</h2>
|
||||
<p>At launch Termux shows a terminal interface, whose text size can be adjusted by pinch zooming or double tapping
|
||||
and pulling the content towards or from you.</p>
|
||||
<p>Besides the terminal (with keyboard shortcuts explained below) there are three additional interface elements available:
|
||||
A <strong>context menu</strong>, <strong>navigation drawer</strong>
|
||||
and <strong>notification</strong>.</p>
|
||||
<p>The <strong>context menu</strong> can be shown by long pressing anywhere on the terminal. It provides menu entries for:</p>
|
||||
<ul>
|
||||
<li>Selecting and pasting text.</li>
|
||||
<li>Sharing text from the terminal to other apps (e.g. email or SMS)</li>
|
||||
<li>Resetting the terminal if it gets stuck.</li>
|
||||
<li>Switching the terminal to full-screen.</li>
|
||||
<li>Hangup (exiting the current terminal session).</li>
|
||||
<li>Styling the terminal by selecting a font and a color scheme.</li>
|
||||
<li>Showing this help page.</li>
|
||||
</ul>
|
||||
<p>The <strong>navigation drawer</strong> is revealed by swiping from the left part of the screen. It has three
|
||||
elements:</p>
|
||||
<ul>
|
||||
<li>A list of sessions. Clicking on a session shows it in the terminal while long pressing allows you to specify a session title.</li>
|
||||
<li>A button to toggle visibility of the touch keyboard.</li>
|
||||
<li>A button to create new terminal sessions (long press for creating a named session or a fail-safe one).</li>
|
||||
</ul>
|
||||
<p>The <strong>notification</strong>, available when a terminal session is running, is available by pulling down the notification menu.
|
||||
Pressing the notification leads to the most current terminal session. The notification may also be expanded
|
||||
(by pinch-zooming or performing a single-finger glide) to expose three actions:</p>
|
||||
<ul>
|
||||
<li>Exiting all running terminal sessions.</li>
|
||||
<li>Use a wake lock to avoid entering sleep mode.</li>
|
||||
<li>Use a high performance wifi lock to maximize wifi performance.</li>
|
||||
</ul>
|
||||
<p>With a wake or wifi lock held the notification and Termux background processes will be available even if no terminal
|
||||
session is running, which allows server and other background processes to run more reliably.</p>
|
||||
|
||||
<h2 id="touch_keyboard">Using a touch keyboard</h2>
|
||||
<p>Using the Ctrl key is necessary for working with a terminal - but most touch keyboards
|
||||
does not include one. For that purpose Termux uses the <em>Volume down</em> button to emulate
|
||||
the Ctrl key. For example, pressing <em>Volume down+L</em> on a touch keyboard sends the same input as
|
||||
pressing <em>Ctrl+L</em> on a hardware keyboard. The result of using Ctrl in combination
|
||||
with a key depends on which program is used, but for many command line tools the following
|
||||
shortcuts works:</p>
|
||||
<ul>
|
||||
<li>Ctrl+A → Move cursor to the beginning of line.</li>
|
||||
<li>Ctrl+C → Abort (send SIGINT to) current process.</li>
|
||||
<li>Ctrl+D → Logout of a terminal session.</li>
|
||||
<li>Ctrl+E → Move cursor to the end of line.</li>
|
||||
<li>Ctrl+K → Delete from cursor to the end of line.</li>
|
||||
<li>Ctrl+L → Clear the terminal.</li>
|
||||
<li>Ctrl+Z → Suspend (send SIGTSTP to) current process.</li>
|
||||
</ul>
|
||||
<p>The <em>Volume up</em> key also serves as a special key to produce certain input:</p>
|
||||
<ul>
|
||||
<li>Volume Up+L → | (the pipe character).</li>
|
||||
<li>Volume Up+E → Escape key.</li>
|
||||
<li>Volume Up+T → Tab key.</li>
|
||||
<li>Volume Up+1 → F1 (and Volume Up+2 → F2, etc).</li>
|
||||
<li>Volume Up+B → Alt+B, back a word when using readline.</li>
|
||||
<li>Volume Up+F → Alt+F, forward a word when using readline.</li>
|
||||
<li>Volume Up+W → Up arrow key.</li>
|
||||
<li>Volume Up+A → Left arrow key.</li>
|
||||
<li>Volume Up+S → Down arrow key.</li>
|
||||
<li>Volume Up+D → Right arrow key.</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="hardware_keyboard">Using a hardware keyboard</h2>
|
||||
<p>The following shortcuts are available when using Termux with a hardware (e.g. bluetooth) keyboard by combining them with <em>Ctrl+Shift</em>:</p>
|
||||
<ul>
|
||||
<li>'C' → Create new session</li>
|
||||
<li>'R' → Rename current session</li>
|
||||
<li>Down arrow (or 'N') → Next session</li>
|
||||
<li>Up arrow (or 'P') → Previous session</li>
|
||||
<li>Right arrow → Open drawer</li>
|
||||
<li>Left arrow → Close drawer</li>
|
||||
<li>'F' → Toggle full screen</li>
|
||||
<li>'M' → Show menu</li>
|
||||
<li>'V' → Paste</li>
|
||||
<li>+/- → Adjust text size</li>
|
||||
<li>1-9 → Go to numbered session</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="package_management">Package management</h2>
|
||||
<p>A minimal base system consisting of the Apt package manager and the busybox collection of system utilities
|
||||
is installed when first starting Termux. Additional packages are available using the apt command:</p>
|
||||
<dl>
|
||||
<dt>apt update</dt><dd>Updates the list of available packages. This commands needs to be run initially directly after installation
|
||||
and regularly afterwards to receive updates.</dd>
|
||||
<dt>apt search <query></dt><dd>Search among available packages.</dd>
|
||||
<dt>apt install <package></dt><dd>Install a new package.</dd>
|
||||
<dt>apt upgrade</dt><dd>Upgrade outdated packages. For Apt to know about newer packages you will need to update the package index, so you will normally want to run <em>apt update</em> before upgrading.</dd>
|
||||
<dt>apt show <package></dt><dd>Show information about a package.</dd>
|
||||
<dt>apt list</dt><dd>List all available packages.</dd>
|
||||
<dt>apt list --installed</dt><dd>List all installed packages.</dd>
|
||||
<dt>apt remove <package></dt><dd>Remove an installed package.</dd>
|
||||
</dl>
|
||||
|
||||
<p>Apt as a package manager uses a package format named <em>dpkg</em>. Normally direct use of dpkg is not necessary, but the
|
||||
following two commands may be of use:</p>
|
||||
<dl>
|
||||
<dt>dpkg -L <package></dt>
|
||||
<dd>List installed files of a package.</dd>
|
||||
<dt>dpkg --verify</dt>
|
||||
<dd>Verify the integrity of installed packages.</dd>
|
||||
</dl>
|
||||
<p>View the apt manual page (execute <em>apt install man</em> to install a man page viewer first) for more information.</p>
|
||||
|
||||
<h2 id="text_editing">Text editing</h2>
|
||||
<p>By default the busybox version of <em>vi</em> is available. This is a barebone and somewhat unfriendly editor -
|
||||
install <a href="http://www.nano-editor.org/dist/v2.2/nano.html">nano</a> for a more straight-forward editor and
|
||||
<a href="http://vimdoc.sourceforge.net/htmldoc/usr_toc.html">vim</a> for a more powerful one.</p>
|
||||
|
||||
<h2 id="using_ssh">Using SSH</h2>
|
||||
<p>By installing the <strong>openssh</strong> package (by executing <em>apt install openssh</em>) you may SSH into remote systems,
|
||||
optionally putting private keys or configuration under $HOME/.ssh/.</p>
|
||||
<p>If you wish to use an SSH agent to avoid entering passwords, the Termux openssh package provides
|
||||
a wrapper script named <strong>ssha</strong> (note the 'a' at the end) for ssh which:</p>
|
||||
<ol>
|
||||
<li>Starts the ssh agent if necessary (or connect to it if already running).</li>
|
||||
<li>Runs <strong>ssh-add</strong> if necessary.</li>
|
||||
<li>Runs <strong>ssh</strong> with the provided arguments.</li>
|
||||
</ol>
|
||||
<p>This means that the agent will prompt for a key password at first run, but remember the authorization for subsequent ones.</p>
|
||||
|
||||
<h2 id="interactive_shells">Interactive shells</h2>
|
||||
<p>The base system that is installed when first starting Termux uses the <em>bash</em> shell while zsh is available as
|
||||
an installable alternative:</p>
|
||||
<ul>
|
||||
<li>bash - the default shell on most Linux distributions, with resources such as
|
||||
<a href="http://www.tldp.org/LDP/Bash-Beginners-Guide/html/">Bash Guide for Beginners</a>,
|
||||
the <a href="https://www.gnu.org/software/bash/manual/bash.html">Bash Reference Manual</a>
|
||||
or the <a href="http://www.tldp.org/LDP/abs/html/">Advanced Bash-Scripting Guide</a> available.</li>
|
||||
<li>zsh - a powerful shell with information available at
|
||||
<a href="http://zsh.sourceforge.net/Guide/zshguide.html">A User's Guide to the Z-Shell</a>, the
|
||||
<a href="http://zsh.sourceforge.net/Doc/Release/zsh_toc.html">Z Shell Manual</a> or
|
||||
<a href="http://www.rayninfo.co.uk/tips/zshtips.html">ZSH Tips by ZZapper</a>.
|
||||
After installing zsh through <em>apt install zsh</em>, execute <em>chsh -s zsh</em> to set it as the default login shell when starting Termux
|
||||
(and change back with <em>chsh -s bash</em> if necessary).</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="termux_android">Termux and Android</h2>
|
||||
<p>Termux is designed to cope with the restrictions of running as an ordinary Android app without requiring root, which
|
||||
leads to several differences between Termux and a traditional desktop system. The file system layout is drastically different:</p>
|
||||
<ul>
|
||||
<li>Common folders such as /bin, /usr/, /var and /etc does not exist.</li>
|
||||
<li>The Android system provides a basic non-standard file system hierarchy, where e.g. /system/bin contains some system binaries.</li>
|
||||
<li>The user folder $HOME is inside the private file area exposed to Termux as an ordinary Android app.
|
||||
Uninstalling Termux will cause this file area to be wiped - so save important files outside this area such as in /sdcard
|
||||
or use a version control system such as <em>git</em>.</li>
|
||||
<li>Termux installs its packages in a folder exposed through the $PREFIX environment variable (with e.g. binaries in $PREFIX/bin,
|
||||
and configuration in $PREFIX/etc).</li>
|
||||
<li>Shared libraries are installed in $PREFIX/lib, which are available from binaries due to Termux setting the $LD_LIBRARY_PATH
|
||||
environment variable. These may clash with Android system binaries in /system/bin, which may force LD_LIBRARY_PATH to be
|
||||
cleared before running system binaries.</li>
|
||||
</ul>
|
||||
<p>Besides the file system being different, Termux is running as a single-user system without root - each Android app is running as
|
||||
its own Linux user, so running commands inside Termux may not interfere with other installed applications.</p>
|
||||
<p>Running as non-root implies that ports below 1024 cannot be bound to. Many packages have been configured to have compatible
|
||||
default values - the ftpd, httpd, and sshd servers default to 8021, 8080 and 8022, respectively.</p>
|
||||
|
||||
<h2 id="add_on_api">Add-on: API</h2>
|
||||
<p>The API add-on exposes Android system functionality such as SMS messages, GPS location or the Text-to-speech functionality through command line tools.</p>
|
||||
<ul><li><a href="http://play.google.com/store/apps/details?id=com.termux.api">See more and install from Google Play</a></li></ul>
|
||||
|
||||
<h2 id="add_on_float">Add-on: Float</h2>
|
||||
<p>The Float add-on consists of a floating terminal window visible while running other apps.</p>
|
||||
<ul><li><a href="http://play.google.com/store/apps/details?id=com.termux.window">See more and install from Google Play</a></li></ul>
|
||||
|
||||
<h2 id="add_on_styling">Add-on: Styling</h2>
|
||||
<p>The Styling add-on provides color schemes and fonts to beabeautify and customize the appearance of the Termux terminal.</p>
|
||||
<ul><li><a href="http://play.google.com/store/apps/details?id=com.termux.styling">See more and install from Google Play</a></li></ul>
|
||||
|
||||
<h2 id="add_on_widget">Add-on: Widget</h2>
|
||||
<p>The Widget add-on brings a widget to your homescreen, providing links to run scripts in your $HOME/.shortcuts/ folder.</p>
|
||||
<ul><li><a href="http://play.google.com/store/apps/details?id=com.termux.widget">See more and install from Google Play</a></li></ul>
|
||||
|
||||
<h2 id="source_and_licenses">Source and licenses</h2>
|
||||
<p>Termux uses terminal emulation code from <a href="https://github.com/jackpal/Android-Terminal-Emulator">Terminal Emulator for Android</a>
|
||||
which is under the <a href="https://raw.githubusercontent.com/jackpal/Android-Terminal-Emulator/master/NOTICE">Apache License, Version 2.0</a>.
|
||||
Packages available through Termux are distributed under their respective licenses with scripts and patches used to build them
|
||||
<a href="https://github.com/termux/termux-packages">available on github</a>.</p>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -3,10 +3,15 @@ package com.termux.app;
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.res.Configuration;
|
||||
import android.text.Selection;
|
||||
import android.util.TypedValue;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.ViewGroup.LayoutParams;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
final class DialogUtils {
|
||||
|
||||
@@ -14,10 +19,25 @@ final class DialogUtils {
|
||||
void onTextSet(String text);
|
||||
}
|
||||
|
||||
static void textInput(Activity activity, int titleText, int positiveButtonText, String initialText, final TextSetListener onPositive) {
|
||||
static void textInput(Activity activity, int titleText, int positiveButtonText, String initialText, final TextSetListener onPositive,
|
||||
int neutralButtonText, final TextSetListener onNeutral) {
|
||||
final EditText input = new EditText(activity);
|
||||
input.setSingleLine();
|
||||
if (initialText != null) input.setText(initialText);
|
||||
if (initialText != null) {
|
||||
input.setText(initialText);
|
||||
Selection.setSelection(input.getText(), initialText.length());
|
||||
}
|
||||
|
||||
final AlertDialog[] dialogHolder = new AlertDialog[1];
|
||||
input.setImeActionLabel(activity.getResources().getString(positiveButtonText), KeyEvent.KEYCODE_ENTER);
|
||||
input.setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
||||
@Override
|
||||
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
|
||||
onPositive.onTextSet(input.getText().toString());
|
||||
dialogHolder[0].dismiss();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
float dipInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, activity.getResources().getDisplayMetrics());
|
||||
// https://www.google.com/design/spec/components/dialogs.html#dialogs-specs
|
||||
@@ -27,17 +47,34 @@ final class DialogUtils {
|
||||
LinearLayout layout = new LinearLayout(activity);
|
||||
layout.setOrientation(LinearLayout.VERTICAL);
|
||||
layout.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
|
||||
// layout.setGravity(Gravity.CLIP_VERTICAL);
|
||||
layout.setPadding(paddingTopAndSides, paddingTopAndSides, paddingTopAndSides, paddingBottom);
|
||||
layout.addView(input);
|
||||
|
||||
new AlertDialog.Builder(activity).setTitle(titleText).setView(layout).setPositiveButton(positiveButtonText, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface d, int whichButton) {
|
||||
onPositive.onTextSet(input.getText().toString());
|
||||
}
|
||||
}).setNegativeButton(android.R.string.cancel, null).show();
|
||||
input.requestFocus();
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity)
|
||||
.setTitle(titleText).setView(layout)
|
||||
.setPositiveButton(positiveButtonText, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface d, int whichButton) {
|
||||
onPositive.onTextSet(input.getText().toString());
|
||||
}
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null);
|
||||
|
||||
if (onNeutral != null) {
|
||||
builder.setNeutralButton(neutralButtonText, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
onNeutral.onTextSet(input.getText().toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
dialogHolder[0] = builder.create();
|
||||
if ((activity.getResources().getConfiguration().hardKeyboardHidden & Configuration.HARDKEYBOARDHIDDEN_YES) == 0) {
|
||||
// Show soft keyboard unless hardware keyboard available.
|
||||
dialogHolder[0].getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
|
||||
}
|
||||
dialogHolder[0].show();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,19 +1,8 @@
|
||||
package com.termux.app;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.drawer.DrawerLayout;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
import com.termux.terminal.TerminalSession.SessionChangedCallback;
|
||||
import com.termux.view.TerminalKeyListener;
|
||||
import com.termux.view.TerminalView;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.ActivityNotFoundException;
|
||||
@@ -27,11 +16,15 @@ import android.content.DialogInterface.OnShowListener;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Typeface;
|
||||
import android.media.AudioAttributes;
|
||||
import android.media.SoundPool;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.os.Vibrator;
|
||||
@@ -64,6 +57,19 @@ import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.drawer.DrawerLayout;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
import com.termux.terminal.TerminalSession.SessionChangedCallback;
|
||||
import com.termux.view.TerminalKeyListener;
|
||||
import com.termux.view.TerminalView;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* A terminal emulator activity.
|
||||
*
|
||||
@@ -76,7 +82,8 @@ import android.widget.Toast;
|
||||
*/
|
||||
public final class TermuxActivity extends Activity implements ServiceConnection {
|
||||
|
||||
private static final int CONTEXTMENU_SELECT_ID = 0;
|
||||
private static final int CONTEXTMENU_SELECT_URL_ID = 0;
|
||||
private static final int CONTEXTMENU_SHARE_TRANSCRIPT_ID = 1;
|
||||
private static final int CONTEXTMENU_PASTE_ID = 3;
|
||||
private static final int CONTEXTMENU_KILL_PROCESS_ID = 4;
|
||||
private static final int CONTEXTMENU_RESET_TERMINAL_ID = 5;
|
||||
@@ -86,10 +93,12 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
|
||||
private static final int MAX_SESSIONS = 8;
|
||||
|
||||
private static final int REQUESTCODE_PERMISSION_STORAGE = 1234;
|
||||
|
||||
private static final String RELOAD_STYLE_ACTION = "com.termux.app.reload_style";
|
||||
|
||||
/** The main view of the activity showing the terminal. */
|
||||
@NonNull TerminalView mTerminalView;
|
||||
/** The main view of the activity showing the terminal. Initialized in onCreate(). */
|
||||
@SuppressWarnings("NullableProblems") @NonNull TerminalView mTerminalView;
|
||||
|
||||
final FullScreenHelper mFullScreenHelper = new FullScreenHelper(this);
|
||||
|
||||
@@ -114,17 +123,42 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
*/
|
||||
boolean mIsVisible;
|
||||
|
||||
private final SoundPool mBellSoundPool = new SoundPool.Builder().setMaxStreams(1).setAudioAttributes(
|
||||
new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||
.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build()).build();
|
||||
private int mBellSoundId;
|
||||
|
||||
private final BroadcastReceiver mBroadcastReceiever = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (mIsVisible) {
|
||||
String whatToReload = intent.getStringExtra(RELOAD_STYLE_ACTION);
|
||||
if (whatToReload == null || "colors".equals(whatToReload)) mTerminalView.checkForColors();
|
||||
if (whatToReload == null || "font".equals(whatToReload)) mTerminalView.checkForTypeface();
|
||||
if ("storage".equals(whatToReload)) {
|
||||
if (ensureStoragePermissionGranted()) TermuxInstaller.setupStorageSymlinks(TermuxActivity.this);
|
||||
return;
|
||||
}
|
||||
mTerminalView.checkForFontAndColors();
|
||||
mSettings.reloadFromProperties(TermuxActivity.this);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** For processes to access shared internal storage (/sdcard) we need this permission. */
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
public boolean ensureStoragePermissionGranted() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (checkSelfPermission(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
|
||||
return true;
|
||||
} else {
|
||||
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUESTCODE_PERMISSION_STORAGE);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// Always granted before Android 6.0.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
@@ -185,6 +219,9 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
getDrawer().closeDrawers();
|
||||
} else if (unicodeChar == 'f'/* full screen */) {
|
||||
toggleImmersive();
|
||||
} else if (unicodeChar == 'k'/* keyboard */) {
|
||||
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
|
||||
} else if (unicodeChar == 'm'/* menu */) {
|
||||
mTerminalView.showContextMenu();
|
||||
} else if (unicodeChar == 'r'/* rename */) {
|
||||
@@ -223,54 +260,50 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLongPress(MotionEvent event) {
|
||||
mTerminalView.showContextMenu();
|
||||
public void onSingleTapUp(MotionEvent e) {
|
||||
InputMethodManager mgr = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
mgr.showSoftInput(mTerminalView, InputMethodManager.SHOW_IMPLICIT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSingleTapUp(MotionEvent e) {
|
||||
// Toggle keyboard visibility if tapping with a finger:
|
||||
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, 0);
|
||||
public boolean shouldBackButtonBeMappedToEscape() {
|
||||
return mSettings.mBackIsEscape;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void copyModeChanged(boolean copyMode) {
|
||||
// Disable drawer while copying.
|
||||
getDrawer().setDrawerLockMode(copyMode ? DrawerLayout.LOCK_MODE_LOCKED_CLOSED : DrawerLayout.LOCK_MODE_UNLOCKED);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
findViewById(R.id.new_session_button).setOnClickListener(new OnClickListener() {
|
||||
View newSessionButton = findViewById(R.id.new_session_button);
|
||||
|
||||
newSessionButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
addNewSession(false, null);
|
||||
}
|
||||
});
|
||||
|
||||
findViewById(R.id.new_session_button).setOnLongClickListener(new OnLongClickListener() {
|
||||
newSessionButton.setOnLongClickListener(new OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
Resources res = getResources();
|
||||
new AlertDialog.Builder(TermuxActivity.this).setTitle(R.string.new_session)
|
||||
.setItems(new String[] { res.getString(R.string.new_session_normal_unnamed), res.getString(R.string.new_session_normal_named),
|
||||
res.getString(R.string.new_session_failsafe) }, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
switch (which) {
|
||||
case 0:
|
||||
addNewSession(false, null);
|
||||
break;
|
||||
case 1:
|
||||
DialogUtils.textInput(TermuxActivity.this, R.string.session_new_named_title, R.string.session_new_named_positive_button, null,
|
||||
new DialogUtils.TextSetListener() {
|
||||
@Override
|
||||
public void onTextSet(String text) {
|
||||
addNewSession(false, text);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 2:
|
||||
addNewSession(true, null);
|
||||
break;
|
||||
DialogUtils.textInput(TermuxActivity.this, R.string.session_new_named_title, R.string.session_new_named_positive_button, null,
|
||||
new DialogUtils.TextSetListener() {
|
||||
@Override
|
||||
public void onTextSet(String text) {
|
||||
addNewSession(false, text);
|
||||
}
|
||||
}, R.string.new_session_failsafe, new DialogUtils.TextSetListener() {
|
||||
@Override
|
||||
public void onTextSet(String text) {
|
||||
addNewSession(true, text);
|
||||
}
|
||||
}
|
||||
}).show();
|
||||
);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
@@ -291,10 +324,9 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
startService(serviceIntent);
|
||||
if (!bindService(serviceIntent, this, 0)) throw new RuntimeException("bindService() failed");
|
||||
|
||||
mTerminalView.checkForTypeface();
|
||||
mTerminalView.checkForColors();
|
||||
mTerminalView.checkForFontAndColors();
|
||||
|
||||
TermuxInstaller.setupStorageSymlink(this);
|
||||
mBellSoundId = mBellSoundPool.load(this, R.raw.bell, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -344,14 +376,24 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
@Override
|
||||
public void onClipboardText(TerminalSession session, String text) {
|
||||
if (!mIsVisible) return;
|
||||
showToast("Clipboard set:\n\"" + text + "\"", true);
|
||||
showToast("Clipboard:\n\"" + text + "\"", false);
|
||||
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
clipboard.setPrimaryClip(new ClipData(null, new String[] { "text/plain" }, new ClipData.Item(text)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBell(TerminalSession session) {
|
||||
if (mIsVisible) ((Vibrator) getSystemService(VIBRATOR_SERVICE)).vibrate(50);
|
||||
if (mIsVisible) {
|
||||
switch (mSettings.mBellBehaviour) {
|
||||
case TermuxPreferences.BELL_BEEP:
|
||||
mBellSoundPool.play(mBellSoundId, 1.f, 1.f, 1, 0, 1.f);
|
||||
break;
|
||||
case TermuxPreferences.BELL_VIBRATE:
|
||||
((Vibrator) getSystemService(VIBRATOR_SERVICE)).vibrate(50);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -420,6 +462,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
TermuxInstaller.setupIfNeeded(TermuxActivity.this, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (mTermService == null) return; // Activity might have been destroyed.
|
||||
try {
|
||||
if (TermuxPreferences.isShowWelcomeDialog(TermuxActivity.this)) {
|
||||
new AlertDialog.Builder(TermuxActivity.this).setTitle(R.string.welcome_dialog_title).setMessage(R.string.welcome_dialog_body)
|
||||
@@ -455,7 +498,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
public void onTextSet(String text) {
|
||||
sessionToRename.mSessionName = text;
|
||||
}
|
||||
});
|
||||
}, -1, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -568,9 +611,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
TerminalSession currentSession = getCurrentTermSession();
|
||||
if (currentSession == null) return;
|
||||
|
||||
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
menu.add(Menu.NONE, CONTEXTMENU_PASTE_ID, Menu.NONE, R.string.paste_text).setEnabled(clipboard.hasPrimaryClip());
|
||||
menu.add(Menu.NONE, CONTEXTMENU_SELECT_ID, Menu.NONE, R.string.select);
|
||||
menu.add(Menu.NONE, CONTEXTMENU_SELECT_URL_ID, Menu.NONE, R.string.select_url);
|
||||
menu.add(Menu.NONE, CONTEXTMENU_SHARE_TRANSCRIPT_ID, Menu.NONE, R.string.select_all_and_share);
|
||||
menu.add(Menu.NONE, CONTEXTMENU_RESET_TERMINAL_ID, Menu.NONE, R.string.reset_terminal);
|
||||
menu.add(Menu.NONE, CONTEXTMENU_KILL_PROCESS_ID, Menu.NONE, R.string.kill_process).setEnabled(currentSession.isRunning());
|
||||
menu.add(Menu.NONE, CONTEXTMENU_TOGGLE_FULLSCREEN_ID, Menu.NONE, R.string.toggle_fullscreen).setCheckable(true).setChecked(mSettings.isFullScreen());
|
||||
@@ -646,84 +688,76 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
|
||||
@Override
|
||||
public boolean onContextItemSelected(MenuItem item) {
|
||||
TerminalSession session = getCurrentTermSession();
|
||||
|
||||
switch (item.getItemId()) {
|
||||
case CONTEXTMENU_SELECT_ID:
|
||||
CharSequence[] items = new CharSequence[] { getString(R.string.select_text), getString(R.string.select_url),
|
||||
getString(R.string.select_all_and_share) };
|
||||
new AlertDialog.Builder(this).setItems(items, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
switch (which) {
|
||||
case 0:
|
||||
mTerminalView.toggleSelectingText();
|
||||
break;
|
||||
case 1:
|
||||
showUrlSelection();
|
||||
break;
|
||||
case 2:
|
||||
TerminalSession session = getCurrentTermSession();
|
||||
if (session != null) {
|
||||
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||
intent.setType("text/plain");
|
||||
intent.putExtra(Intent.EXTRA_TEXT, session.getEmulator().getScreen().getTranscriptText().trim());
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.share_transcript_title));
|
||||
startActivity(Intent.createChooser(intent, getString(R.string.share_transcript_chooser_title)));
|
||||
}
|
||||
break;
|
||||
case CONTEXTMENU_SELECT_URL_ID:
|
||||
showUrlSelection();
|
||||
return true;
|
||||
case CONTEXTMENU_SHARE_TRANSCRIPT_ID:
|
||||
if (session != null) {
|
||||
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||
intent.setType("text/plain");
|
||||
intent.putExtra(Intent.EXTRA_TEXT, session.getEmulator().getScreen().getTranscriptText().trim());
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.share_transcript_title));
|
||||
startActivity(Intent.createChooser(intent, getString(R.string.share_transcript_chooser_title)));
|
||||
}
|
||||
return true;
|
||||
case CONTEXTMENU_PASTE_ID:
|
||||
doPaste();
|
||||
return true;
|
||||
case CONTEXTMENU_KILL_PROCESS_ID:
|
||||
final AlertDialog.Builder b = new AlertDialog.Builder(this);
|
||||
b.setIcon(android.R.drawable.ic_dialog_alert);
|
||||
b.setMessage(R.string.confirm_kill_process);
|
||||
b.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
dialog.dismiss();
|
||||
getCurrentTermSession().finishIfRunning();
|
||||
}
|
||||
dialog.dismiss();
|
||||
});
|
||||
b.setNegativeButton(android.R.string.no, null);
|
||||
b.show();
|
||||
return true;
|
||||
case CONTEXTMENU_RESET_TERMINAL_ID: {
|
||||
if (session != null) {
|
||||
session.reset();
|
||||
showToast(getResources().getString(R.string.reset_toast_notification), true);
|
||||
}
|
||||
}).show();
|
||||
return true;
|
||||
case CONTEXTMENU_PASTE_ID:
|
||||
doPaste();
|
||||
return true;
|
||||
case CONTEXTMENU_KILL_PROCESS_ID:
|
||||
final AlertDialog.Builder b = new AlertDialog.Builder(this);
|
||||
b.setIcon(android.R.drawable.ic_dialog_alert);
|
||||
b.setMessage(R.string.confirm_kill_process);
|
||||
b.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
dialog.dismiss();
|
||||
getCurrentTermSession().finishIfRunning();
|
||||
return true;
|
||||
}
|
||||
case CONTEXTMENU_STYLING_ID: {
|
||||
Intent stylingIntent = new Intent();
|
||||
stylingIntent.setClassName("com.termux.styling", "com.termux.styling.TermuxStyleActivity");
|
||||
try {
|
||||
startActivity(stylingIntent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
new AlertDialog.Builder(this).setMessage(R.string.styling_not_installed)
|
||||
.setPositiveButton(R.string.styling_install, new android.content.DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://play.google.com/store/apps/details?id=com.termux.styling")));
|
||||
}
|
||||
}).setNegativeButton(android.R.string.cancel, null).show();
|
||||
}
|
||||
});
|
||||
b.setNegativeButton(android.R.string.no, null);
|
||||
b.show();
|
||||
return true;
|
||||
case CONTEXTMENU_RESET_TERMINAL_ID: {
|
||||
TerminalSession session = getCurrentTermSession();
|
||||
if (session != null) {
|
||||
session.reset();
|
||||
showToast(getResources().getString(R.string.reset_toast_notification), true);
|
||||
}
|
||||
return true;
|
||||
case CONTEXTMENU_TOGGLE_FULLSCREEN_ID:
|
||||
toggleImmersive();
|
||||
return true;
|
||||
case CONTEXTMENU_HELP_ID:
|
||||
startActivity(new Intent(this, TermuxHelpActivity.class));
|
||||
return true;
|
||||
default:
|
||||
return super.onContextItemSelected(item);
|
||||
}
|
||||
case CONTEXTMENU_STYLING_ID: {
|
||||
Intent stylingIntent = new Intent();
|
||||
stylingIntent.setClassName("com.termux.styling", "com.termux.styling.TermuxStyleActivity");
|
||||
try {
|
||||
startActivity(stylingIntent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
new AlertDialog.Builder(this).setMessage(R.string.styling_not_installed)
|
||||
.setPositiveButton(R.string.styling_install, new android.content.DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://play.google.com/store/apps/details?id=com.termux.styling")));
|
||||
}
|
||||
}).setNegativeButton(android.R.string.cancel, null).show();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
case CONTEXTMENU_TOGGLE_FULLSCREEN_ID:
|
||||
toggleImmersive();
|
||||
return true;
|
||||
case CONTEXTMENU_HELP_ID:
|
||||
startActivity(new Intent(this, TermuxHelpActivity.class));
|
||||
return true;
|
||||
default:
|
||||
return super.onContextItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
|
||||
if (requestCode == REQUESTCODE_PERMISSION_STORAGE && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
TermuxInstaller.setupStorageSymlinks(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,14 @@ import android.content.ActivityNotFoundException;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.ViewGroup;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.RelativeLayout;
|
||||
|
||||
/** Basic embedded browser for viewing the bundled help page. */
|
||||
/** Basic embedded browser for viewing help pages. */
|
||||
public final class TermuxHelpActivity extends Activity {
|
||||
|
||||
private WebView mWebView;
|
||||
@@ -16,22 +20,47 @@ public final class TermuxHelpActivity extends Activity {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
final RelativeLayout progressLayout = new RelativeLayout(this);
|
||||
RelativeLayout.LayoutParams lParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
lParams.addRule(RelativeLayout.CENTER_IN_PARENT);
|
||||
ProgressBar progressBar = new ProgressBar(this);
|
||||
progressBar.setIndeterminate(true);
|
||||
progressBar.setLayoutParams(lParams);
|
||||
progressLayout.addView(progressBar);
|
||||
|
||||
mWebView = new WebView(this);
|
||||
setContentView(mWebView);
|
||||
WebSettings settings = mWebView.getSettings();
|
||||
settings.setCacheMode(WebSettings.LOAD_NO_CACHE);
|
||||
settings.setAppCacheEnabled(false);
|
||||
setContentView(progressLayout);
|
||||
mWebView.clearCache(true);
|
||||
|
||||
mWebView.setWebViewClient(new WebViewClient() {
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
||||
if (url.startsWith("https://termux.com")) {
|
||||
// Inline help.
|
||||
setContentView(progressLayout);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
|
||||
} catch (ActivityNotFoundException e) {
|
||||
// TODO: Android TV does not have a system browser - but needs better method of getting back
|
||||
// than navigating deep here.
|
||||
// Android TV does not have a system browser.
|
||||
setContentView(progressLayout);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageFinished(WebView view, String url) {
|
||||
setContentView(mWebView);
|
||||
}
|
||||
});
|
||||
mWebView.loadUrl("file:///android_asset/help.html");
|
||||
mWebView.loadUrl("https://termux.com/help.html");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.DialogInterface.OnClickListener;
|
||||
import android.content.DialogInterface.OnDismissListener;
|
||||
import android.os.Environment;
|
||||
import android.system.Os;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
@@ -184,7 +185,7 @@ final class TermuxInstaller {
|
||||
} else if (arch.equals("x86_64")) {
|
||||
arch = "i686";
|
||||
}
|
||||
return new URL("http://apt.termux.com/bootstrap/bootstrap-" + arch + ".zip");
|
||||
return new URL("https://termux.net/bootstrap/bootstrap-" + arch + ".zip");
|
||||
}
|
||||
|
||||
/** Delete a folder and all its content or throw. */
|
||||
@@ -200,33 +201,48 @@ final class TermuxInstaller {
|
||||
}
|
||||
}
|
||||
|
||||
public static void setupStorageSymlink(final Context context) {
|
||||
final File[] dirs = context.getExternalFilesDirs(null);
|
||||
if (dirs == null || dirs.length < 2) return;
|
||||
public static void setupStorageSymlinks(final Context context) {
|
||||
final String LOG_TAG = "termux-storage";
|
||||
new Thread() {
|
||||
public void run() {
|
||||
try {
|
||||
final File externalDir = dirs[1];
|
||||
File homeDir = new File(TermuxService.HOME_PATH);
|
||||
homeDir.mkdirs();
|
||||
File externalLink = new File(homeDir, "storage");
|
||||
File storageDir = new File(TermuxService.HOME_PATH, "storage");
|
||||
|
||||
if (externalLink.exists()) {
|
||||
if (externalLink.getCanonicalPath().equals(externalDir.getPath())) {
|
||||
// Keeping existing link.
|
||||
return;
|
||||
} else {
|
||||
// Removing old link to give place to new.
|
||||
if (!externalLink.delete()) {
|
||||
Log.e("termux", "Unable to remove old $HOME/storage to give place for new");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (storageDir.exists() && !storageDir.delete()) {
|
||||
Log.e(LOG_TAG, "Could not delete old $HOME/storage");
|
||||
return;
|
||||
}
|
||||
|
||||
Os.symlink(externalDir.getAbsolutePath(), externalLink.getAbsolutePath());
|
||||
if (!storageDir.mkdirs()) {
|
||||
Log.e(LOG_TAG, "Unable to mkdirs() for $HOME/storage");
|
||||
return;
|
||||
}
|
||||
|
||||
File sharedDir = Environment.getExternalStorageDirectory();
|
||||
Os.symlink(sharedDir.getAbsolutePath(), new File(storageDir, "shared").getAbsolutePath());
|
||||
|
||||
File downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
|
||||
Os.symlink(downloadsDir.getAbsolutePath(), new File(storageDir, "downloads").getAbsolutePath());
|
||||
|
||||
File dcimDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM);
|
||||
Os.symlink(dcimDir.getAbsolutePath(), new File(storageDir, "dcim").getAbsolutePath());
|
||||
|
||||
File picturesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
|
||||
Os.symlink(picturesDir.getAbsolutePath(), new File(storageDir, "pictures").getAbsolutePath());
|
||||
|
||||
File musicDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC);
|
||||
Os.symlink(musicDir.getAbsolutePath(), new File(storageDir, "music").getAbsolutePath());
|
||||
|
||||
File moviesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES);
|
||||
Os.symlink(moviesDir.getAbsolutePath(), new File(storageDir, "movies").getAbsolutePath());
|
||||
|
||||
final File[] dirs = context.getExternalFilesDirs(null);
|
||||
if (dirs != null && dirs.length >= 2) {
|
||||
final File externalDir = dirs[1];
|
||||
Os.symlink(externalDir.getAbsolutePath(), new File(storageDir, "external").getAbsolutePath());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e("termux", "Error setting up link", e);
|
||||
Log.e(LOG_TAG, "Error setting up link", e);
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
|
||||
@@ -1,16 +1,34 @@
|
||||
package com.termux.app;
|
||||
|
||||
import com.termux.terminal.TerminalSession;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.IntDef;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.termux.terminal.TerminalSession;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.Properties;
|
||||
|
||||
final class TermuxPreferences {
|
||||
|
||||
@IntDef({BELL_VIBRATE, BELL_BEEP, BELL_IGNORE})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
public @interface AsciiBellBehaviour {}
|
||||
|
||||
static final int BELL_VIBRATE = 1;
|
||||
static final int BELL_BEEP = 2;
|
||||
static final int BELL_IGNORE = 3;
|
||||
|
||||
private final int MIN_FONTSIZE;
|
||||
private static final int MAX_FONTSIZE = 256;
|
||||
|
||||
private static final String FULLSCREEN_KEY = "fullscreen";
|
||||
private static final String FONTSIZE_KEY = "fontsize";
|
||||
private static final String CURRENT_SESSION_KEY = "current_session";
|
||||
@@ -19,7 +37,13 @@ final class TermuxPreferences {
|
||||
private boolean mFullScreen;
|
||||
private int mFontSize;
|
||||
|
||||
@AsciiBellBehaviour
|
||||
int mBellBehaviour = BELL_VIBRATE;
|
||||
|
||||
boolean mBackIsEscape = true;
|
||||
|
||||
TermuxPreferences(Context context) {
|
||||
reloadFromProperties(context);
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
|
||||
float dipInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, context.getResources().getDisplayMetrics());
|
||||
@@ -86,4 +110,33 @@ final class TermuxPreferences {
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(SHOW_WELCOME_DIALOG_KEY, false).apply();
|
||||
}
|
||||
|
||||
public void reloadFromProperties(Context context) {
|
||||
try {
|
||||
File propsFile = new File(TermuxService.HOME_PATH + "/.config/termux/termux.properties");
|
||||
Properties props = new Properties();
|
||||
if (propsFile.isFile() && propsFile.canRead()) {
|
||||
try (FileInputStream in = new FileInputStream(propsFile)) {
|
||||
props.load(in);
|
||||
}
|
||||
}
|
||||
|
||||
switch (props.getProperty("bell-character", "vibrate")) {
|
||||
case "beep":
|
||||
mBellBehaviour = BELL_BEEP;
|
||||
break;
|
||||
case "ignore":
|
||||
mBellBehaviour = BELL_IGNORE;
|
||||
break;
|
||||
default: // "vibrate".
|
||||
mBellBehaviour = BELL_VIBRATE;
|
||||
break;
|
||||
}
|
||||
|
||||
mBackIsEscape = "escape".equals(props.getProperty("back-key", "escape"));
|
||||
} catch (Exception e) {
|
||||
Toast.makeText(context, "Error loading properties: " + e.getMessage(), Toast.LENGTH_LONG).show();
|
||||
Log.e("termux", "Error loading props", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,8 +16,6 @@ package com.termux.drawer;
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
@@ -27,8 +25,6 @@ import android.graphics.PixelFormat;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.os.SystemClock;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.Gravity;
|
||||
@@ -39,6 +35,8 @@ import android.view.ViewGroup;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* DrawerLayout acts as a top-level container for window content that allows for interactive "drawer" views to be pulled
|
||||
* out from the edge of the window.
|
||||
@@ -138,11 +136,11 @@ public class DrawerLayout extends ViewGroup {
|
||||
|
||||
private final ChildAccessibilityDelegate mChildAccessibilityDelegate = new ChildAccessibilityDelegate();
|
||||
|
||||
private int mMinDrawerMargin;
|
||||
private final int mMinDrawerMargin;
|
||||
|
||||
private int mScrimColor = DEFAULT_SCRIM_COLOR;
|
||||
private float mScrimOpacity;
|
||||
private Paint mScrimPaint = new Paint();
|
||||
private final Paint mScrimPaint = new Paint();
|
||||
|
||||
private final ViewDragHelper mLeftDragger;
|
||||
private final ViewDragHelper mRightDragger;
|
||||
@@ -1422,38 +1420,6 @@ public class DrawerLayout extends ViewGroup {
|
||||
return super.onKeyUp(keyCode, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(Parcelable state) {
|
||||
final SavedState ss = (SavedState) state;
|
||||
super.onRestoreInstanceState(ss.getSuperState());
|
||||
|
||||
if (ss.openDrawerGravity != Gravity.NO_GRAVITY) {
|
||||
final View toOpen = findDrawerWithGravity(ss.openDrawerGravity);
|
||||
if (toOpen != null) {
|
||||
openDrawer(toOpen);
|
||||
}
|
||||
}
|
||||
|
||||
setDrawerLockMode(ss.lockModeLeft, Gravity.LEFT);
|
||||
setDrawerLockMode(ss.lockModeRight, Gravity.RIGHT);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Parcelable onSaveInstanceState() {
|
||||
final Parcelable superState = super.onSaveInstanceState();
|
||||
final SavedState ss = new SavedState(superState);
|
||||
|
||||
final View openDrawer = findOpenDrawer();
|
||||
if (openDrawer != null) {
|
||||
ss.openDrawerGravity = ((LayoutParams) openDrawer.getLayoutParams()).gravity;
|
||||
}
|
||||
|
||||
ss.lockModeLeft = mLockModeLeft;
|
||||
ss.lockModeRight = mLockModeRight;
|
||||
|
||||
return ss;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addView(View child, int index, ViewGroup.LayoutParams params) {
|
||||
super.addView(child, index, params);
|
||||
@@ -1486,30 +1452,6 @@ public class DrawerLayout extends ViewGroup {
|
||||
&& child.getImportantForAccessibility() != View.IMPORTANT_FOR_ACCESSIBILITY_NO;
|
||||
}
|
||||
|
||||
/**
|
||||
* State persisted across instances
|
||||
*/
|
||||
protected static class SavedState extends BaseSavedState {
|
||||
int openDrawerGravity = Gravity.NO_GRAVITY;
|
||||
int lockModeLeft = LOCK_MODE_UNLOCKED;
|
||||
int lockModeRight = LOCK_MODE_UNLOCKED;
|
||||
|
||||
public SavedState(Parcel in) {
|
||||
super(in);
|
||||
openDrawerGravity = in.readInt();
|
||||
}
|
||||
|
||||
public SavedState(Parcelable superState) {
|
||||
super(superState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
super.writeToParcel(dest, flags);
|
||||
dest.writeInt(openDrawerGravity);
|
||||
}
|
||||
}
|
||||
|
||||
private class ViewDragCallback extends ViewDragHelper.Callback {
|
||||
private final int mAbsGravity;
|
||||
private ViewDragHelper mDragger;
|
||||
|
||||
@@ -57,7 +57,7 @@ public class ViewDragHelper {
|
||||
/**
|
||||
* Edge flag indicating that the left edge should be affected.
|
||||
*/
|
||||
public static final int EDGE_LEFT = 1 << 0;
|
||||
public static final int EDGE_LEFT = 1 /*1 << 0*/;
|
||||
|
||||
/**
|
||||
* Edge flag indicating that the right edge should be affected.
|
||||
@@ -82,7 +82,7 @@ public class ViewDragHelper {
|
||||
/**
|
||||
* Indicates that a check should occur along the horizontal axis
|
||||
*/
|
||||
public static final int DIRECTION_HORIZONTAL = 1 << 0;
|
||||
public static final int DIRECTION_HORIZONTAL = 1 /*1 << 0*/;
|
||||
|
||||
/**
|
||||
* Indicates that a check should occur along the vertical axis
|
||||
|
||||
@@ -61,6 +61,10 @@ public final class TerminalBuffer {
|
||||
TerminalRow lineObject = mLines[externalToInternalRow(row)];
|
||||
int x1Index = lineObject.findStartOfColumn(x1);
|
||||
int x2Index = (x2 < mColumns) ? lineObject.findStartOfColumn(x2) : lineObject.getSpaceUsed();
|
||||
if (x2Index == x1Index) {
|
||||
// Selected the start of a wide character.
|
||||
x2Index = lineObject.findStartOfColumn(x2+1);
|
||||
}
|
||||
char[] line = lineObject.mText;
|
||||
int lastPrintingCharIndex = -1;
|
||||
int i;
|
||||
@@ -71,7 +75,7 @@ public final class TerminalBuffer {
|
||||
} else {
|
||||
for (i = x1Index; i < x2Index; ++i) {
|
||||
char c = line[i];
|
||||
if (c != ' ' && !Character.isLowSurrogate(c)) lastPrintingCharIndex = i;
|
||||
if (c != ' ') lastPrintingCharIndex = i;
|
||||
}
|
||||
}
|
||||
if (lastPrintingCharIndex != -1) builder.append(line, x1Index, lastPrintingCharIndex - x1Index + 1);
|
||||
|
||||
@@ -33,8 +33,7 @@ public final class TerminalEmulator {
|
||||
private static final boolean LOG_ESCAPE_SEQUENCES = false;
|
||||
|
||||
public static final int MOUSE_LEFT_BUTTON = 0;
|
||||
public static final int MOUSE_MIDDLE_BUTTON = 1;
|
||||
public static final int MOUSE_RIGHT_BUTTON = 2;
|
||||
|
||||
/** Mouse moving while having left mouse button pressed. */
|
||||
public static final int MOUSE_LEFT_BUTTON_MOVED = 32;
|
||||
public static final int MOUSE_WHEELUP_BUTTON = 64;
|
||||
|
||||
@@ -184,6 +184,7 @@ public final class TerminalRow {
|
||||
mSpaceUsed += javaCharDifference;
|
||||
|
||||
// Store char. A combining character is stored at the end of the existing contents so that it modifies them:
|
||||
//noinspection ResultOfMethodCallIgnored - since we already now how many java chars is used.
|
||||
Character.toChars(codePoint, text, oldStartOfColumnIndex + (newIsCombining ? oldCharactersUsedForColumn : 0));
|
||||
|
||||
if (oldCodePointDisplayWidth == 2 && newCodePointDisplayWidth == 1) {
|
||||
|
||||
@@ -6,15 +6,19 @@ import android.view.ScaleGestureDetector;
|
||||
/**
|
||||
* Input and scale listener which may be set on a {@link TerminalView} through
|
||||
* {@link TerminalView#setOnKeyListener(TerminalKeyListener)}.
|
||||
*
|
||||
* TODO: Rename to TerminalViewClient.
|
||||
*/
|
||||
public interface TerminalKeyListener {
|
||||
|
||||
/** Callback function on scale events according to {@link ScaleGestureDetector#getScaleFactor()}. */
|
||||
float onScale(float scale);
|
||||
|
||||
void onLongPress(MotionEvent e);
|
||||
|
||||
/** On a single tap on the terminal if terminal mouse reporting not enabled. */
|
||||
void onSingleTapUp(MotionEvent e);
|
||||
|
||||
boolean shouldBackButtonBeMappedToEscape();
|
||||
|
||||
void copyModeChanged(boolean copyMode);
|
||||
|
||||
}
|
||||
|
||||
@@ -1,29 +1,27 @@
|
||||
package com.termux.view;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.util.Properties;
|
||||
|
||||
import com.termux.terminal.EmulatorDebug;
|
||||
import com.termux.terminal.KeyHandler;
|
||||
import com.termux.terminal.TerminalColors;
|
||||
import com.termux.terminal.TerminalEmulator;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.os.Build;
|
||||
import android.text.InputType;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.ActionMode;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyCharacterMap;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.BaseInputConnection;
|
||||
@@ -31,11 +29,24 @@ import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
import android.widget.Scroller;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.terminal.EmulatorDebug;
|
||||
import com.termux.terminal.KeyHandler;
|
||||
import com.termux.terminal.TerminalBuffer;
|
||||
import com.termux.terminal.TerminalColors;
|
||||
import com.termux.terminal.TerminalEmulator;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.util.Properties;
|
||||
|
||||
/** View displaying and interacting with a {@link TerminalSession}. */
|
||||
public final class TerminalView extends View {
|
||||
|
||||
/** Log view key and IME events. */
|
||||
private static final boolean LOG_KEY_EVENTS = false;
|
||||
private static final boolean LOG_KEY_EVENTS = true;
|
||||
|
||||
/** The currently displayed terminal session, whose emulator is {@link #mEmulator}. */
|
||||
TerminalSession mTermSession;
|
||||
@@ -52,9 +63,11 @@ public final class TerminalView extends View {
|
||||
/** Keeping track of the special keys acting as Ctrl and Fn for the soft keyboard and other hardware keys. */
|
||||
boolean mVirtualControlKeyDown, mVirtualFnKeyDown;
|
||||
|
||||
boolean mIsSelectingText = false;
|
||||
int mSelXAnchor = -1, mSelYAnchor = -1;
|
||||
boolean mIsSelectingText = false, mIsDraggingLeftSelection, mInitialTextSelection;
|
||||
int mSelX1 = -1, mSelX2 = -1, mSelY1 = -1, mSelY2 = -1;
|
||||
float mSelectionDownX, mSelectionDownY;
|
||||
private ActionMode mActionMode;
|
||||
private BitmapDrawable mLeftSelectionHandle, mRightSelectionHandle;
|
||||
|
||||
float mScaleFactor = 1.f;
|
||||
final GestureAndScaleRecognizer mGestureRecognizer;
|
||||
@@ -79,7 +92,7 @@ public final class TerminalView extends View {
|
||||
@Override
|
||||
public boolean onUp(MotionEvent e) {
|
||||
mScrollRemainder = 0.0f;
|
||||
if (mEmulator != null && mEmulator.isMouseTrackingActive()) {
|
||||
if (mEmulator != null && mEmulator.isMouseTrackingActive() && !mIsSelectingText) {
|
||||
// Quick event processing when mouse tracking is active - do not wait for check of double tapping
|
||||
// for zooming.
|
||||
sendMouseEventCode(e, TerminalEmulator.MOUSE_LEFT_BUTTON, true);
|
||||
@@ -92,6 +105,7 @@ public final class TerminalView extends View {
|
||||
@Override
|
||||
public boolean onSingleTapUp(MotionEvent e) {
|
||||
if (mEmulator == null) return true;
|
||||
if (mIsSelectingText) { toggleSelectingText(null); return true; }
|
||||
requestFocus();
|
||||
if (!mEmulator.isMouseTrackingActive()) {
|
||||
if (!e.isFromSource(InputDevice.SOURCE_MOUSE)) {
|
||||
@@ -104,7 +118,7 @@ public final class TerminalView extends View {
|
||||
|
||||
@Override
|
||||
public boolean onScroll(MotionEvent e2, float distanceX, float distanceY) {
|
||||
if (mEmulator == null) return true;
|
||||
if (mEmulator == null || mIsSelectingText) return true;
|
||||
if (mEmulator.isMouseTrackingActive() && e2.isFromSource(InputDevice.SOURCE_MOUSE)) {
|
||||
// If moving with mouse pointer while pressing button, report that instead of scroll.
|
||||
// This means that we never report moving with button press-events for touch input,
|
||||
@@ -122,6 +136,7 @@ public final class TerminalView extends View {
|
||||
|
||||
@Override
|
||||
public boolean onScale(float focusX, float focusY, float scale) {
|
||||
if (mEmulator == null || mIsSelectingText) return true;
|
||||
mScaleFactor *= scale;
|
||||
mScaleFactor = mOnKeyListener.onScale(mScaleFactor);
|
||||
return true;
|
||||
@@ -129,7 +144,7 @@ public final class TerminalView extends View {
|
||||
|
||||
@Override
|
||||
public boolean onFling(final MotionEvent e2, float velocityX, float velocityY) {
|
||||
if (mEmulator == null) return true;
|
||||
if (mEmulator == null || mIsSelectingText) return true;
|
||||
// Do not start scrolling until last fling has been taken care of:
|
||||
if (!mScroller.isFinished()) return true;
|
||||
|
||||
@@ -176,9 +191,9 @@ public final class TerminalView extends View {
|
||||
|
||||
@Override
|
||||
public void onLongPress(MotionEvent e) {
|
||||
if (mEmulator != null && !mGestureRecognizer.isInProgress()) {
|
||||
if (!mGestureRecognizer.isInProgress() && !mIsSelectingText) {
|
||||
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
|
||||
mOnKeyListener.onLongPress(e);
|
||||
toggleSelectingText(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -229,7 +244,7 @@ public final class TerminalView extends View {
|
||||
//
|
||||
// So a bit messy. If this gets too messy it's perhaps best resolved by reverting back to just
|
||||
// "TYPE_NULL" and let the Pinyin Input english keyboard be in word mode.
|
||||
outAttrs.inputType = InputType.TYPE_NULL | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD;
|
||||
outAttrs.inputType = InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD;
|
||||
|
||||
// Let part of the application show behind when in landscape:
|
||||
outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_FULLSCREEN;
|
||||
@@ -336,20 +351,36 @@ public final class TerminalView extends View {
|
||||
public void onScreenUpdated() {
|
||||
if (mEmulator == null) return;
|
||||
|
||||
boolean skipScrolling = false;
|
||||
if (mIsSelectingText) {
|
||||
// Do not scroll when selecting text.
|
||||
int rowsInHistory = mEmulator.getScreen().getActiveTranscriptRows();
|
||||
int rowShift = mEmulator.getScrollCounter();
|
||||
mSelY1 -= rowShift;
|
||||
mSelY2 -= rowShift;
|
||||
mSelYAnchor -= rowShift;
|
||||
if (-mTopRow + rowShift > rowsInHistory) {
|
||||
// .. unless we're hitting the end of history transcript, in which
|
||||
// case we abort text selection and scroll to end.
|
||||
toggleSelectingText(null);
|
||||
} else {
|
||||
skipScrolling = true;
|
||||
mTopRow -= rowShift;
|
||||
mSelY1 -= rowShift;
|
||||
mSelY2 -= rowShift;
|
||||
}
|
||||
}
|
||||
mEmulator.clearScrollCounter();
|
||||
|
||||
if (mTopRow != 0) {
|
||||
if (!skipScrolling && mTopRow != 0) {
|
||||
// Scroll down if not already there.
|
||||
if (mTopRow < -3) {
|
||||
// Awaken scroll bars only if scrolling a noticeable amount
|
||||
// - we do not want visible scroll bars during normal typing
|
||||
// of one row at a time.
|
||||
awakenScrollBars();
|
||||
}
|
||||
mTopRow = 0;
|
||||
scrollTo(0, 0);
|
||||
}
|
||||
|
||||
mEmulator.clearScrollCounter();
|
||||
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@@ -423,83 +454,105 @@ public final class TerminalView extends View {
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
@TargetApi(23)
|
||||
public boolean onTouchEvent(MotionEvent ev) {
|
||||
if (mEmulator == null) return true;
|
||||
final boolean eventFromMouse = ev.isFromSource(InputDevice.SOURCE_MOUSE);
|
||||
final int action = ev.getAction();
|
||||
|
||||
if (eventFromMouse) {
|
||||
if ((ev.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) {
|
||||
if (action == MotionEvent.ACTION_DOWN) showContextMenu();
|
||||
return true;
|
||||
} else if (mEmulator.isMouseTrackingActive() && (ev.getAction() == MotionEvent.ACTION_DOWN || ev.getAction() == MotionEvent.ACTION_UP)) {
|
||||
sendMouseEventCode(ev, TerminalEmulator.MOUSE_LEFT_BUTTON, ev.getAction() == MotionEvent.ACTION_DOWN);
|
||||
return true;
|
||||
} else if (!mEmulator.isMouseTrackingActive() && action == MotionEvent.ACTION_DOWN) {
|
||||
// Start text selection with mouse. Note that the check against MotionEvent.ACTION_DOWN is
|
||||
// important, since we otherwise would pick up secondary mouse button up actions.
|
||||
mIsSelectingText = true;
|
||||
}
|
||||
} else if (!mIsSelectingText) {
|
||||
mGestureRecognizer.onTouchEvent(ev);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mIsSelectingText) {
|
||||
int cy = (int) (ev.getY() / mRenderer.mFontLineSpacing) + mTopRow;
|
||||
int cx = (int) (ev.getX() / mRenderer.mFontWidth);
|
||||
// Offset for finger:
|
||||
final int SELECT_TEXT_OFFSET_Y = eventFromMouse ? 0 : -40;
|
||||
int cy = (int) ((ev.getY() + SELECT_TEXT_OFFSET_Y) / mRenderer.mFontLineSpacing) + mTopRow;
|
||||
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_UP:
|
||||
mInitialTextSelection = false;
|
||||
break;
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
mSelXAnchor = cx;
|
||||
mSelYAnchor = cy;
|
||||
mSelX1 = cx;
|
||||
mSelY1 = cy;
|
||||
mSelX2 = mSelX1;
|
||||
mSelY2 = mSelY1;
|
||||
invalidate();
|
||||
int distanceFromSel1 = Math.abs(cx-mSelX1) + Math.abs(cy-mSelY1);
|
||||
int distanceFromSel2 = Math.abs(cx-mSelX2) + Math.abs(cy-mSelY2);
|
||||
mIsDraggingLeftSelection = distanceFromSel1 <= distanceFromSel2;
|
||||
mSelectionDownX = ev.getX();
|
||||
mSelectionDownY = ev.getY();
|
||||
break;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
case MotionEvent.ACTION_UP:
|
||||
boolean touchBeforeAnchor = (cy < mSelYAnchor || (cy == mSelYAnchor && cx < mSelXAnchor));
|
||||
int minx = touchBeforeAnchor ? cx : mSelXAnchor;
|
||||
int maxx = !touchBeforeAnchor ? cx : mSelXAnchor;
|
||||
int miny = touchBeforeAnchor ? cy : mSelYAnchor;
|
||||
int maxy = !touchBeforeAnchor ? cy : mSelYAnchor;
|
||||
mSelX1 = minx;
|
||||
mSelY1 = miny;
|
||||
mSelX2 = maxx;
|
||||
mSelY2 = maxy;
|
||||
if (action == MotionEvent.ACTION_UP) {
|
||||
String selectedText = mEmulator.getSelectedText(mSelX1, mSelY1, mSelX2, mSelY2).trim();
|
||||
mTermSession.clipboardText(selectedText);
|
||||
toggleSelectingText();
|
||||
if (mInitialTextSelection) break;
|
||||
float deltaX = ev.getX() - mSelectionDownX;
|
||||
float deltaY = ev.getY() - mSelectionDownY;
|
||||
int deltaCols = (int) Math.ceil(deltaX / mRenderer.mFontWidth);
|
||||
int deltaRows = (int) Math.ceil(deltaY / mRenderer.mFontLineSpacing);
|
||||
mSelectionDownX += deltaCols * mRenderer.mFontWidth;
|
||||
mSelectionDownY += deltaRows * mRenderer.mFontLineSpacing;
|
||||
if (mIsDraggingLeftSelection) {
|
||||
mSelX1 += deltaCols;
|
||||
mSelY1 += deltaRows;
|
||||
} else {
|
||||
mSelX2 += deltaCols;
|
||||
mSelY2 += deltaRows;
|
||||
}
|
||||
|
||||
mSelX1 = Math.min(mEmulator.mColumns, Math.max(0, mSelX1));
|
||||
mSelX2 = Math.min(mEmulator.mColumns, Math.max(0, mSelX2));
|
||||
|
||||
if (mSelY1 == mSelY2 && mSelX1 > mSelX2 || mSelY1 > mSelY2) {
|
||||
// Switch handles.
|
||||
mIsDraggingLeftSelection = !mIsDraggingLeftSelection;
|
||||
int tmpX1 = mSelX1, tmpY1 = mSelY1;
|
||||
mSelX1 = mSelX2; mSelY1 = mSelY2;
|
||||
mSelX2 = tmpX1; mSelY2 = tmpY1;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) mActionMode.invalidateContentRect();
|
||||
invalidate();
|
||||
break;
|
||||
default:
|
||||
toggleSelectingText();
|
||||
invalidate();
|
||||
break;
|
||||
}
|
||||
mGestureRecognizer.onTouchEvent(ev);
|
||||
return true;
|
||||
} else if (ev.isFromSource(InputDevice.SOURCE_MOUSE)) {
|
||||
if (ev.isButtonPressed(MotionEvent.BUTTON_SECONDARY)) {
|
||||
if (action == MotionEvent.ACTION_DOWN) showContextMenu();
|
||||
return true;
|
||||
} else if (ev.isButtonPressed(MotionEvent.BUTTON_TERTIARY)) {
|
||||
ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clipData = clipboard.getPrimaryClip();
|
||||
if (clipData != null) {
|
||||
CharSequence paste = clipData.getItemAt(0).coerceToText(getContext());
|
||||
if (!TextUtils.isEmpty(paste)) mEmulator.paste(paste.toString());
|
||||
}
|
||||
} else if (mEmulator.isMouseTrackingActive()) { // BUTTON_PRIMARY.
|
||||
switch (ev.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
case MotionEvent.ACTION_UP:
|
||||
sendMouseEventCode(ev, TerminalEmulator.MOUSE_LEFT_BUTTON, ev.getAction() == MotionEvent.ACTION_DOWN);
|
||||
break;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
sendMouseEventCode(ev, TerminalEmulator.MOUSE_LEFT_BUTTON_MOVED, true);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
mGestureRecognizer.onTouchEvent(ev);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
|
||||
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "onKeyPreIme(keyCode=" + keyCode + ", event=" + event + ")");
|
||||
if (keyCode == KeyEvent.KEYCODE_ESCAPE || keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
// Handle the escape key ourselves to avoid the system from treating it as back key
|
||||
// and e.g. close keyboard.
|
||||
switch (event.getAction()) {
|
||||
case KeyEvent.ACTION_DOWN:
|
||||
return onKeyDown(keyCode, event);
|
||||
case KeyEvent.ACTION_UP:
|
||||
return onKeyUp(keyCode, event);
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
if (mIsSelectingText) {
|
||||
toggleSelectingText(null);
|
||||
return true;
|
||||
} else if (mOnKeyListener.shouldBackButtonBeMappedToEscape()) {
|
||||
// Intercept back button to treat it as escape:
|
||||
switch (event.getAction()) {
|
||||
case KeyEvent.ACTION_DOWN:
|
||||
return onKeyDown(keyCode, event);
|
||||
case KeyEvent.ACTION_UP:
|
||||
return onKeyUp(keyCode, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.onKeyPreIme(keyCode, event);
|
||||
@@ -518,7 +571,7 @@ public final class TerminalView extends View {
|
||||
if (handleVirtualKeys(keyCode, event, true)) {
|
||||
invalidate();
|
||||
return true;
|
||||
} else if (event.isSystem() && keyCode != KeyEvent.KEYCODE_BACK) {
|
||||
} else if (event.isSystem() && (!mOnKeyListener.shouldBackButtonBeMappedToEscape() || keyCode != KeyEvent.KEYCODE_BACK)) {
|
||||
return super.onKeyDown(keyCode, event);
|
||||
}
|
||||
|
||||
@@ -646,13 +699,20 @@ public final class TerminalView extends View {
|
||||
if (resultingKeyCode > -1) {
|
||||
handleKeyCode(resultingKeyCode, 0);
|
||||
} else {
|
||||
// The below two workarounds are needed on at least Logitech Keyboard k810 on Samsung Galaxy Tab Pro
|
||||
// (Android 4.4) with the stock Samsung Keyboard. They should be harmless when not used since the need
|
||||
// to input the original characters instead of the new ones using the keyboard should be low.
|
||||
// Rewrite U+02DC 'SMALL TILDE' to U+007E 'TILDE' for ~ to work in shells:
|
||||
if (codePoint == 0x02DC) codePoint = 0x07E;
|
||||
// Rewrite U+02CB 'MODIFIER LETTER GRAVE ACCENT' to U+0060 'GRAVE ACCENT' for ` (backticks) to work:
|
||||
if (codePoint == 0x02CB) codePoint = 0x60;
|
||||
// Work around bluetooth keyboards sending funny unicode characters instead
|
||||
// of the more normal ones from ASCII that terminal programs expect - the
|
||||
// desire to input the original characters should be low.
|
||||
switch (codePoint) {
|
||||
case 0x02DC: // SMALL TILDE.
|
||||
codePoint = 0x007E; // TILDE (~).
|
||||
break;
|
||||
case 0x02CB: // MODIFIER LETTER GRAVE ACCENT.
|
||||
codePoint = 0x0060; // GRAVE ACCENT (`).
|
||||
break;
|
||||
case 0x02C6: // MODIFIER LETTER CIRCUMFLEX ACCENT.
|
||||
codePoint = 0x005E; // CIRCUMFLEX ACCENT (^).
|
||||
break;
|
||||
}
|
||||
|
||||
// If left alt, send escape before the code point to make e.g. Alt+B and Alt+F work in readline:
|
||||
mTermSession.writeCodePoint(leftAltDownFromEvent, codePoint);
|
||||
@@ -712,67 +772,28 @@ public final class TerminalView extends View {
|
||||
return false;
|
||||
}
|
||||
|
||||
public void checkForTypeface() {
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
File fontFile = new File(getContext().getFilesDir().getPath() + "/home/.termux/font.ttf");
|
||||
final Typeface newTypeface = fontFile.exists() ? Typeface.createFromFile(fontFile) : Typeface.MONOSPACE;
|
||||
if (newTypeface != mRenderer.mTypeface) {
|
||||
((Activity) getContext()).runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
mRenderer = new TerminalRenderer(mRenderer.mTextSize, newTypeface);
|
||||
updateSize();
|
||||
invalidate();
|
||||
} catch (Exception e) {
|
||||
Log.e(EmulatorDebug.LOG_TAG, "Error loading font", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(EmulatorDebug.LOG_TAG, "Error loading font", e);
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
public void checkForFontAndColors() {
|
||||
try {
|
||||
File fontFile = new File("/data/data/com.termux/files/home/.termux/font.ttf");
|
||||
File colorsFile = new File("/data/data/com.termux/files/home/.termux/colors.properties");
|
||||
|
||||
public void checkForColors() {
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
File colorsFile = new File(getContext().getFilesDir().getPath() + "/home/.termux/colors.properties");
|
||||
final Properties props = colorsFile.isFile() ? new Properties() : null;
|
||||
if (props != null) {
|
||||
try (InputStream in = new FileInputStream(colorsFile)) {
|
||||
props.load(in);
|
||||
}
|
||||
}
|
||||
((Activity) getContext()).runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
if (props == null) {
|
||||
TerminalColors.COLOR_SCHEME.reset();
|
||||
} else {
|
||||
TerminalColors.COLOR_SCHEME.updateWith(props);
|
||||
}
|
||||
if (mEmulator != null) mEmulator.mColors.reset();
|
||||
invalidate();
|
||||
} catch (Exception e) {
|
||||
Log.e(EmulatorDebug.LOG_TAG, "Setting colors failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Log.e(EmulatorDebug.LOG_TAG, "Failed colors handling", e);
|
||||
final Properties props = new Properties();
|
||||
if (colorsFile.isFile()) {
|
||||
try (InputStream in = new FileInputStream(colorsFile)) {
|
||||
props.load(in);
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
TerminalColors.COLOR_SCHEME.updateWith(props);
|
||||
if (mEmulator != null) mEmulator.mColors.reset();
|
||||
|
||||
final Typeface newTypeface = fontFile.exists() ? Typeface.createFromFile(fontFile) : Typeface.MONOSPACE;
|
||||
mRenderer = new TerminalRenderer(mRenderer.mTextSize, newTypeface);
|
||||
updateSize();
|
||||
|
||||
invalidate();
|
||||
} catch (Exception e) {
|
||||
Log.e(EmulatorDebug.LOG_TAG, "Error in checkForFontAndColors()", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -810,13 +831,151 @@ public final class TerminalView extends View {
|
||||
canvas.drawColor(0XFF000000);
|
||||
} else {
|
||||
mRenderer.render(mEmulator, canvas, mTopRow, mSelY1, mSelY2, mSelX1, mSelX2);
|
||||
|
||||
if (mIsSelectingText) {
|
||||
final int gripHandleWidth = mLeftSelectionHandle.getIntrinsicWidth();
|
||||
final int gripHandleMargin = gripHandleWidth / 4; // See the png.
|
||||
|
||||
int right = Math.round((mSelX1) * mRenderer.mFontWidth) + gripHandleMargin;
|
||||
int top = (mSelY1+1 - mTopRow)*mRenderer.mFontLineSpacing + mRenderer.mFontLineSpacingAndAscent;
|
||||
mLeftSelectionHandle.setBounds(right - gripHandleWidth, top, right, top + mLeftSelectionHandle.getIntrinsicHeight());
|
||||
mLeftSelectionHandle.draw(canvas);
|
||||
|
||||
int left = Math.round((mSelX2+1)*mRenderer.mFontWidth) - gripHandleMargin;
|
||||
top = (mSelY2+1 - mTopRow) *mRenderer.mFontLineSpacing + mRenderer.mFontLineSpacingAndAscent;
|
||||
mRightSelectionHandle.setBounds(left, top, left + gripHandleWidth, top + mRightSelectionHandle.getIntrinsicHeight());
|
||||
mRightSelectionHandle.draw(canvas);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Toggle text selection mode in the view. */
|
||||
public void toggleSelectingText() {
|
||||
@TargetApi(23)
|
||||
public void toggleSelectingText(MotionEvent ev) {
|
||||
mIsSelectingText = !mIsSelectingText;
|
||||
if (!mIsSelectingText) mSelX1 = mSelY1 = mSelX2 = mSelY2 = -1;
|
||||
mOnKeyListener.copyModeChanged(mIsSelectingText);
|
||||
|
||||
if (mIsSelectingText) {
|
||||
if (mLeftSelectionHandle == null) {
|
||||
mLeftSelectionHandle = (BitmapDrawable) getContext().getDrawable(R.drawable.text_select_handle_left_material);
|
||||
mRightSelectionHandle = (BitmapDrawable) getContext().getDrawable(R.drawable.text_select_handle_right_material);
|
||||
}
|
||||
|
||||
int cx = (int) (ev.getX() / mRenderer.mFontWidth);
|
||||
final boolean eventFromMouse = ev.isFromSource(InputDevice.SOURCE_MOUSE);
|
||||
// Offset for finger:
|
||||
final int SELECT_TEXT_OFFSET_Y = eventFromMouse ? 0 : -40;
|
||||
int cy = (int) ((ev.getY() + SELECT_TEXT_OFFSET_Y) / mRenderer.mFontLineSpacing) + mTopRow;
|
||||
|
||||
mSelX1 = mSelX2 = cx;
|
||||
mSelY1 = mSelY2 = cy;
|
||||
|
||||
TerminalBuffer screen = mEmulator.getScreen();
|
||||
if (!" ".equals(screen.getSelectedText(mSelX1, mSelY1, mSelX1, mSelY1))) {
|
||||
// Selecting something other than whitespace. Expand to word.
|
||||
while (mSelX1 > 0 && !"".equals(screen.getSelectedText(mSelX1-1, mSelY1, mSelX1-1, mSelY1))) {
|
||||
mSelX1--;
|
||||
}
|
||||
while (mSelX2 < mEmulator.mColumns-1 && !"".equals(screen.getSelectedText(mSelX2+1, mSelY1, mSelX2+1, mSelY1))) {
|
||||
mSelX2++;
|
||||
}
|
||||
}
|
||||
|
||||
mInitialTextSelection = true;
|
||||
mIsDraggingLeftSelection = true;
|
||||
mSelectionDownX = ev.getX();
|
||||
mSelectionDownY = ev.getY();
|
||||
|
||||
final ActionMode.Callback callback = new ActionMode.Callback() {
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
final int[] ACTION_MODE_ATTRS = { android.R.attr.actionModeCopyDrawable, android.R.attr.actionModePasteDrawable, };
|
||||
TypedArray styledAttributes = getContext().obtainStyledAttributes(ACTION_MODE_ATTRS);
|
||||
int show = MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT;
|
||||
|
||||
ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
menu.add(Menu.NONE, 1, Menu.NONE, R.string.copy_text).setIcon(styledAttributes.getResourceId(0, 0)).setShowAsAction(show);
|
||||
menu.add(Menu.NONE, 2, Menu.NONE, R.string.paste_text).setIcon(styledAttributes.getResourceId(1, 0)).setEnabled(clipboard.hasPrimaryClip()).setShowAsAction(show);
|
||||
menu.add(Menu.NONE, 3, Menu.NONE, R.string.text_selection_more);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case 1:
|
||||
String selectedText = mEmulator.getSelectedText(mSelX1, mSelY1, mSelX2, mSelY2).trim();
|
||||
mTermSession.clipboardText(selectedText);
|
||||
break;
|
||||
case 2:
|
||||
ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clipData = clipboard.getPrimaryClip();
|
||||
if (clipData != null) {
|
||||
CharSequence paste = clipData.getItemAt(0).coerceToText(getContext());
|
||||
if (!TextUtils.isEmpty(paste)) mEmulator.paste(paste.toString());
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
showContextMenu();
|
||||
break;
|
||||
}
|
||||
toggleSelectingText(null);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode) {
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
mActionMode = startActionMode(new ActionMode.Callback2() {
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
return callback.onCreateActionMode(mode, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||
return callback.onActionItemClicked(mode, item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode) {
|
||||
// Ignore.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
|
||||
int x1 = Math.round(mSelX1 * mRenderer.mFontWidth);
|
||||
int x2 = Math.round(mSelX2 * mRenderer.mFontWidth);
|
||||
int y1 = Math.round((mSelY1 - mTopRow) * mRenderer.mFontLineSpacing);
|
||||
int y2 = Math.round((mSelY2 + 1 - mTopRow) * mRenderer.mFontLineSpacing);
|
||||
outRect.set(Math.min(x1, x2), y1, Math.max(x1, x2), y2);
|
||||
}
|
||||
}, ActionMode.TYPE_FLOATING);
|
||||
} else {
|
||||
mActionMode = startActionMode(callback);
|
||||
}
|
||||
|
||||
|
||||
invalidate();
|
||||
} else {
|
||||
mActionMode.finish();
|
||||
mSelX1 = mSelY1 = mSelX2 = mSelY2 = -1;
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public TerminalSession getCurrentSession() {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:src="@drawable/text_select_handle_left_mtrl_alpha"
|
||||
android:tint="#2196F3" />
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:src="@drawable/text_select_handle_right_mtrl_alpha"
|
||||
android:tint="#2196F3" />
|
||||
BIN
app/src/main/res/raw/bell.ogg
Normal file
BIN
app/src/main/res/raw/bell.ogg
Normal file
Binary file not shown.
@@ -1,11 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="application_name">Termux</string>
|
||||
<string name="application_help">Termux help</string>
|
||||
<string name="shared_user_label">Termux user</string>
|
||||
<string name="new_session">New session</string>
|
||||
<string name="new_session_normal_unnamed">Normal - unnamed</string>
|
||||
<string name="new_session_normal_named">Normal - named</string>
|
||||
<string name="new_session_failsafe">Failsafe</string>
|
||||
<string name="toggle_soft_keyboard">Keyboard</string>
|
||||
<string name="reset_terminal">Reset</string>
|
||||
@@ -30,18 +27,18 @@
|
||||
|
||||
<string name="reset_toast_notification">Terminal reset.</string>
|
||||
|
||||
<string name="select">Select…</string>
|
||||
<string name="select_text">Select text</string>
|
||||
<string name="select_url">Select URL</string>
|
||||
<string name="select_url_dialog_title">Click URL to copy or long press to open</string>
|
||||
<string name="select_all_and_share">Select all text and share</string>
|
||||
<string name="select_all_and_share">Share transcript</string>
|
||||
<string name="select_url_no_found">No URL found in the terminal.</string>
|
||||
<string name="select_url_copied_to_clipboard">URL copied to clipboard</string>
|
||||
<string name="share_transcript_chooser_title">Send text to:</string>
|
||||
|
||||
<string name="paste_text">Paste</string>
|
||||
<string name="kill_process">Hangup</string>
|
||||
<string name="copy_text">Copy</string>
|
||||
<string name="text_selection_more">More…</string>
|
||||
|
||||
<string name="kill_process">Hangup</string>
|
||||
<string name="confirm_kill_process">Close this process?</string>
|
||||
|
||||
<string name="session_rename_title">Set session name</string>
|
||||
|
||||
@@ -6,10 +6,13 @@
|
||||
<style name="Theme.Termux" parent="@android:style/Theme.Material.Light.NoActionBar">
|
||||
<item name="android:statusBarColor">#000000</item>
|
||||
<item name="android:windowBackground">@android:color/black</item>
|
||||
|
||||
|
||||
<!-- Seen in buttons on left drawer: -->
|
||||
<item name="android:colorAccent">#212121</item>
|
||||
<item name="android:alertDialogTheme">@style/TermuxAlertDialogStyle</item>
|
||||
<item name="android:alertDialogTheme">@style/TermuxAlertDialogStyle</item>
|
||||
<!-- Avoid action mode toolbar pushing down terminal content when
|
||||
selecting text on pre-6.0 (non-floating toolbar). -->
|
||||
<item name="android:windowActionModeOverlay">true</item>
|
||||
</style>
|
||||
|
||||
<style name="TermuxAlertDialogStyle" parent="@android:style/Theme.Material.Light.Dialog.Alert">
|
||||
@@ -17,4 +20,4 @@
|
||||
<item name="android:colorAccent">#212121</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
@@ -111,6 +111,10 @@ public class KeyHandlerTest extends TestCase {
|
||||
// Backspace.
|
||||
assertKeysEquals("\u007f", KeyHandler.getCode(KeyEvent.KEYCODE_DEL, 0, false, false));
|
||||
|
||||
// Space.
|
||||
assertNull(KeyHandler.getCode(KeyEvent.KEYCODE_SPACE, 0, false, false));
|
||||
assertKeysEquals("\u0000", KeyHandler.getCode(KeyEvent.KEYCODE_SPACE, KeyHandler.KEYMOD_CTRL, false, false));
|
||||
|
||||
// Back tab.
|
||||
assertKeysEquals("\033[Z", KeyHandler.getCode(KeyEvent.KEYCODE_TAB, KeyHandler.KEYMOD_SHIFT, false, false));
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ public class OperatingSystemControlTest extends TerminalTestCase {
|
||||
withTerminalSized(10, 10);
|
||||
enterString("\033]0;Hello, world\007");
|
||||
assertEquals("Hello, world", mTerminal.getTitle());
|
||||
expectedTitleChanges.add(new ChangedTitle((String) null, "Hello, world"));
|
||||
expectedTitleChanges.add(new ChangedTitle(null, "Hello, world"));
|
||||
assertEquals(expectedTitleChanges, mOutput.titleChanges);
|
||||
|
||||
enterString("\033]0;Goodbye, world\007");
|
||||
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
||||
#Sat Nov 28 23:59:10 CET 2015
|
||||
#Wed Dec 23 01:44:47 CET 2015
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-2.9-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-bin.zip
|
||||
|
||||
90
gradlew.bat
vendored
90
gradlew.bat
vendored
@@ -1,90 +0,0 @@
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS=
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windowz variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
if "%@eval[2+2]" == "4" goto 4NT_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
goto execute
|
||||
|
||||
:4NT_args
|
||||
@rem Get arguments from the 4NT Shell from JP Software
|
||||
set CMD_LINE_ARGS=%$
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
BIN
travis.keystore
BIN
travis.keystore
Binary file not shown.
Reference in New Issue
Block a user