My own simple terminal app #4718
RohitVerma882
started this conversation in
General
Replies: 1 comment
-
TerminalSession.kt package com.xyz.terminal
import android.content.Context
import com.xyz.utils.ShellUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
//data class TerminalCell(
// var char: Char = ' '
//)
//
//data class TerminalRow(
// val cells: MutableList<TerminalCell>
//) {
// companion object {
// fun empty(cols: Int): TerminalRow {
// return TerminalRow(MutableList(cols) { TerminalCell() })
// }
// }
//}
class TerminalSession(
private val context: Context,
private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
private var maxLines: Int = 1000
) {
private var readJob: Job? = null
private var waitJob: Job? = null
private var fd: Int = -1
private var pid: Int = -1
private var rows = 24
private var cols = 80
// private var buffer: MutableList<TerminalRow> = MutableList(rows) { TerminalRow.empty(cols) }
private val mutex = Mutex()
private var cursorRow = 0
private var cursorCol = 0
// private val _grids = MutableStateFlow<List<TerminalRow>>(buffer)
// val grids: StateFlow<List<TerminalRow>> = _grids.asStateFlow()
var lines: MutableList<String> = mutableListOf("")
private set
private val _dirty = MutableStateFlow<Long>(0)
val dirty: StateFlow<Long> = _dirty.asStateFlow()
fun start() {
if (readJob?.isActive == true && waitJob?.isActive == true) {
return
}
val shell = "/system/bin/sh"
val homeDir = ShellUtils.getHomeDir(context).absolutePath
val envVars = ShellUtils.createEnvVarsArray(context)
val process = spawn(shell, homeDir, emptyArray(), envVars, rows, cols)
require(process != null) { "Failed to spawn PTY process." }
fd = process.getOrElse(0) { -1 }
pid = process.getOrElse(1) { -1 }
startReader()
startWaiter()
}
fun stop() {
readJob?.cancel()
waitJob?.cancel()
close(fd)
kill(pid)
fd = -1
pid = -1
}
fun write(data: String) {
scope.launch {
write(fd, data.toByteArray())
}
}
fun resize(newRows: Int, newCols: Int) {
Timber.d("newRows: %d, newCols: %d", newRows, newCols)
}
private fun startReader() {
readJob = scope.launch {
val buffer = ByteArray(4096)
while (isActive) {
val count = read(fd, buffer)
if (count > 0) {
processChunk(buffer.decodeToString(0, count))
Timber.d("count: %d, chunk: %s", count, buffer.decodeToString(0, count))
notifyScreenUpdated()
} else if (count < 0) {
break
}
}
}
}
private fun startWaiter() {
waitJob = scope.launch {
try {
val exitCode = waitFor(pid)
val message = buildString {
append('\n')
append("Process exited (code ")
append(exitCode)
append(")")
}
processChunk(message)
notifyScreenUpdated()
} finally {
stop()
}
}
}
private fun processChunk(chunk: String) {
chunk.forEach(::processChar)
}
// private fun processChar(char: Char) {
// when (char) {
// '\n' -> newLine()
// '\r' -> cursorCol = 0
// else -> putChar(char)
// }
// }
//
// private fun newLine() {
// cursorRow++
// cursorCol = 0
// if (cursorRow >= rows) {
// scrollUp()
// cursorRow = rows - 1
// }
// }
//
// private fun scrollUp() {
// buffer.removeAt(0)
// buffer.add(TerminalRow.empty(cols))
// }
//
// private fun putChar(ch: Char) {
// if (cursorCol >= cols) newLine()
//
// cursorRow = cursorRow.coerceIn(0, rows - 1)
// cursorCol = cursorCol.coerceIn(0, cols - 1)
//
// buffer[cursorRow].cells[cursorCol].char = ch
//
// cursorCol++
// }
//
// private suspend fun notifyScreenUpdated() {
// mutex.withLock {
// _grids.value = buffer.map { row ->
// TerminalRow(row.cells.map { it.copy() }.toMutableList())
// }
// }
// }
private fun processChar(char: Char) {
when (char) {
'\n' -> {
// Start new line
lines.add("")
if (lines.size > maxLines) {
lines.removeAt(0)
}
}
'\r' -> {
// Optional: treat as new line or ignore
}
else -> {
// Append to last line
if (lines.isEmpty()) {
lines.add("")
}
lines[lines.lastIndex] += char
}
}
}
private fun notifyScreenUpdated() {
_dirty.value = dirty.value + 1
}
private companion object {
init {
System.loadLibrary("pty")
}
@JvmStatic
external fun spawn(
shell: String,
cwd: String,
args: Array<String>,
envs: Array<String>,
rows: Int,
cols: Int
): IntArray?
@JvmStatic
external fun waitFor(pid: Int): Int
@JvmStatic
external fun resize(fd: Int, rows: Int, cols: Int)
@JvmStatic
external fun kill(pid: Int)
@JvmStatic
external fun close(fd: Int)
@JvmStatic
external fun read(fd: Int, buf: ByteArray): Int
@JvmStatic
external fun write(fd: Int, buf: ByteArray): Int
}
} |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
I want to create my own simple terminal session/view for my app in kotlin with pure compose ui.
Help me to only draw outputs to screen cell by cell in back/white color.
Beta Was this translation helpful? Give feedback.
All reactions