Project: Add app icon
This commit is contained in:
parent
b611b0eaf4
commit
53fd024b14
178
app/src/main/java/io/neoterm/backend/ShellTermSession.kt
Normal file
178
app/src/main/java/io/neoterm/backend/ShellTermSession.kt
Normal file
@ -0,0 +1,178 @@
|
||||
package io.neoterm.backend
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import io.neoterm.R
|
||||
import io.neoterm.preference.NeoPreference
|
||||
import io.neoterm.preference.NeoTermPath
|
||||
import io.neoterm.ui.term.tab.TermSessionCallback
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* @author kiva
|
||||
*/
|
||||
open class ShellTermSession : TerminalSession {
|
||||
class Builder {
|
||||
private var shell: String? = null
|
||||
private var cwd: String? = null
|
||||
private var args: MutableList<String>? = null
|
||||
private var env: MutableList<Pair<String, String>>? = null
|
||||
private var changeCallback: SessionChangedCallback? = null
|
||||
private var systemShell = false
|
||||
|
||||
fun shell(shell: String?): Builder {
|
||||
this.shell = shell
|
||||
return this
|
||||
}
|
||||
|
||||
fun currentWorkingDirectory(cwd: String?): Builder {
|
||||
this.cwd = cwd
|
||||
return this
|
||||
}
|
||||
|
||||
fun arg(arg: String?): Builder {
|
||||
if (arg != null) {
|
||||
if (args == null) {
|
||||
args = mutableListOf(arg)
|
||||
} else {
|
||||
args!!.add(arg)
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun args(vararg args: String?): Builder {
|
||||
if (args.isEmpty()) {
|
||||
this.args = null
|
||||
return this
|
||||
}
|
||||
args.forEach { arg(it) }
|
||||
return this
|
||||
}
|
||||
|
||||
fun env(env: Pair<String, String>): Builder {
|
||||
if (this.env == null) {
|
||||
this.env = mutableListOf(env)
|
||||
} else {
|
||||
this.env!!.add(env)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun envs(vararg env: Pair<String, String>): Builder {
|
||||
if (env.isEmpty()) {
|
||||
this.env = null
|
||||
return this
|
||||
}
|
||||
env.forEach { env(it) }
|
||||
return this
|
||||
}
|
||||
|
||||
fun callback(callback: SessionChangedCallback): Builder {
|
||||
this.changeCallback = callback
|
||||
return this
|
||||
}
|
||||
|
||||
fun systemShell(systemShell: Boolean): Builder {
|
||||
this.systemShell = systemShell
|
||||
return this
|
||||
}
|
||||
|
||||
fun create(context: Context): ShellTermSession {
|
||||
val cwd = this.cwd ?: NeoTermPath.HOME_PATH
|
||||
|
||||
var shell = this.shell ?:
|
||||
if (systemShell)
|
||||
"/system/bin/sh"
|
||||
else
|
||||
NeoTermPath.USR_PATH + "/bin/" + NeoPreference.loadString(R.string.key_general_shell, "sh")
|
||||
|
||||
if (!File(shell).exists()) {
|
||||
Toast.makeText(context, context.getString(R.string.shell_not_found, shell), Toast.LENGTH_LONG).show()
|
||||
shell = NeoTermPath.USR_PATH + "/bin/sh"
|
||||
}
|
||||
|
||||
val args = this.args ?: mutableListOf(shell)
|
||||
val env = transformEnvironment(this.env) ?: buildEnvironment(cwd, systemShell, shell)
|
||||
val callback = changeCallback ?: TermSessionCallback()
|
||||
return ShellTermSession(shell, cwd, args.toTypedArray(), env, callback)
|
||||
}
|
||||
|
||||
private fun transformEnvironment(env: MutableList<Pair<String, String>>?): Array<String>? {
|
||||
if (env == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val result = mutableListOf<String>()
|
||||
env.mapTo(result, { "${it.first}=${it.second}" })
|
||||
return result.toTypedArray()
|
||||
}
|
||||
|
||||
|
||||
private fun buildEnvironment(cwd: String?, systemShell: Boolean, executablePath: String): Array<String> {
|
||||
var cwd = cwd
|
||||
File(NeoTermPath.HOME_PATH).mkdirs()
|
||||
|
||||
if (cwd == null) cwd = NeoTermPath.HOME_PATH
|
||||
|
||||
val termEnv = "TERM=xterm-256color"
|
||||
val homeEnv = "HOME=" + NeoTermPath.HOME_PATH
|
||||
val androidRootEnv = "ANDROID_ROOT=" + System.getenv("ANDROID_ROOT")
|
||||
val androidDataEnv = "ANDROID_DATA=" + System.getenv("ANDROID_DATA")
|
||||
val externalStorageEnv = "EXTERNAL_STORAGE=" + System.getenv("EXTERNAL_STORAGE")
|
||||
|
||||
if (systemShell) {
|
||||
val pathEnv = "PATH=" + System.getenv("PATH")
|
||||
return arrayOf(termEnv, homeEnv, androidRootEnv, androidDataEnv, externalStorageEnv, pathEnv)
|
||||
|
||||
} else {
|
||||
val ps1Env = "PS1=$ "
|
||||
val langEnv = "LANG=en_US.UTF-8"
|
||||
val pathEnv = "PATH=" + buildPathEnv()
|
||||
val ldEnv = "LD_LIBRARY_PATH=" + buildLdLibraryEnv()
|
||||
val pwdEnv = "PWD=" + cwd
|
||||
val tmpdirEnv = "TMPDIR=${NeoTermPath.USR_PATH}/tmp"
|
||||
|
||||
return arrayOf(termEnv, homeEnv, ps1Env, ldEnv, langEnv, pathEnv, pwdEnv, androidRootEnv, androidDataEnv, externalStorageEnv, tmpdirEnv)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildLdLibraryEnv(): String {
|
||||
val builder = StringBuilder("${NeoTermPath.USR_PATH}/lib")
|
||||
|
||||
val programSelection = NeoPreference.loadString(R.string.key_general_program_selection, NeoPreference.VALUE_NEOTERM_ONLY)
|
||||
val systemPath = System.getenv("LD_LIBRARY_PATH")
|
||||
|
||||
if (programSelection != NeoPreference.VALUE_NEOTERM_ONLY) {
|
||||
builder.append(":$systemPath")
|
||||
}
|
||||
|
||||
return builder.toString()
|
||||
}
|
||||
|
||||
private fun buildPathEnv(): String {
|
||||
val builder = StringBuilder()
|
||||
val programSelection = NeoPreference.loadString(R.string.key_general_program_selection, NeoPreference.VALUE_NEOTERM_ONLY)
|
||||
val basePath = "${NeoTermPath.USR_PATH}/bin:${NeoTermPath.USR_PATH}/bin/applets"
|
||||
val systemPath = System.getenv("PATH")
|
||||
|
||||
when (programSelection) {
|
||||
NeoPreference.VALUE_NEOTERM_ONLY -> {
|
||||
builder.append(basePath)
|
||||
}
|
||||
NeoPreference.VALUE_NEOTERM_FIRST -> {
|
||||
builder.append("$basePath:$systemPath")
|
||||
}
|
||||
NeoPreference.VALUE_SYSTEM_FIRST -> {
|
||||
builder.append("$systemPath:$basePath")
|
||||
}
|
||||
}
|
||||
return builder.toString()
|
||||
}
|
||||
}
|
||||
|
||||
private constructor(shellPath: String, cwd: String,
|
||||
args: Array<String>, env: Array<String>,
|
||||
changeCallback: SessionChangedCallback)
|
||||
: super(shellPath, cwd, args, env, changeCallback)
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
package io.neoterm.terminal
|
||||
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import io.neoterm.customize.completion.AutoCompleteManager
|
||||
import io.neoterm.customize.completion.CompleteCandidate
|
||||
import io.neoterm.view.AutoCompletePopupWindow
|
||||
import io.neoterm.view.OnAutoCompleteListener
|
||||
import io.neoterm.view.TerminalView
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* @author kiva
|
||||
*/
|
||||
class TermCompleteListener(var terminalView: TerminalView?) : OnAutoCompleteListener {
|
||||
|
||||
private val inputStack = Stack<Char>()
|
||||
private val popupWindow = AutoCompletePopupWindow(terminalView!!.context)
|
||||
|
||||
override fun onKeyCode(keyCode: Int, keyMod: Int) {
|
||||
when (keyCode) {
|
||||
KeyEvent.KEYCODE_DEL -> {
|
||||
Log.e("NeoTerm-AC", "BackSpace")
|
||||
popChar()
|
||||
activateAutoCompletion()
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_ENTER -> {
|
||||
Log.e("NeoTerm-AC", "Clear Chars")
|
||||
clearChars()
|
||||
popupWindow.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAutoComplete(newText: String?) {
|
||||
if (newText == null || newText.isEmpty()) {
|
||||
return
|
||||
}
|
||||
newText.toCharArray().forEach { pushChar(it) }
|
||||
activateAutoCompletion()
|
||||
}
|
||||
|
||||
override fun onCleanUp() {
|
||||
popupWindow.cleanup()
|
||||
terminalView = null
|
||||
}
|
||||
|
||||
private fun activateAutoCompletion() {
|
||||
val text = getCurrentEditingText()
|
||||
if (text.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val candidates = AutoCompleteManager.filter(text)
|
||||
Log.e("NeoTerm-AC", "Completing for $text")
|
||||
candidates.forEach {
|
||||
Log.e("NeoTerm-AC", " Candidate: ${it.completeString}")
|
||||
}
|
||||
if (candidates.isNotEmpty()) {
|
||||
showAutoCompleteCandidates(candidates)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showAutoCompleteCandidates(candidates: List<CompleteCandidate>) {
|
||||
popupWindow.candidates = candidates
|
||||
popupWindow.show(terminalView!!)
|
||||
}
|
||||
|
||||
private fun getCurrentEditingText(): String {
|
||||
val builder = StringBuilder()
|
||||
val size = inputStack.size
|
||||
(0..(size - 1))
|
||||
.map { inputStack[it] }
|
||||
.takeWhile { !(it == 0.toChar() || it == ' ') }
|
||||
.forEach { builder.append(it) }
|
||||
return builder.toString()
|
||||
}
|
||||
|
||||
private fun clearChars() {
|
||||
inputStack.clear()
|
||||
}
|
||||
|
||||
private fun popChar() {
|
||||
if (inputStack.isNotEmpty()) {
|
||||
inputStack.pop()
|
||||
}
|
||||
}
|
||||
|
||||
private fun pushChar(char: Char) {
|
||||
inputStack.push(char)
|
||||
}
|
||||
}
|
69
app/src/main/java/io/neoterm/terminal/TermSessionCallback.kt
Normal file
69
app/src/main/java/io/neoterm/terminal/TermSessionCallback.kt
Normal file
@ -0,0 +1,69 @@
|
||||
package io.neoterm.terminal
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.media.SoundPool
|
||||
import android.os.Vibrator
|
||||
import io.neoterm.R
|
||||
import io.neoterm.backend.TerminalSession
|
||||
import io.neoterm.preference.NeoPreference
|
||||
import io.neoterm.ui.term.tab.TermTab
|
||||
import io.neoterm.view.TerminalView
|
||||
|
||||
/**
|
||||
* @author kiva
|
||||
*/
|
||||
class TermSessionCallback : TerminalSession.SessionChangedCallback {
|
||||
var termView: TerminalView? = null
|
||||
var termTab: TermTab? = null
|
||||
|
||||
var bellId: Int = 0
|
||||
var soundPool: SoundPool? = null
|
||||
|
||||
override fun onTextChanged(changedSession: TerminalSession?) {
|
||||
termView?.onScreenUpdated()
|
||||
}
|
||||
|
||||
override fun onTitleChanged(changedSession: TerminalSession?) {
|
||||
if (changedSession?.title != null) {
|
||||
termTab?.updateTitle(changedSession.title)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSessionFinished(finishedSession: TerminalSession?) {
|
||||
termTab?.onSessionFinished()
|
||||
}
|
||||
|
||||
override fun onClipboardText(session: TerminalSession?, text: String?) {
|
||||
if (termView != null) {
|
||||
val clipboard = termView!!.context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipboard.primaryClip = ClipData.newPlainText("", text)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBell(session: TerminalSession?) {
|
||||
if (termView == null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (NeoPreference.loadBoolean(R.string.key_general_bell, false)) {
|
||||
if (soundPool == null) {
|
||||
soundPool = SoundPool.Builder().setMaxStreams(1).build()
|
||||
bellId = soundPool!!.load(termView!!.context, R.raw.bell, 1)
|
||||
}
|
||||
soundPool?.play(bellId, 1f, 1f, 0, 0, 1f)
|
||||
}
|
||||
|
||||
if (NeoPreference.loadBoolean(R.string.key_general_vibrate, false)) {
|
||||
val vibrator = termView!!.context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||
vibrator.vibrate(100)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onColorsChanged(session: TerminalSession?) {
|
||||
if (session != null) {
|
||||
termView?.onScreenUpdated()
|
||||
}
|
||||
}
|
||||
}
|
205
app/src/main/java/io/neoterm/terminal/TermViewClient.kt
Normal file
205
app/src/main/java/io/neoterm/terminal/TermViewClient.kt
Normal file
@ -0,0 +1,205 @@
|
||||
package io.neoterm.terminal
|
||||
|
||||
import android.content.Context
|
||||
import android.media.AudioManager
|
||||
import android.view.InputDevice
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import io.neoterm.R
|
||||
import io.neoterm.backend.KeyHandler
|
||||
import io.neoterm.backend.TerminalSession
|
||||
import io.neoterm.customize.eks.EksKeysManager
|
||||
import io.neoterm.preference.NeoPreference
|
||||
import io.neoterm.ui.term.tab.TermTab
|
||||
import io.neoterm.view.ExtraKeysView
|
||||
import io.neoterm.view.TerminalView
|
||||
import io.neoterm.view.TerminalViewClient
|
||||
|
||||
|
||||
/**
|
||||
* @author kiva
|
||||
*/
|
||||
class TermViewClient(val context: Context) : TerminalViewClient {
|
||||
private var mVirtualControlKeyDown: Boolean = false
|
||||
private var mVirtualFnKeyDown: Boolean = false
|
||||
private var lastTitle: String = ""
|
||||
|
||||
var sessionFinished: Boolean = false
|
||||
|
||||
var termTab: TermTab? = null
|
||||
var termView: TerminalView? = null
|
||||
var extraKeysView: ExtraKeysView? = null
|
||||
|
||||
override fun onScale(scale: Float): Float {
|
||||
if (scale < 0.9f || scale > 1.1f) {
|
||||
val increase = scale > 1f
|
||||
changeFontSize(increase)
|
||||
return 1.0f
|
||||
}
|
||||
return scale
|
||||
}
|
||||
|
||||
override fun onSingleTapUp(e: MotionEvent?) {
|
||||
(context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager)
|
||||
.showSoftInput(termView, InputMethodManager.SHOW_IMPLICIT)
|
||||
}
|
||||
|
||||
override fun shouldBackButtonBeMappedToEscape(): Boolean {
|
||||
return NeoPreference.loadBoolean(R.string.key_generaL_backspace_map_to_esc, false)
|
||||
}
|
||||
|
||||
override fun copyModeChanged(copyMode: Boolean) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
override fun onKeyDown(keyCode: Int, e: KeyEvent?, session: TerminalSession?): Boolean {
|
||||
when (keyCode) {
|
||||
KeyEvent.KEYCODE_ENTER -> {
|
||||
if (e?.action == KeyEvent.ACTION_DOWN && sessionFinished) {
|
||||
termTab?.requireCloseTab()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
if (e != null && e.isCtrlPressed && e.isAltPressed) {
|
||||
// Get the unmodified code point:
|
||||
val unicodeChar = e.getUnicodeChar(0).toChar()
|
||||
|
||||
if (unicodeChar == 'f'/* full screen */) {
|
||||
termTab?.requireToggleFullScreen()
|
||||
} else if (unicodeChar == 'v') {
|
||||
termTab?.requirePaste()
|
||||
} else if (unicodeChar == '+' || e.getUnicodeChar(KeyEvent.META_SHIFT_ON).toChar() == '+') {
|
||||
// We also check for the shifted char here since shift may be required to produce '+',
|
||||
// see https://github.com/termux/termux-api/issues/2
|
||||
changeFontSize(true)
|
||||
} else if (unicodeChar == '-') {
|
||||
changeFontSize(false)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onKeyUp(keyCode: Int, e: KeyEvent?): Boolean {
|
||||
return handleVirtualKeys(keyCode, e, false)
|
||||
}
|
||||
|
||||
override fun readControlKey(): Boolean {
|
||||
return (extraKeysView != null && extraKeysView!!.readControlButton()) || mVirtualControlKeyDown
|
||||
}
|
||||
|
||||
override fun readAltKey(): Boolean {
|
||||
return (extraKeysView != null && extraKeysView!!.readAltButton()) || mVirtualFnKeyDown
|
||||
}
|
||||
|
||||
override fun onCodePoint(codePoint: Int, ctrlDown: Boolean, session: TerminalSession?): Boolean {
|
||||
if (mVirtualFnKeyDown) {
|
||||
var resultingKeyCode: Int = -1
|
||||
var resultingCodePoint: Int = -1
|
||||
var altDown = false
|
||||
val lowerCase = Character.toLowerCase(codePoint)
|
||||
when (lowerCase.toChar()) {
|
||||
// Arrow keys.
|
||||
'w' -> resultingKeyCode = KeyEvent.KEYCODE_DPAD_UP
|
||||
'a' -> resultingKeyCode = KeyEvent.KEYCODE_DPAD_LEFT
|
||||
's' -> resultingKeyCode = KeyEvent.KEYCODE_DPAD_DOWN
|
||||
'd' -> resultingKeyCode = KeyEvent.KEYCODE_DPAD_RIGHT
|
||||
|
||||
// Page up and down.
|
||||
'p' -> resultingKeyCode = KeyEvent.KEYCODE_PAGE_UP
|
||||
'n' -> resultingKeyCode = KeyEvent.KEYCODE_PAGE_DOWN
|
||||
|
||||
// Some special keys:
|
||||
't' -> resultingKeyCode = KeyEvent.KEYCODE_TAB
|
||||
'i' -> resultingKeyCode = KeyEvent.KEYCODE_INSERT
|
||||
'h' -> resultingCodePoint = '~'.toInt()
|
||||
|
||||
// Special characters to input.
|
||||
'u' -> resultingCodePoint = '_'.toInt()
|
||||
'l' -> resultingCodePoint = '|'.toInt()
|
||||
|
||||
// Function keys.
|
||||
'1', '2', '3', '4', '5', '6', '7', '8', '9' -> resultingKeyCode = codePoint - '1'.toInt() + KeyEvent.KEYCODE_F1
|
||||
'0' -> resultingKeyCode = KeyEvent.KEYCODE_F10
|
||||
|
||||
// Other special keys.
|
||||
'e' -> resultingCodePoint = 27 /*Escape*/
|
||||
'.' -> resultingCodePoint = 28 /*^.*/
|
||||
|
||||
'b' // alt+b, jumping backward in readline.
|
||||
, 'f' // alf+f, jumping forward in readline.
|
||||
, 'x' // alt+x, common in emacs.
|
||||
-> {
|
||||
resultingCodePoint = lowerCase
|
||||
altDown = true
|
||||
}
|
||||
|
||||
// Volume control.
|
||||
'v' -> {
|
||||
resultingCodePoint = -1
|
||||
val audio = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
audio.adjustSuggestedStreamVolume(AudioManager.ADJUST_SAME, AudioManager.USE_DEFAULT_STREAM_TYPE, AudioManager.FLAG_SHOW_UI)
|
||||
}
|
||||
}
|
||||
|
||||
if (resultingKeyCode != -1) {
|
||||
if (session != null) {
|
||||
val term = session.emulator
|
||||
session.write(KeyHandler.getCode(resultingKeyCode, 0, term.isCursorKeysApplicationMode, term.isKeypadApplicationMode))
|
||||
}
|
||||
} else if (resultingCodePoint != -1) {
|
||||
session?.writeCodePoint(altDown, resultingCodePoint)
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onLongPress(event: MotionEvent?): Boolean {
|
||||
// TODO
|
||||
return false
|
||||
}
|
||||
|
||||
private fun handleVirtualKeys(keyCode: Int, event: KeyEvent?, down: Boolean): Boolean {
|
||||
if (event == null) {
|
||||
return false
|
||||
}
|
||||
val inputDevice = event.device
|
||||
if (inputDevice != null && inputDevice.keyboardType == InputDevice.KEYBOARD_TYPE_ALPHABETIC) {
|
||||
return false
|
||||
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
|
||||
mVirtualControlKeyDown = down
|
||||
return true
|
||||
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
|
||||
mVirtualFnKeyDown = down
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun updateSuggestions(title: String?, force: Boolean = false) {
|
||||
if (extraKeysView == null || title == null || title.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (lastTitle != title || force) {
|
||||
removeSuggestions()
|
||||
EksKeysManager.showShortcutKeys(title, extraKeysView)
|
||||
extraKeysView?.updateButtons()
|
||||
lastTitle = title
|
||||
}
|
||||
}
|
||||
|
||||
fun removeSuggestions() {
|
||||
extraKeysView?.clearUserDefinedButton()
|
||||
}
|
||||
|
||||
private fun changeFontSize(increase: Boolean) {
|
||||
val changedSize = (if (increase) 1 else -1) * 2
|
||||
val fontSize = termView!!.textSize + changedSize
|
||||
termView!!.textSize = fontSize
|
||||
NeoPreference.store(NeoPreference.KEY_FONT_SIZE, fontSize)
|
||||
}
|
||||
}
|
BIN
icon-512.png
Normal file
BIN
icon-512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
Loading…
Reference in New Issue
Block a user