Skip to content

Commit fa75717

Browse files
committed
feat: Add basic terminal
Tested-by: Pranav Purwar <purwarpranav80@gmail.com> Signed-off-by: PranavPurwar <purwarpranav80@gmail.com> Signed-off-by: Pranav Purwar <purwarpranav80@gmail.com>
1 parent f48c78d commit fa75717

File tree

7 files changed

+321
-4
lines changed

7 files changed

+321
-4
lines changed

app/build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,10 @@ configurations.all {
177177
}
178178

179179
dependencies {
180+
implementation("com.github.termux.termux-app:terminal-view:062c9771a9")
181+
implementation("com.github.termux.termux-app:terminal-emulator:062c9771a9")
182+
implementation("com.blankj:utilcodex:1.31.1")
183+
180184
implementation("com.android.tools:r8:8.3.37")
181185
implementation("com.android.tools.smali:smali-dexlib2:3.0.7")
182186

app/src/main/kotlin/org/cosmicide/fragment/EditorFragment.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,15 @@ class EditorFragment : BaseBindingFragment<FragmentEditorBinding>() {
382382
true
383383
}
384384

385+
R.id.action_terminal -> {
386+
parentFragmentManager.commit {
387+
add(R.id.fragment_container, TerminalFragment())
388+
addToBackStack(null)
389+
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
390+
}
391+
true
392+
}
393+
385394
R.id.arguments -> {
386395
val binding = TextDialogBinding.inflate(layoutInflater)
387396
MaterialAlertDialogBuilder(context).setTitle("Enter program arguments")
@@ -394,7 +403,6 @@ class EditorFragment : BaseBindingFragment<FragmentEditorBinding>() {
394403
)
395404
val args = binding.textInputLayout.editText?.text.toString()
396405

397-
// split args into a list considering both single and double quotes and ending with a space
398406
val argList = mutableListOf<String>()
399407
var arg = ""
400408
var inSingleQuote = false

app/src/main/kotlin/org/cosmicide/fragment/ProjectFragment.kt

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@ import kotlinx.coroutines.launch
3636
import kotlinx.coroutines.withContext
3737
import org.cosmicide.R
3838
import org.cosmicide.adapter.ProjectAdapter
39-
import org.cosmicide.databinding.FragmentProjectBinding
40-
import org.cosmicide.model.ProjectViewModel
41-
import org.cosmicide.project.Project
4239
import org.cosmicide.common.Analytics
4340
import org.cosmicide.common.BaseBindingFragment
4441
import org.cosmicide.common.Prefs
42+
import org.cosmicide.databinding.FragmentProjectBinding
43+
import org.cosmicide.model.ProjectViewModel
44+
import org.cosmicide.project.Project
4545
import org.cosmicide.rewrite.util.FileUtil
4646
import org.cosmicide.rewrite.util.compressToZip
4747
import org.cosmicide.rewrite.util.unzip
@@ -133,6 +133,11 @@ class ProjectFragment : BaseBindingFragment<FragmentProjectBinding>(),
133133
}
134134
true
135135
}
136+
137+
R.id.action_terminal -> {
138+
navigateToTerminalFragment()
139+
true
140+
}
136141
else -> false
137142
}
138143
}
@@ -399,6 +404,14 @@ class ProjectFragment : BaseBindingFragment<FragmentProjectBinding>(),
399404
}
400405
}
401406

407+
private fun navigateToTerminalFragment() {
408+
parentFragmentManager.commit {
409+
add(R.id.fragment_container, TerminalFragment())
410+
addToBackStack(null)
411+
setTransition(androidx.fragment.app.FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
412+
}
413+
}
414+
402415
private fun navigateToEditorFragment(project: Project) {
403416
parentFragmentManager.commit {
404417
add(R.id.fragment_container, EditorFragment().apply {
Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
/*
2+
* This file is part of Cosmic IDE.
3+
* 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.
4+
* 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.
5+
* You should have received a copy of the GNU General Public License along with Cosmic IDE. If not, see <https://www.gnu.org/licenses/>.
6+
*/
7+
8+
package org.cosmicide.fragment
9+
10+
import android.os.Bundle
11+
import android.os.Environment
12+
import android.util.Log
13+
import android.view.KeyEvent
14+
import android.view.MotionEvent
15+
import android.view.View
16+
import androidx.core.content.res.ResourcesCompat
17+
import androidx.lifecycle.lifecycleScope
18+
import com.blankj.utilcode.util.ClipboardUtils
19+
import com.blankj.utilcode.util.KeyboardUtils
20+
import com.termux.terminal.TerminalEmulator
21+
import com.termux.terminal.TerminalSession
22+
import com.termux.terminal.TerminalSessionClient
23+
import com.termux.view.TerminalRenderer
24+
import com.termux.view.TerminalView
25+
import com.termux.view.TerminalViewClient
26+
import kotlinx.coroutines.launch
27+
import org.cosmicide.R
28+
import org.cosmicide.common.BaseBindingFragment
29+
import org.cosmicide.databinding.FragmentTerminalBinding
30+
import org.cosmicide.project.Project
31+
import org.cosmicide.util.ProjectHandler
32+
import java.io.File
33+
34+
/**
35+
* A fragment for displaying information about the compilation process.
36+
*/
37+
class TerminalFragment : BaseBindingFragment<FragmentTerminalBinding>() {
38+
val project: Project? = ProjectHandler.getProject()
39+
40+
override fun getViewBinding() = FragmentTerminalBinding.inflate(layoutInflater)
41+
42+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
43+
super.onViewCreated(view, savedInstanceState)
44+
45+
binding.terminalView.attachSession(getTerminalSession())
46+
binding.terminalView.setTerminalViewClient(TerminalClient(binding.terminalView))
47+
binding.terminalView.mRenderer =
48+
TerminalRenderer(28, ResourcesCompat.getFont(requireContext(), R.font.noto_sans_mono)!!)
49+
binding.terminalView.requestFocus()
50+
KeyboardUtils.showSoftInput(binding.terminalView)
51+
}
52+
53+
private fun getTerminalSession(): TerminalSession {
54+
val cwd = project?.root?.absolutePath
55+
?: if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) {
56+
Environment.getExternalStorageDirectory().absolutePath
57+
} else {
58+
requireContext().filesDir.absolutePath
59+
}
60+
var shell = "/bin/sh"
61+
62+
if (File(shell).exists().not()) {
63+
shell = "/system/bin/sh"
64+
}
65+
66+
return TerminalSession(
67+
shell,
68+
cwd,
69+
arrayOf<String>(),
70+
arrayOf(),
71+
TerminalEmulator.DEFAULT_TERMINAL_TRANSCRIPT_ROWS,
72+
getTermSessionClient()
73+
)
74+
}
75+
76+
private fun getTermSessionClient(): TerminalSessionClient {
77+
return object : TerminalSessionClient {
78+
override fun onTextChanged(changedSession: TerminalSession) {
79+
lifecycleScope.launch {
80+
binding.terminalView.onScreenUpdated()
81+
}
82+
}
83+
84+
override fun onTitleChanged(updatedSession: TerminalSession) {}
85+
86+
override fun onSessionFinished(finishedSession: TerminalSession) {
87+
lifecycleScope.launch {
88+
binding.terminalView.let {
89+
KeyboardUtils.hideSoftInput(it)
90+
it.mTermSession?.finishIfRunning()
91+
}
92+
requireActivity().supportFragmentManager.popBackStack()
93+
}
94+
}
95+
96+
override fun onCopyTextToClipboard(session: TerminalSession, text: String?) {
97+
ClipboardUtils.copyText(text)
98+
}
99+
100+
override fun onPasteTextFromClipboard(session: TerminalSession?) {
101+
lifecycleScope.launch {
102+
val clip = ClipboardUtils.getText().toString()
103+
if (clip.trim { it <= ' ' }
104+
.isNotEmpty() && binding.terminalView.mEmulator != null) {
105+
binding.terminalView.mEmulator.paste(clip)
106+
}
107+
}
108+
}
109+
110+
override fun onBell(session: TerminalSession) {}
111+
112+
override fun onColorsChanged(changedSession: TerminalSession) {}
113+
114+
override fun onTerminalCursorStateChange(state: Boolean) {}
115+
override fun setTerminalShellPid(session: TerminalSession, pid: Int) {
116+
Log.d("TerminalFragment", "setTerminalShellPid: $pid")
117+
}
118+
119+
override fun getTerminalCursorStyle(): Int {
120+
return TerminalEmulator.TERMINAL_CURSOR_STYLE_UNDERLINE
121+
}
122+
123+
override fun logError(tag: String?, message: String?) {
124+
if (message != null) {
125+
Log.e(tag, message)
126+
}
127+
}
128+
129+
override fun logWarn(tag: String?, message: String?) {
130+
if (message != null) {
131+
Log.w(tag, message)
132+
}
133+
}
134+
135+
override fun logInfo(tag: String?, message: String?) {
136+
if (message != null) {
137+
Log.i(tag, message)
138+
}
139+
}
140+
141+
override fun logDebug(tag: String?, message: String?) {
142+
if (message != null) {
143+
Log.d(tag, message)
144+
}
145+
}
146+
147+
override fun logVerbose(tag: String?, message: String?) {
148+
if (message != null) {
149+
Log.v(tag, message)
150+
}
151+
}
152+
153+
override fun logStackTraceWithMessage(
154+
tag: String?,
155+
message: String?,
156+
e: Exception?
157+
) {
158+
Log.e(tag, message + "\n" + Log.getStackTraceString(e))
159+
}
160+
161+
override fun logStackTrace(tag: String?, e: Exception?) {
162+
Log.e(tag, Log.getStackTraceString(e))
163+
}
164+
165+
}
166+
}
167+
168+
class TerminalClient(val terminal: TerminalView) : TerminalViewClient {
169+
override fun logError(tag: String?, message: String?) {
170+
if (message != null) {
171+
Log.e(tag, message)
172+
}
173+
}
174+
175+
override fun logWarn(tag: String?, message: String?) {
176+
if (message != null) {
177+
Log.w(tag, message)
178+
}
179+
}
180+
181+
override fun logInfo(tag: String?, message: String?) {
182+
if (message != null) {
183+
Log.i(tag, message)
184+
}
185+
}
186+
187+
override fun logDebug(tag: String?, message: String?) {
188+
if (message != null) {
189+
Log.d(tag, message)
190+
}
191+
}
192+
193+
override fun logVerbose(tag: String?, message: String?) {
194+
if (message != null) {
195+
Log.v(tag, message)
196+
}
197+
}
198+
199+
override fun logStackTraceWithMessage(
200+
tag: String?,
201+
message: String?,
202+
e: Exception?
203+
) {
204+
Log.e(tag, message + "\n" + Log.getStackTraceString(e))
205+
}
206+
207+
override fun logStackTrace(tag: String?, e: Exception?) {
208+
Log.e(tag, Log.getStackTraceString(e))
209+
}
210+
211+
override fun onScale(scale: Float): Float {
212+
return scale
213+
}
214+
215+
override fun onSingleTapUp(e: MotionEvent?) {
216+
if (terminal.mTermSession.isRunning) {
217+
terminal.requestFocus()
218+
KeyboardUtils.showSoftInput(terminal)
219+
}
220+
}
221+
222+
override fun shouldBackButtonBeMappedToEscape(): Boolean {
223+
return false
224+
}
225+
226+
override fun shouldEnforceCharBasedInput(): Boolean {
227+
return true
228+
}
229+
230+
override fun shouldUseCtrlSpaceWorkaround(): Boolean {
231+
return false
232+
}
233+
234+
override fun isTerminalViewSelected(): Boolean {
235+
return true
236+
}
237+
238+
override fun copyModeChanged(copyMode: Boolean) {}
239+
240+
override fun onKeyDown(keyCode: Int, e: KeyEvent?, session: TerminalSession?): Boolean {
241+
return false
242+
}
243+
244+
override fun onKeyUp(keyCode: Int, e: KeyEvent?): Boolean {
245+
if (keyCode == KeyEvent.KEYCODE_BACK) {
246+
if (terminal.mTermSession.isRunning) {
247+
terminal.mTermSession.finishIfRunning()
248+
}
249+
return true
250+
}
251+
return false
252+
}
253+
254+
override fun onLongPress(event: MotionEvent?): Boolean {
255+
return false
256+
}
257+
258+
override fun readControlKey(): Boolean {
259+
return false
260+
}
261+
262+
override fun readAltKey(): Boolean {
263+
return false
264+
}
265+
266+
override fun readShiftKey(): Boolean {
267+
return false
268+
}
269+
270+
override fun readFnKey(): Boolean {
271+
return false
272+
}
273+
274+
override fun onCodePoint(
275+
codePoint: Int,
276+
ctrlDown: Boolean,
277+
session: TerminalSession?
278+
): Boolean {
279+
return false
280+
}
281+
282+
override fun onEmulatorSet() {}
283+
}
284+
}

app/src/main/res/menu/menu_main.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@
5757
android:id="@+id/action_git"
5858
android:title="@string/git" />
5959

60+
<item
61+
android:id="@+id/action_terminal"
62+
android:title="@string/terminal" />
6063

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

app/src/main/res/menu/projects_menu.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
<menu xmlns:android="http://schemas.android.com/apk/res/android"
99
xmlns:app="http://schemas.android.com/apk/res-auto">
1010

11+
<item
12+
android:id="@+id/action_terminal"
13+
android:title="@string/terminal" />
14+
1115
<item
1216
android:id="@+id/action_settings"
1317
android:title="@string/action_settings"

app/src/main/res/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,5 @@
103103
<string name="program_arguments">Program Arguments</string>
104104
<string name="advanced">Advanced</string>
105105
<string name="gemini_pro">Gemini Pro</string>
106+
<string name="terminal">Terminal</string>
106107
</resources>

0 commit comments

Comments
 (0)