Project: Refactor Terminal Client

This commit is contained in:
zt515 2017-07-21 00:42:30 +08:00
parent e21d7ccf6f
commit bdc8f60da0
21 changed files with 441 additions and 253 deletions

View File

@ -17,8 +17,8 @@ android {
applicationId "io.neoterm"
minSdkVersion 21
targetSdkVersion 25
versionCode 13
versionName "1.2.0-rc2"
versionCode 14
versionName "1.2.0-rc3"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
resConfigs "zh"
externalNativeBuild {

View File

@ -28,7 +28,7 @@ import java.util.UUID;
* <p>
* NOTE: The terminal session may outlive the EmulatorView, so be careful with callbacks!
*/
public final class TerminalSession extends TerminalOutput {
public class TerminalSession extends TerminalOutput {
/** Callback to be invoked when a {@link TerminalSession} changes. */
public interface SessionChangedCallback {
@ -143,18 +143,16 @@ public final class TerminalSession extends TerminalOutput {
private final String mShellPath;
private final String mCwd;
private final String mInitialCommand;
private final String[] mArgs;
private final String[] mEnv;
public TerminalSession(String shellPath, String cwd, String initialCommand, String[] args, String[] env, SessionChangedCallback changeCallback) {
public TerminalSession(String shellPath, String cwd, String[] args, String[] env, SessionChangedCallback changeCallback) {
mChangeCallback = changeCallback;
this.mShellPath = shellPath;
this.mCwd = cwd;
this.mArgs = args;
this.mEnv = env;
this.mInitialCommand = initialCommand;
}
/** Inform the attached pty of the new size and reflow or initialize the emulator. */
@ -227,10 +225,6 @@ public final class TerminalSession extends TerminalOutput {
mMainThreadHandler.sendMessage(mMainThreadHandler.obtainMessage(MSG_PROCESS_EXITED, processExitCode));
}
}.start();
if (mInitialCommand != null && mInitialCommand.length() > 0) {
write(mInitialCommand + '\r');
}
}
/** Write data to the shell process. */

View File

@ -96,67 +96,6 @@ object NeoPreference {
return null
}
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, VALUE_NEOTERM_ONLY)
val systemPath = System.getenv("LD_LIBRARY_PATH")
if (programSelection != 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, VALUE_NEOTERM_ONLY)
val basePath = "${NeoTermPath.USR_PATH}/bin:${NeoTermPath.USR_PATH}/bin/applets"
val systemPath = System.getenv("PATH")
when (programSelection) {
VALUE_NEOTERM_ONLY -> {
builder.append(basePath)
}
VALUE_NEOTERM_FIRST -> {
builder.append("$basePath:$systemPath")
}
VALUE_SYSTEM_FIRST -> {
builder.append("$systemPath:$basePath")
}
}
return builder.toString()
}
/**
* TODO
* To print the job name about to be executed in bash:

View File

@ -76,8 +76,11 @@ class NeoTermService : Service() {
val sessions: List<TerminalSession>
get() = mTerminalSessions
fun createTermSession(executablePath: String?, arguments: Array<String>?, cwd: String?, initialCommand: String?, env: Array<String>?, sessionCallback: TerminalSession.SessionChangedCallback?, systemShell: Boolean): TerminalSession {
val session = TerminalUtils.createSession(this, executablePath, arguments, cwd, initialCommand, env, sessionCallback, systemShell)
fun createTermSession(executablePath: String?, arguments: Array<String>?,
cwd: String?, initialCommand: String?,
env: Array<Pair<String, String>>?, sessionCallback:
TerminalSession.SessionChangedCallback?, systemShell: Boolean): TerminalSession {
val session = TerminalUtils.createShellSession(this, executablePath, arguments, cwd, initialCommand, env, sessionCallback, systemShell)
mTerminalSessions.add(session)
updateNotification()
return session

View File

@ -0,0 +1,199 @@
package io.neoterm.terminal
import android.content.Context
import android.widget.Toast
import io.neoterm.R
import io.neoterm.backend.TerminalSession
import io.neoterm.preference.NeoPreference
import io.neoterm.preference.NeoTermPath
import io.neoterm.terminal.client.TermSessionCallback
import java.io.File
/**
* @author kiva
*/
open class ShellTermSession private constructor(shellPath: String, cwd: String, args: Array<String>, env: Array<String>, changeCallback: SessionChangedCallback) : TerminalSession(shellPath, cwd, args, env, changeCallback) {
var initialCommand : String? = null
override fun initializeEmulator(columns: Int, rows: Int) {
super.initializeEmulator(columns, rows)
val initCommand = initialCommand
if (initCommand != null && initCommand.isNotEmpty()) {
write(initCommand + '\r')
}
}
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)
}
} else {
this.args = null
}
return this
}
fun argArray(args: Array<String>?): Builder {
if (args != null) {
if (args.isEmpty()) {
this.args = null
return this
}
args.forEach { arg(it) }
} else {
this.args = null
}
return this
}
fun env(env: Pair<String, String>?): Builder {
if (env != null) {
if (this.env == null) {
this.env = mutableListOf(env)
} else {
this.env!!.add(env)
}
} else {
this.env = null
}
return this
}
fun envArray(env: Array<Pair<String, String>>?): Builder {
if (env != null) {
if (env.isEmpty()) {
this.env = null
return this
}
env.forEach { env(it) }
} else {
this.env = null
}
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()
}
}
}

View File

@ -1,4 +1,4 @@
package io.neoterm.ui.term.tab
package io.neoterm.terminal.client
import android.util.Log
import android.view.KeyEvent

View File

@ -0,0 +1,47 @@
package io.neoterm.terminal.client
import io.neoterm.backend.TerminalSession
import io.neoterm.view.ExtraKeysView
import io.neoterm.view.OnAutoCompleteListener
import io.neoterm.view.TerminalView
/**
* @author kiva
*/
class TermDataHolder {
var termSession: TerminalSession? = null
var sessionCallback: TermSessionCallback? = null
var viewClient: TermViewClient? = null
var onAutoCompleteListener: OnAutoCompleteListener? = null
var termUI: TermUiPresenter? = null
var termView: TerminalView? = null
var extraKeysView: ExtraKeysView? = null
fun cleanup() {
onAutoCompleteListener?.onCleanUp()
onAutoCompleteListener = null
sessionCallback?.termData = null
viewClient?.termData = null
termUI = null
termView = null
extraKeysView = null
termSession = null
}
fun initializeSessionWith(session: TerminalSession, sessionCallback: TermSessionCallback?, viewClient: TermViewClient?) {
this.termSession = session
this.sessionCallback = sessionCallback
this.viewClient = viewClient
sessionCallback?.termData = this
viewClient?.termData = this
}
fun initializeViewWith(termUI: TermUiPresenter?, termView: TerminalView?, eks: ExtraKeysView?) {
this.termUI = termUI
this.termView = termView
this.extraKeysView = eks
}
}

View File

@ -1,4 +1,4 @@
package io.neoterm.ui.term.tab
package io.neoterm.terminal.client
import android.content.ClipData
import android.content.ClipboardManager
@ -8,43 +8,40 @@ import android.os.Vibrator
import io.neoterm.R
import io.neoterm.backend.TerminalSession
import io.neoterm.preference.NeoPreference
import io.neoterm.view.TerminalView
/**
* @author kiva
*/
class TermSessionCallback : TerminalSession.SessionChangedCallback {
var termView: TerminalView? = null
var termTab: TermTab? = null
var termData: TermDataHolder? = null
var bellId: Int = 0
var soundPool: SoundPool? = null
override fun onTextChanged(changedSession: TerminalSession?) {
termView?.onScreenUpdated()
termData?.termView?.onScreenUpdated()
}
override fun onTitleChanged(changedSession: TerminalSession?) {
if (changedSession?.title != null) {
termTab?.updateTitle(changedSession.title)
termData?.termUI?.requireUpdateTitle(changedSession.title)
}
}
override fun onSessionFinished(finishedSession: TerminalSession?) {
termTab?.onSessionFinished()
termData?.termUI?.requireOnSessionFinished()
}
override fun onClipboardText(session: TerminalSession?, text: String?) {
val termView = termData?.termView
if (termView != null) {
val clipboard = termView!!.context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clipboard = termView.context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.primaryClip = ClipData.newPlainText("", text)
}
}
override fun onBell(session: TerminalSession?) {
if (termView == null) {
return
}
val termView = termData?.termView ?: return
if (NeoPreference.loadBoolean(R.string.key_general_bell, false)) {
if (soundPool == null) {
@ -61,8 +58,9 @@ class TermSessionCallback : TerminalSession.SessionChangedCallback {
}
override fun onColorsChanged(session: TerminalSession?) {
if (session != null) {
termView?.onScreenUpdated()
val termView = termData?.termView
if (session != null && termView != null) {
termView.onScreenUpdated()
}
}
}

View File

@ -0,0 +1,12 @@
package io.neoterm.terminal.client
/**
* @author kiva
*/
interface TermUiPresenter {
fun requireClose()
fun requireToggleFullScreen()
fun requirePaste()
fun requireUpdateTitle(title: String?)
fun requireOnSessionFinished()
}

View File

@ -1,4 +1,4 @@
package io.neoterm.ui.term.tab
package io.neoterm.terminal.client
import android.content.Context
import android.media.AudioManager
@ -11,8 +11,6 @@ import io.neoterm.backend.KeyHandler
import io.neoterm.backend.TerminalSession
import io.neoterm.customize.eks.EksKeysManager
import io.neoterm.preference.NeoPreference
import io.neoterm.view.ExtraKeysView
import io.neoterm.view.TerminalView
import io.neoterm.view.TerminalViewClient
@ -26,9 +24,7 @@ class TermViewClient(val context: Context) : TerminalViewClient {
var sessionFinished: Boolean = false
var termTab: TermTab? = null
var termView: TerminalView? = null
var extraKeysView: ExtraKeysView? = null
var termData: TermDataHolder? = null
override fun onScale(scale: Float): Float {
if (scale < 0.9f || scale > 1.1f) {
@ -40,9 +36,12 @@ class TermViewClient(val context: Context) : TerminalViewClient {
}
override fun onSingleTapUp(e: MotionEvent?) {
val termView = termData?.termView
if (termView != null) {
(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)
@ -53,10 +52,12 @@ class TermViewClient(val context: Context) : TerminalViewClient {
}
override fun onKeyDown(keyCode: Int, e: KeyEvent?, session: TerminalSession?): Boolean {
val termUI = termData?.termUI
when (keyCode) {
KeyEvent.KEYCODE_ENTER -> {
if (e?.action == KeyEvent.ACTION_DOWN && sessionFinished) {
termTab?.requireCloseTab()
termUI?.requireClose()
return true
}
return false
@ -67,9 +68,9 @@ class TermViewClient(val context: Context) : TerminalViewClient {
val unicodeChar = e.getUnicodeChar(0).toChar()
if (unicodeChar == 'f'/* full screen */) {
termTab?.requireToggleFullScreen()
termUI?.requireToggleFullScreen()
} else if (unicodeChar == 'v') {
termTab?.requirePaste()
termUI?.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
@ -86,11 +87,13 @@ class TermViewClient(val context: Context) : TerminalViewClient {
}
override fun readControlKey(): Boolean {
return (extraKeysView != null && extraKeysView!!.readControlButton()) || mVirtualControlKeyDown
val extraKeysView = termData?.extraKeysView
return (extraKeysView != null && extraKeysView.readControlButton()) || mVirtualControlKeyDown
}
override fun readAltKey(): Boolean {
return (extraKeysView != null && extraKeysView!!.readAltButton()) || mVirtualFnKeyDown
val extraKeysView = termData?.extraKeysView
return (extraKeysView != null && extraKeysView.readAltButton()) || mVirtualFnKeyDown
}
override fun onCodePoint(codePoint: Int, ctrlDown: Boolean, session: TerminalSession?): Boolean {
@ -179,6 +182,8 @@ class TermViewClient(val context: Context) : TerminalViewClient {
}
fun updateSuggestions(title: String?, force: Boolean = false) {
val extraKeysView = termData?.extraKeysView
if (extraKeysView == null || title == null || title.isEmpty()) {
return
}
@ -192,13 +197,17 @@ class TermViewClient(val context: Context) : TerminalViewClient {
}
fun removeSuggestions() {
val extraKeysView = termData?.extraKeysView
extraKeysView?.clearUserDefinedButton()
}
private fun changeFontSize(increase: Boolean) {
val termView = termData?.termView
if (termView != null) {
val changedSize = (if (increase) 1 else -1) * 2
val fontSize = termView!!.textSize + changedSize
termView!!.textSize = fontSize
val fontSize = termView.textSize + changedSize
termView.textSize = fontSize
NeoPreference.store(NeoPreference.KEY_FONT_SIZE, fontSize)
}
}
}

View File

@ -2,7 +2,6 @@ package io.neoterm.ui.customization
import android.app.Activity
import android.content.Intent
import android.graphics.Typeface
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
@ -57,7 +56,7 @@ class CustomizationActivity : AppCompatActivity() {
viewClient = BasicViewClient(terminalView)
sessionCallback = BasicSessionCallback(terminalView)
TerminalUtils.setupTerminalView(terminalView, viewClient)
session = TerminalUtils.createSession(this, "${NeoTermPath.USR_PATH}/bin/applets/echo",
session = TerminalUtils.createShellSession(this, "${NeoTermPath.USR_PATH}/bin/applets/echo",
arrayOf("echo", "Hello NeoTerm."), null, null, null, sessionCallback, false)
terminalView.attachSession(session)

View File

@ -97,6 +97,7 @@ class PackageManagerActivity : AppCompatActivity(), SearchView.OnQueryTextListen
dialog.setTitle(getString(R.string.done))
}
})
.imeEnabled(true)
.show("Installing $packageName")
}
}
@ -185,6 +186,7 @@ class PackageManagerActivity : AppCompatActivity(), SearchView.OnQueryTextListen
}
})
.execute(NeoTermPath.APT_BIN_PATH, arrayOf("apt", "update"))
.imeEnabled(true)
.show("apt update")
}

View File

@ -6,7 +6,6 @@ import android.os.Bundle
import android.support.v4.content.ContextCompat
import android.support.v7.app.AlertDialog
import android.support.v7.app.AppCompatActivity
import android.view.View
import android.widget.Button
import android.widget.Toast
import com.igalata.bubblepicker.BubblePickerListener
@ -22,6 +21,7 @@ import io.neoterm.customize.setup.BaseFileInstaller
import io.neoterm.preference.NeoPreference
import io.neoterm.preference.NeoTermPath
import io.neoterm.utils.PackageUtils
import io.neoterm.utils.TerminalUtils
import io.neoterm.view.TerminalDialog
import java.util.*
@ -38,7 +38,6 @@ class SetupActivity : AppCompatActivity() {
}
lateinit var picker: BubblePicker
lateinit var nextButton: Button
lateinit var toast: Toast
var aptUpdated = false
@ -46,7 +45,7 @@ class SetupActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.ui_setup)
picker = findViewById(R.id.bubble_picker) as BubblePicker
nextButton = findViewById(R.id.setup_next) as Button
val nextButton = findViewById(R.id.setup_next) as Button
nextButton.setOnClickListener {
if (aptUpdated) {
val packageList = mutableListOf("apt", "install", "-y")
@ -101,7 +100,6 @@ class SetupActivity : AppCompatActivity() {
.setMessage(error.toString())
.setNegativeButton(R.string.use_system_shell, { _, _ ->
setResult(Activity.RESULT_CANCELED)
nextButton.visibility = View.VISIBLE
finish()
})
.setPositiveButton(R.string.retry, { dialog, _ ->
@ -114,21 +112,25 @@ class SetupActivity : AppCompatActivity() {
}
private fun executeAptUpdate() {
TerminalDialog(this)
.onFinish(object : TerminalDialog.SessionFinishedCallback {
override fun onSessionFinished(dialog: TerminalDialog, finishedSession: TerminalSession?) {
nextButton.visibility = View.VISIBLE
val exit = finishedSession?.exitStatus ?: 1
if (exit == 0) {
TerminalUtils.executeApt(this, "update", { exitStatus, dialog ->
if (exitStatus == 0) {
dialog.dismiss()
aptUpdated = true
executeAptUpgrade()
} else {
dialog.setTitle(getString(R.string.error))
}
})
}
private fun executeAptUpgrade() {
TerminalUtils.executeApt(this, "upgrade", { exitStatus, dialog ->
if (exitStatus == 0) {
dialog.dismiss()
} else {
dialog.setTitle(getString(R.string.error))
}
})
.execute(NeoTermPath.APT_BIN_PATH, arrayOf("apt", "update"))
.show("apt update")
}
@SuppressLint("ShowToast")

View File

@ -33,10 +33,10 @@ import io.neoterm.ui.bonus.BonusActivity
import io.neoterm.ui.pm.PackageManagerActivity
import io.neoterm.ui.settings.SettingActivity
import io.neoterm.ui.setup.SetupActivity
import io.neoterm.ui.term.tab.TermSessionCallback
import io.neoterm.terminal.client.TermSessionCallback
import io.neoterm.ui.term.tab.TermTab
import io.neoterm.ui.term.tab.TermTabDecorator
import io.neoterm.ui.term.tab.TermViewClient
import io.neoterm.terminal.client.TermViewClient
import io.neoterm.ui.term.tab.event.TabCloseEvent
import io.neoterm.ui.term.tab.event.TitleChangedEvent
import io.neoterm.ui.term.tab.event.ToggleFullScreenEvent
@ -189,8 +189,8 @@ class NeoTermActivity : AppCompatActivity(), ServiceConnection, SharedPreference
}
override fun onSelectionChanged(tabSwitcher: TabSwitcher, selectedTabIndex: Int, selectedTab: Tab?) {
if (selectedTab is TermTab && selectedTab.termSession != null) {
NeoPreference.storeCurrentSession(selectedTab.termSession!!)
if (selectedTab is TermTab && selectedTab.termData.termSession != null) {
NeoPreference.storeCurrentSession(selectedTab.termData.termSession!!)
}
}
@ -199,8 +199,8 @@ class NeoTermActivity : AppCompatActivity(), ServiceConnection, SharedPreference
override fun onTabRemoved(tabSwitcher: TabSwitcher, index: Int, tab: Tab, animation: Animation) {
if (tab is TermTab) {
tab.termSession?.finishIfRunning()
removeFinishedSession(tab.termSession)
tab.termData.termSession?.finishIfRunning()
removeFinishedSession(tab.termData.termSession)
tab.cleanup()
}
}
@ -387,29 +387,32 @@ class NeoTermActivity : AppCompatActivity(), ServiceConnection, SharedPreference
val tabCount = tabSwitcher.count
(0..(tabCount - 1))
.map { tabSwitcher.getTab(it) }
.filter { it is TermTab && it.termSession == session }
.filter { it is TermTab && it.termData.termSession == session }
.forEach { return }
val sessionCallback = session.sessionChangedCallback as TermSessionCallback
val viewClient = TermViewClient(this)
val tab = createTab(session.title) as TermTab
tab.sessionCallback = session.sessionChangedCallback as TermSessionCallback
tab.viewClient = TermViewClient(this)
tab.termSession = session
tab.termData.initializeSessionWith(session, sessionCallback, viewClient)
addNewTab(tab, createRevealAnimation())
switchToSession(tab)
}
private fun addNewSession(sessionName: String?, systemShell: Boolean, animation: Animation) {
val tab = createTab(sessionName) as TermTab
tab.sessionCallback = TermSessionCallback()
tab.viewClient = TermViewClient(this)
tab.termSession = termService!!.createTermSession(null, null,
null, null, null, tab.sessionCallback, systemShell)
val sessionCallback = TermSessionCallback()
val viewClient = TermViewClient(this)
val session = termService!!.createTermSession(null, null,
null, null, null, sessionCallback, systemShell)
if (sessionName != null) {
tab.termSession!!.mSessionName = sessionName
session.mSessionName = sessionName
}
val tab = createTab(sessionName) as TermTab
tab.termData.initializeSessionWith(session, sessionCallback, viewClient)
addNewTab(tab, animation)
switchToSession(tab)
}
@ -421,7 +424,7 @@ class NeoTermActivity : AppCompatActivity(), ServiceConnection, SharedPreference
for (i in 0..tabSwitcher.count - 1) {
val tab = tabSwitcher.getTab(i)
if (tab is TermTab && tab.termSession == session) {
if (tab is TermTab && tab.termData.termSession == session) {
switchToSession(tab)
break
}

View File

@ -16,7 +16,7 @@ import io.neoterm.customize.script.UserScript
import io.neoterm.customize.script.UserScriptManager
import io.neoterm.preference.NeoPreference
import io.neoterm.services.NeoTermService
import io.neoterm.ui.term.tab.TermSessionCallback
import io.neoterm.terminal.client.TermSessionCallback
import io.neoterm.utils.TerminalUtils
import java.io.File

View File

@ -5,58 +5,31 @@ import android.support.v7.widget.Toolbar
import android.view.inputmethod.InputMethodManager
import de.mrapp.android.tabswitcher.Tab
import io.neoterm.R
import io.neoterm.backend.TerminalSession
import io.neoterm.customize.color.ColorSchemeManager
import io.neoterm.preference.NeoPreference
import io.neoterm.terminal.client.TermDataHolder
import io.neoterm.terminal.client.TermUiPresenter
import io.neoterm.ui.term.tab.event.TabCloseEvent
import io.neoterm.ui.term.tab.event.TitleChangedEvent
import io.neoterm.ui.term.tab.event.ToggleFullScreenEvent
import io.neoterm.view.OnAutoCompleteListener
import org.greenrobot.eventbus.EventBus
/**
* @author kiva
*/
class TermTab(title: CharSequence) : Tab(title) {
var termSession: TerminalSession? = null
var sessionCallback: TermSessionCallback? = null
var viewClient: TermViewClient? = null
var onAutoCompleteListener: OnAutoCompleteListener? = null
class TermTab(title: CharSequence) : Tab(title), TermUiPresenter {
var termData = TermDataHolder()
var toolbar: Toolbar? = null
fun updateColorScheme() {
ColorSchemeManager.applyColorScheme(viewClient?.termView, viewClient?.extraKeysView,
ColorSchemeManager.applyColorScheme(termData.termView, termData.extraKeysView,
ColorSchemeManager.getCurrentColorScheme())
}
fun cleanup() {
onAutoCompleteListener?.onCleanUp()
onAutoCompleteListener = null
viewClient?.termTab = null
viewClient?.termView = null
viewClient?.extraKeysView = null
sessionCallback?.termView = null
sessionCallback?.termTab = null
termData.cleanup()
toolbar = null
termSession = null
}
fun updateTitle(title: String) {
if (title.isNotEmpty()) {
this.title = title
EventBus.getDefault().post(TitleChangedEvent(title))
if (NeoPreference.loadBoolean(R.string.key_ui_suggestions, true)) {
viewClient?.updateSuggestions(title)
} else {
viewClient?.removeSuggestions()
}
}
}
fun onSessionFinished() {
viewClient?.sessionFinished = true
}
fun onFullScreenModeChanged(fullScreen: Boolean) {
@ -64,13 +37,8 @@ class TermTab(title: CharSequence) : Tab(title) {
resetAutoCompleteStatus()
}
fun requireCloseTab() {
requireHideIme()
EventBus.getDefault().post(TabCloseEvent(this))
}
fun requireHideIme() {
val terminalView = viewClient?.termView
val terminalView = termData.termView
if (terminalView != null) {
val imm = terminalView.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
if (imm.isActive) {
@ -79,16 +47,37 @@ class TermTab(title: CharSequence) : Tab(title) {
}
}
fun requireToggleFullScreen() {
override fun requireToggleFullScreen() {
EventBus.getDefault().post(ToggleFullScreenEvent())
}
fun requirePaste() {
viewClient?.termView?.pasteFromClipboard()
override fun requirePaste() {
termData.termView?.pasteFromClipboard()
}
override fun requireClose() {
requireHideIme()
EventBus.getDefault().post(TabCloseEvent(this))
}
override fun requireUpdateTitle(title: String?) {
if (title != null && title.isNotEmpty()) {
this.title = title
EventBus.getDefault().post(TitleChangedEvent(title))
if (NeoPreference.loadBoolean(R.string.key_ui_suggestions, true)) {
termData.viewClient?.updateSuggestions(title)
} else {
termData.viewClient?.removeSuggestions()
}
}
}
override fun requireOnSessionFinished() {
termData.viewClient?.sessionFinished = true
}
fun resetAutoCompleteStatus() {
onAutoCompleteListener?.onCleanUp()
onAutoCompleteListener = null
termData.onAutoCompleteListener?.onCleanUp()
termData.onAutoCompleteListener = null
}
}

View File

@ -12,6 +12,7 @@ import io.neoterm.BuildConfig
import io.neoterm.R
import io.neoterm.customize.color.ColorSchemeManager
import io.neoterm.preference.NeoPreference
import io.neoterm.terminal.client.TermCompleteListener
import io.neoterm.ui.term.NeoTermActivity
import io.neoterm.utils.TerminalUtils
import io.neoterm.view.ExtraKeysView
@ -58,32 +59,27 @@ class TermTabDecorator(val context: NeoTermActivity) : TabSwitcherDecorator() {
if (tab is TermTab) {
val termTab = tab
val termData = tab.termData
TerminalUtils.setupTerminalSession(termTab.termSession)
TerminalUtils.setupTerminalSession(termData.termSession)
// 复用前一次的 TermSessionCallback
termTab.sessionCallback?.termView = view
termTab.sessionCallback?.termTab = termTab
// 复用前一次的 TermSessionCallback 和 TermViewClient
termData.initializeViewWith(termTab, view, extraKeysView)
// 复用上一次的 TermViewClient
termTab.viewClient?.termTab = termTab
termTab.viewClient?.termView = view
termTab.viewClient?.extraKeysView = extraKeysView
if (termTab.termSession != null) {
termTab.viewClient?.updateSuggestions(termTab.termSession?.title, true)
if (termData.termSession != null) {
termData.viewClient?.updateSuggestions(termData.termSession?.title, true)
}
view.setTerminalViewClient(termTab.viewClient)
view.attachSession(termTab.termSession)
view.setTerminalViewClient(termData.viewClient)
view.attachSession(termData.termSession)
// Still in progress
// Only available for developers.
if (BuildConfig.DEBUG) {
if (termTab.onAutoCompleteListener == null) {
termTab.onAutoCompleteListener = createAutoCompleteListener(view)
if (termData.onAutoCompleteListener == null) {
termData.onAutoCompleteListener = createAutoCompleteListener(view)
}
view.onAutoCompleteListener = termTab.onAutoCompleteListener
view.onAutoCompleteListener = termData.onAutoCompleteListener
}
}
}

View File

@ -1,22 +1,22 @@
package io.neoterm.utils
import android.content.Context
import android.widget.Toast
import io.neoterm.R
import io.neoterm.backend.TerminalSession
import io.neoterm.customize.font.FontManager
import io.neoterm.preference.NeoPreference
import io.neoterm.preference.NeoTermPath
import io.neoterm.view.BasicViewClient
import io.neoterm.terminal.ShellTermSession
import io.neoterm.view.ExtraKeysView
import io.neoterm.view.TerminalDialog
import io.neoterm.view.TerminalView
import java.io.File
import io.neoterm.view.TerminalViewClient
/**
* @author kiva
*/
object TerminalUtils {
fun setupTerminalView(terminalView: TerminalView?, terminalViewClient: BasicViewClient? = null) {
fun setupTerminalView(terminalView: TerminalView?, terminalViewClient: TerminalViewClient? = null) {
terminalView?.textSize = NeoPreference.loadInt(NeoPreference.KEY_FONT_SIZE, 30)
terminalView?.setTypeface(FontManager.getCurrentFont().getTypeFace())
if (terminalViewClient != null) {
@ -31,44 +31,23 @@ object TerminalUtils {
fun setupTerminalSession(session: TerminalSession?) {
}
fun createSession(context: Context, executablePath: String?, arguments: Array<String>?,
cwd: String?, initialCommand: String?, env: Array<String>?,
fun createShellSession(context: Context, executablePath: String?, arguments: Array<String>?,
cwd: String?, initialCommand: String?, env: Array<Pair<String, String>>?,
sessionCallback: TerminalSession.SessionChangedCallback?,
systemShell: Boolean): TerminalSession {
val initCommand = initialCommand ?:
NeoPreference.loadString(R.string.key_general_initial_command, "")
var executablePath = executablePath
var arguments = arguments
var initialCommand = initialCommand
var cwd = cwd
if (cwd == null) {
cwd = NeoTermPath.HOME_PATH
}
if (executablePath == null) {
executablePath = if (systemShell)
"/system/bin/sh"
else
NeoTermPath.USR_PATH + "/bin/" + NeoPreference.loadString(R.string.key_general_shell, "sh")
if (!File(executablePath).exists()) {
Toast.makeText(context, context.getString(R.string.shell_not_found, executablePath), Toast.LENGTH_LONG).show()
executablePath = NeoTermPath.USR_PATH + "/bin/sh"
}
}
if (arguments == null) {
arguments = arrayOf<String>(executablePath)
}
if (initialCommand == null) {
initialCommand = NeoPreference.loadString(R.string.key_general_initial_command, "")
}
val session = TerminalSession(executablePath, cwd, initialCommand, arguments,
env ?: NeoPreference.buildEnvironment(cwd, systemShell, executablePath),
sessionCallback)
val session = ShellTermSession.Builder()
.shell(executablePath)
.currentWorkingDirectory(cwd)
.callback(sessionCallback)
.systemShell(systemShell)
.envArray(env)
.argArray(arguments)
.create(context)
setupTerminalSession(session)
session.initialCommand = initCommand
return session
}
@ -91,4 +70,17 @@ object TerminalUtils {
builder.append('"')
return builder.toString()
}
fun executeApt(context: Context, subCommand: String, callback: (Int, TerminalDialog) -> Unit) {
TerminalDialog(context)
.onFinish(object : TerminalDialog.SessionFinishedCallback {
override fun onSessionFinished(dialog: TerminalDialog, finishedSession: TerminalSession?) {
val exit = finishedSession?.exitStatus ?: 1
callback(exit, dialog)
}
})
.imeEnabled(true)
.execute(NeoTermPath.APT_BIN_PATH, arrayOf("apt", subCommand))
.show("apt $subCommand")
}
}

View File

@ -22,7 +22,6 @@ class TerminalDialog(val context: Context) {
@SuppressLint("InflateParams")
private var view: View = LayoutInflater.from(context).inflate(R.layout.ui_term_dialog, null, false)
private var terminalView: TerminalView
private var terminalViewClient: BasicViewClient
private var terminalSessionCallback: BasicSessionCallback
private var dialog: AlertDialog? = null
private var terminalSession: TerminalSession? = null
@ -31,10 +30,8 @@ class TerminalDialog(val context: Context) {
init {
terminalView = view.findViewById(R.id.terminal_view_dialog) as TerminalView
terminalViewClient = BasicViewClient(terminalView)
TerminalUtils.setupTerminalView(terminalView, terminalViewClient)
TerminalUtils.setupTerminalView(terminalView, BasicViewClient(terminalView))
terminalView.setTerminalViewClient(terminalViewClient)
terminalSessionCallback = object : BasicSessionCallback(terminalView) {
override fun onSessionFinished(finishedSession: TerminalSession?) {
sessionFinishedCallback?.onSessionFinished(this@TerminalDialog, finishedSession)
@ -56,7 +53,7 @@ class TerminalDialog(val context: Context) {
}
.create()
terminalSession = TerminalUtils.createSession(context, executablePath, arguments, null, null, null, terminalSessionCallback, false)
terminalSession = TerminalUtils.createShellSession(context, executablePath, arguments, null, null, null, terminalSessionCallback, false)
terminalView.attachSession(terminalSession)
return this
}
@ -85,4 +82,12 @@ class TerminalDialog(val context: Context) {
dialog?.dismiss()
return this
}
fun imeEnabled(enabled: Boolean): TerminalDialog {
if (enabled) {
terminalView.isFocusable = true
terminalView.isFocusableInTouchMode = true
}
return this
}
}

View File

@ -286,7 +286,6 @@ public final class TerminalView extends View {
@Override
public boolean commitText(CharSequence text, int newCursorPosition) {
// TODO: AutoComplete
if (LOG_KEY_EVENTS) {
Log.i(EmulatorDebug.LOG_TAG, "IME: commitText(\"" + text + "\", " + newCursorPosition + ")");
}

View File

@ -7,7 +7,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.0-alpha5'
classpath 'com.android.tools.build:gradle:3.0.0-alpha7'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong