From fa75717c91664b056ccad45eafbf0a163e9ee8ca Mon Sep 17 00:00:00 2001 From: Pranav Purwar Date: Fri, 7 Jun 2024 22:44:24 +0530 Subject: [PATCH] feat: Add basic terminal Tested-by: Pranav Purwar Signed-off-by: PranavPurwar Signed-off-by: Pranav Purwar --- app/build.gradle.kts | 4 + .../org/cosmicide/fragment/EditorFragment.kt | 10 +- .../org/cosmicide/fragment/ProjectFragment.kt | 19 +- .../cosmicide/fragment/TerminalFragment.kt | 284 ++++++++++++++++++ app/src/main/res/menu/menu_main.xml | 3 + app/src/main/res/menu/projects_menu.xml | 4 + app/src/main/res/values/strings.xml | 1 + 7 files changed, 321 insertions(+), 4 deletions(-) create mode 100644 app/src/main/kotlin/org/cosmicide/fragment/TerminalFragment.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 785a58d18..cfb983ac8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -177,6 +177,10 @@ configurations.all { } dependencies { + implementation("com.github.termux.termux-app:terminal-view:062c9771a9") + implementation("com.github.termux.termux-app:terminal-emulator:062c9771a9") + implementation("com.blankj:utilcodex:1.31.1") + implementation("com.android.tools:r8:8.3.37") implementation("com.android.tools.smali:smali-dexlib2:3.0.7") diff --git a/app/src/main/kotlin/org/cosmicide/fragment/EditorFragment.kt b/app/src/main/kotlin/org/cosmicide/fragment/EditorFragment.kt index 504158558..8164ef78a 100644 --- a/app/src/main/kotlin/org/cosmicide/fragment/EditorFragment.kt +++ b/app/src/main/kotlin/org/cosmicide/fragment/EditorFragment.kt @@ -382,6 +382,15 @@ class EditorFragment : BaseBindingFragment() { true } + R.id.action_terminal -> { + parentFragmentManager.commit { + add(R.id.fragment_container, TerminalFragment()) + addToBackStack(null) + setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN) + } + true + } + R.id.arguments -> { val binding = TextDialogBinding.inflate(layoutInflater) MaterialAlertDialogBuilder(context).setTitle("Enter program arguments") @@ -394,7 +403,6 @@ class EditorFragment : BaseBindingFragment() { ) val args = binding.textInputLayout.editText?.text.toString() - // split args into a list considering both single and double quotes and ending with a space val argList = mutableListOf() var arg = "" var inSingleQuote = false diff --git a/app/src/main/kotlin/org/cosmicide/fragment/ProjectFragment.kt b/app/src/main/kotlin/org/cosmicide/fragment/ProjectFragment.kt index fd47cd66a..66415d92e 100644 --- a/app/src/main/kotlin/org/cosmicide/fragment/ProjectFragment.kt +++ b/app/src/main/kotlin/org/cosmicide/fragment/ProjectFragment.kt @@ -36,12 +36,12 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.cosmicide.R import org.cosmicide.adapter.ProjectAdapter -import org.cosmicide.databinding.FragmentProjectBinding -import org.cosmicide.model.ProjectViewModel -import org.cosmicide.project.Project import org.cosmicide.common.Analytics import org.cosmicide.common.BaseBindingFragment import org.cosmicide.common.Prefs +import org.cosmicide.databinding.FragmentProjectBinding +import org.cosmicide.model.ProjectViewModel +import org.cosmicide.project.Project import org.cosmicide.rewrite.util.FileUtil import org.cosmicide.rewrite.util.compressToZip import org.cosmicide.rewrite.util.unzip @@ -133,6 +133,11 @@ class ProjectFragment : BaseBindingFragment(), } true } + + R.id.action_terminal -> { + navigateToTerminalFragment() + true + } else -> false } } @@ -399,6 +404,14 @@ class ProjectFragment : BaseBindingFragment(), } } + private fun navigateToTerminalFragment() { + parentFragmentManager.commit { + add(R.id.fragment_container, TerminalFragment()) + addToBackStack(null) + setTransition(androidx.fragment.app.FragmentTransaction.TRANSIT_FRAGMENT_OPEN) + } + } + private fun navigateToEditorFragment(project: Project) { parentFragmentManager.commit { add(R.id.fragment_container, EditorFragment().apply { diff --git a/app/src/main/kotlin/org/cosmicide/fragment/TerminalFragment.kt b/app/src/main/kotlin/org/cosmicide/fragment/TerminalFragment.kt new file mode 100644 index 000000000..f5cfcec86 --- /dev/null +++ b/app/src/main/kotlin/org/cosmicide/fragment/TerminalFragment.kt @@ -0,0 +1,284 @@ +/* + * This file is part of Cosmic IDE. + * Cosmic IDE is a free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * Cosmic IDE is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * You should have received a copy of the GNU General Public License along with Cosmic IDE. If not, see . + */ + +package org.cosmicide.fragment + +import android.os.Bundle +import android.os.Environment +import android.util.Log +import android.view.KeyEvent +import android.view.MotionEvent +import android.view.View +import androidx.core.content.res.ResourcesCompat +import androidx.lifecycle.lifecycleScope +import com.blankj.utilcode.util.ClipboardUtils +import com.blankj.utilcode.util.KeyboardUtils +import com.termux.terminal.TerminalEmulator +import com.termux.terminal.TerminalSession +import com.termux.terminal.TerminalSessionClient +import com.termux.view.TerminalRenderer +import com.termux.view.TerminalView +import com.termux.view.TerminalViewClient +import kotlinx.coroutines.launch +import org.cosmicide.R +import org.cosmicide.common.BaseBindingFragment +import org.cosmicide.databinding.FragmentTerminalBinding +import org.cosmicide.project.Project +import org.cosmicide.util.ProjectHandler +import java.io.File + +/** + * A fragment for displaying information about the compilation process. + */ +class TerminalFragment : BaseBindingFragment() { + val project: Project? = ProjectHandler.getProject() + + override fun getViewBinding() = FragmentTerminalBinding.inflate(layoutInflater) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.terminalView.attachSession(getTerminalSession()) + binding.terminalView.setTerminalViewClient(TerminalClient(binding.terminalView)) + binding.terminalView.mRenderer = + TerminalRenderer(28, ResourcesCompat.getFont(requireContext(), R.font.noto_sans_mono)!!) + binding.terminalView.requestFocus() + KeyboardUtils.showSoftInput(binding.terminalView) + } + + private fun getTerminalSession(): TerminalSession { + val cwd = project?.root?.absolutePath + ?: if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) { + Environment.getExternalStorageDirectory().absolutePath + } else { + requireContext().filesDir.absolutePath + } + var shell = "/bin/sh" + + if (File(shell).exists().not()) { + shell = "/system/bin/sh" + } + + return TerminalSession( + shell, + cwd, + arrayOf(), + arrayOf(), + TerminalEmulator.DEFAULT_TERMINAL_TRANSCRIPT_ROWS, + getTermSessionClient() + ) + } + + private fun getTermSessionClient(): TerminalSessionClient { + return object : TerminalSessionClient { + override fun onTextChanged(changedSession: TerminalSession) { + lifecycleScope.launch { + binding.terminalView.onScreenUpdated() + } + } + + override fun onTitleChanged(updatedSession: TerminalSession) {} + + override fun onSessionFinished(finishedSession: TerminalSession) { + lifecycleScope.launch { + binding.terminalView.let { + KeyboardUtils.hideSoftInput(it) + it.mTermSession?.finishIfRunning() + } + requireActivity().supportFragmentManager.popBackStack() + } + } + + override fun onCopyTextToClipboard(session: TerminalSession, text: String?) { + ClipboardUtils.copyText(text) + } + + override fun onPasteTextFromClipboard(session: TerminalSession?) { + lifecycleScope.launch { + val clip = ClipboardUtils.getText().toString() + if (clip.trim { it <= ' ' } + .isNotEmpty() && binding.terminalView.mEmulator != null) { + binding.terminalView.mEmulator.paste(clip) + } + } + } + + override fun onBell(session: TerminalSession) {} + + override fun onColorsChanged(changedSession: TerminalSession) {} + + override fun onTerminalCursorStateChange(state: Boolean) {} + override fun setTerminalShellPid(session: TerminalSession, pid: Int) { + Log.d("TerminalFragment", "setTerminalShellPid: $pid") + } + + override fun getTerminalCursorStyle(): Int { + return TerminalEmulator.TERMINAL_CURSOR_STYLE_UNDERLINE + } + + override fun logError(tag: String?, message: String?) { + if (message != null) { + Log.e(tag, message) + } + } + + override fun logWarn(tag: String?, message: String?) { + if (message != null) { + Log.w(tag, message) + } + } + + override fun logInfo(tag: String?, message: String?) { + if (message != null) { + Log.i(tag, message) + } + } + + override fun logDebug(tag: String?, message: String?) { + if (message != null) { + Log.d(tag, message) + } + } + + override fun logVerbose(tag: String?, message: String?) { + if (message != null) { + Log.v(tag, message) + } + } + + override fun logStackTraceWithMessage( + tag: String?, + message: String?, + e: Exception? + ) { + Log.e(tag, message + "\n" + Log.getStackTraceString(e)) + } + + override fun logStackTrace(tag: String?, e: Exception?) { + Log.e(tag, Log.getStackTraceString(e)) + } + + } + } + + class TerminalClient(val terminal: TerminalView) : TerminalViewClient { + override fun logError(tag: String?, message: String?) { + if (message != null) { + Log.e(tag, message) + } + } + + override fun logWarn(tag: String?, message: String?) { + if (message != null) { + Log.w(tag, message) + } + } + + override fun logInfo(tag: String?, message: String?) { + if (message != null) { + Log.i(tag, message) + } + } + + override fun logDebug(tag: String?, message: String?) { + if (message != null) { + Log.d(tag, message) + } + } + + override fun logVerbose(tag: String?, message: String?) { + if (message != null) { + Log.v(tag, message) + } + } + + override fun logStackTraceWithMessage( + tag: String?, + message: String?, + e: Exception? + ) { + Log.e(tag, message + "\n" + Log.getStackTraceString(e)) + } + + override fun logStackTrace(tag: String?, e: Exception?) { + Log.e(tag, Log.getStackTraceString(e)) + } + + override fun onScale(scale: Float): Float { + return scale + } + + override fun onSingleTapUp(e: MotionEvent?) { + if (terminal.mTermSession.isRunning) { + terminal.requestFocus() + KeyboardUtils.showSoftInput(terminal) + } + } + + override fun shouldBackButtonBeMappedToEscape(): Boolean { + return false + } + + override fun shouldEnforceCharBasedInput(): Boolean { + return true + } + + override fun shouldUseCtrlSpaceWorkaround(): Boolean { + return false + } + + override fun isTerminalViewSelected(): Boolean { + return true + } + + override fun copyModeChanged(copyMode: Boolean) {} + + override fun onKeyDown(keyCode: Int, e: KeyEvent?, session: TerminalSession?): Boolean { + return false + } + + override fun onKeyUp(keyCode: Int, e: KeyEvent?): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (terminal.mTermSession.isRunning) { + terminal.mTermSession.finishIfRunning() + } + return true + } + return false + } + + override fun onLongPress(event: MotionEvent?): Boolean { + return false + } + + override fun readControlKey(): Boolean { + return false + } + + override fun readAltKey(): Boolean { + return false + } + + override fun readShiftKey(): Boolean { + return false + } + + override fun readFnKey(): Boolean { + return false + } + + override fun onCodePoint( + codePoint: Int, + ctrlDown: Boolean, + session: TerminalSession? + ): Boolean { + return false + } + + override fun onEmulatorSet() {} + } +} diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml index 691ed9ba3..d25a0a9d2 100644 --- a/app/src/main/res/menu/menu_main.xml +++ b/app/src/main/res/menu/menu_main.xml @@ -57,6 +57,9 @@ android:id="@+id/action_git" android:title="@string/git" /> + diff --git a/app/src/main/res/menu/projects_menu.xml b/app/src/main/res/menu/projects_menu.xml index bbb710443..72c9112af 100644 --- a/app/src/main/res/menu/projects_menu.xml +++ b/app/src/main/res/menu/projects_menu.xml @@ -8,6 +8,10 @@ + + Program Arguments Advanced Gemini Pro + Terminal