Skip to content

Commit

Permalink
feat: Add basic terminal
Browse files Browse the repository at this point in the history
Tested-by: Pranav Purwar <[email protected]>
Signed-off-by: PranavPurwar <[email protected]>
Signed-off-by: Pranav Purwar <[email protected]>
  • Loading branch information
PranavPurwar committed Jun 7, 2024
1 parent f48c78d commit fa75717
Show file tree
Hide file tree
Showing 7 changed files with 321 additions and 4 deletions.
4 changes: 4 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
10 changes: 9 additions & 1 deletion app/src/main/kotlin/org/cosmicide/fragment/EditorFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,15 @@ class EditorFragment : BaseBindingFragment<FragmentEditorBinding>() {
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")
Expand All @@ -394,7 +403,6 @@ class EditorFragment : BaseBindingFragment<FragmentEditorBinding>() {
)
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<String>()
var arg = ""
var inSingleQuote = false
Expand Down
19 changes: 16 additions & 3 deletions app/src/main/kotlin/org/cosmicide/fragment/ProjectFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -133,6 +133,11 @@ class ProjectFragment : BaseBindingFragment<FragmentProjectBinding>(),
}
true
}

R.id.action_terminal -> {
navigateToTerminalFragment()
true
}
else -> false
}
}
Expand Down Expand Up @@ -399,6 +404,14 @@ class ProjectFragment : BaseBindingFragment<FragmentProjectBinding>(),
}
}

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 {
Expand Down
284 changes: 284 additions & 0 deletions app/src/main/kotlin/org/cosmicide/fragment/TerminalFragment.kt
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*/

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<FragmentTerminalBinding>() {
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<String>(),
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() {}
}
}
3 changes: 3 additions & 0 deletions app/src/main/res/menu/menu_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@
android:id="@+id/action_git"
android:title="@string/git" />

<item
android:id="@+id/action_terminal"
android:title="@string/terminal" />

<item android:title="@string/advanced">

Expand Down
4 changes: 4 additions & 0 deletions app/src/main/res/menu/projects_menu.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">

<item
android:id="@+id/action_terminal"
android:title="@string/terminal" />

<item
android:id="@+id/action_settings"
android:title="@string/action_settings"
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,5 @@
<string name="program_arguments">Program Arguments</string>
<string name="advanced">Advanced</string>
<string name="gemini_pro">Gemini Pro</string>
<string name="terminal">Terminal</string>
</resources>

0 comments on commit fa75717

Please sign in to comment.