mirror of
https://github.com/topjohnwu/Magisk.git
synced 2026-06-12 19:11:46 -07:00
Replace Termux terminal libraries with custom read-only Kotlin implementation
Fork and heavily simplify terminal-emulator/terminal-view from Termux into a self-contained Kotlin terminal package. Remove all library-style abstractions (TerminalOutput, TerminalSessionClient, Logger) and dead code (mouse events, paste, key input) since the terminal is read-only. The emulator creates a PTY via busybox script for proper escape sequence support. The UI is a pure Compose Canvas with scroll support, replacing the old AndroidView-based approach. Made-with: Cursor
This commit is contained in:
@@ -52,6 +52,4 @@ dependencies {
|
||||
implementation(libs.navigationevent.compose)
|
||||
implementation(libs.lifecycle.viewmodel.navigation3)
|
||||
|
||||
// Terminal
|
||||
implementation(libs.termux.terminal.view)
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-sdk tools:overrideLibrary="com.termux.view, com.termux.terminal" />
|
||||
|
||||
<application android:localeConfig="@xml/locale_config">
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
|
||||
@@ -0,0 +1,452 @@
|
||||
package com.topjohnwu.magisk.terminal
|
||||
|
||||
import java.util.Arrays
|
||||
|
||||
/**
|
||||
* A circular buffer of [TerminalRow]s which keeps notes about what is visible on a logical screen and the scroll
|
||||
* history.
|
||||
*
|
||||
* See [externalToInternalRow] for how to map from logical screen rows to array indices.
|
||||
*/
|
||||
class TerminalBuffer(columns: Int, totalRows: Int, screenRows: Int) {
|
||||
|
||||
var lines: Array<TerminalRow?>
|
||||
|
||||
/** The length of [lines]. */
|
||||
var totalRows: Int = totalRows
|
||||
private set
|
||||
|
||||
/** The number of rows and columns visible on the screen. */
|
||||
var screenRows: Int = screenRows
|
||||
|
||||
var columns: Int = columns
|
||||
|
||||
/** The number of rows kept in history. */
|
||||
var activeTranscriptRows: Int = 0
|
||||
private set
|
||||
|
||||
/** The index in the circular buffer where the visible screen starts. */
|
||||
private var screenFirstRow = 0
|
||||
|
||||
init {
|
||||
lines = arrayOfNulls(totalRows)
|
||||
blockSet(0, 0, columns, screenRows, ' '.code, TextStyle.NORMAL)
|
||||
}
|
||||
|
||||
val transcriptText: String
|
||||
get() = getSelectedText(0, -activeTranscriptRows, columns, screenRows).trim()
|
||||
|
||||
val transcriptTextWithoutJoinedLines: String
|
||||
get() = getSelectedText(0, -activeTranscriptRows, columns, screenRows, false).trim()
|
||||
|
||||
val transcriptTextWithFullLinesJoined: String
|
||||
get() = getSelectedText(0, -activeTranscriptRows, columns, screenRows, joinBackLines = true, joinFullLines = true).trim()
|
||||
|
||||
fun getSelectedText(selX1: Int, selY1: Int, selX2: Int, selY2: Int, joinBackLines: Boolean = true, joinFullLines: Boolean = false): String {
|
||||
val builder = StringBuilder()
|
||||
|
||||
var y1 = selY1
|
||||
var y2 = selY2
|
||||
if (y1 < -activeTranscriptRows) y1 = -activeTranscriptRows
|
||||
if (y2 >= screenRows) y2 = screenRows - 1
|
||||
|
||||
for (row in y1..y2) {
|
||||
val x1 = if (row == y1) selX1 else 0
|
||||
var x2: Int
|
||||
if (row == y2) {
|
||||
x2 = selX2 + 1
|
||||
if (x2 > columns) x2 = columns
|
||||
} else {
|
||||
x2 = columns
|
||||
}
|
||||
val lineObject = lines[externalToInternalRow(row)]!!
|
||||
val x1Index = lineObject.findStartOfColumn(x1)
|
||||
var x2Index = if (x2 < columns) lineObject.findStartOfColumn(x2) else lineObject.spaceUsed
|
||||
if (x2Index == x1Index) {
|
||||
x2Index = lineObject.findStartOfColumn(x2 + 1)
|
||||
}
|
||||
val line = lineObject.text
|
||||
var lastPrintingCharIndex = -1
|
||||
val rowLineWrap = getLineWrap(row)
|
||||
if (rowLineWrap && x2 == columns) {
|
||||
lastPrintingCharIndex = x2Index - 1
|
||||
} else {
|
||||
for (i in x1Index until x2Index) {
|
||||
val c = line[i]
|
||||
if (c != ' ') lastPrintingCharIndex = i
|
||||
}
|
||||
}
|
||||
|
||||
val len = lastPrintingCharIndex - x1Index + 1
|
||||
if (lastPrintingCharIndex != -1 && len > 0)
|
||||
builder.append(line, x1Index, len)
|
||||
|
||||
val lineFillsWidth = lastPrintingCharIndex == x2Index - 1
|
||||
if ((!joinBackLines || !rowLineWrap) && (!joinFullLines || !lineFillsWidth)
|
||||
&& row < y2 && row < screenRows - 1) builder.append('\n')
|
||||
}
|
||||
return builder.toString()
|
||||
}
|
||||
|
||||
fun getWordAtLocation(x: Int, y: Int): String {
|
||||
var y1 = y
|
||||
var y2 = y
|
||||
while (y1 > 0 && !getSelectedText(0, y1 - 1, columns, y, joinBackLines = true, joinFullLines = true).contains("\n")) {
|
||||
y1--
|
||||
}
|
||||
while (y2 < screenRows && !getSelectedText(0, y, columns, y2 + 1, joinBackLines = true, joinFullLines = true).contains("\n")) {
|
||||
y2++
|
||||
}
|
||||
|
||||
val text = getSelectedText(0, y1, columns, y2, joinBackLines = true, joinFullLines = true)
|
||||
val textOffset = (y - y1) * columns + x
|
||||
|
||||
if (textOffset >= text.length) {
|
||||
return ""
|
||||
}
|
||||
|
||||
val x1 = text.lastIndexOf(' ', textOffset)
|
||||
var x2 = text.indexOf(' ', textOffset)
|
||||
if (x2 == -1) {
|
||||
x2 = text.length
|
||||
}
|
||||
|
||||
if (x1 == x2) {
|
||||
return ""
|
||||
}
|
||||
return text.substring(x1 + 1, x2)
|
||||
}
|
||||
|
||||
val activeRows: Int get() = activeTranscriptRows + screenRows
|
||||
|
||||
/**
|
||||
* Convert a row value from the public external coordinate system to our internal private coordinate system.
|
||||
*
|
||||
* ```
|
||||
* - External coordinate system: -activeTranscriptRows to screenRows-1, with the screen being 0..screenRows-1.
|
||||
* - Internal coordinate system: the screenRows lines starting at screenFirstRow comprise the screen, while the
|
||||
* activeTranscriptRows lines ending at screenFirstRow-1 form the transcript (as a circular buffer).
|
||||
*
|
||||
* External <-> Internal:
|
||||
*
|
||||
* [ ... ] [ ... ]
|
||||
* [ -activeTranscriptRows ] [ screenFirstRow - activeTranscriptRows ]
|
||||
* [ ... ] [ ... ]
|
||||
* [ 0 (visible screen starts here) ] <-> [ screenFirstRow ]
|
||||
* [ ... ] [ ... ]
|
||||
* [ screenRows-1 ] [ screenFirstRow + screenRows-1 ]
|
||||
* ```
|
||||
*
|
||||
* @param externalRow a row in the external coordinate system.
|
||||
* @return The row corresponding to the input argument in the private coordinate system.
|
||||
*/
|
||||
fun externalToInternalRow(externalRow: Int): Int {
|
||||
if (externalRow < -activeTranscriptRows || externalRow > screenRows)
|
||||
throw IllegalArgumentException("extRow=$externalRow, screenRows=$screenRows, activeTranscriptRows=$activeTranscriptRows")
|
||||
val internalRow = screenFirstRow + externalRow
|
||||
return if (internalRow < 0) (totalRows + internalRow) else (internalRow % totalRows)
|
||||
}
|
||||
|
||||
fun setLineWrap(row: Int) {
|
||||
lines[externalToInternalRow(row)]!!.lineWrap = true
|
||||
}
|
||||
|
||||
fun getLineWrap(row: Int): Boolean {
|
||||
return lines[externalToInternalRow(row)]!!.lineWrap
|
||||
}
|
||||
|
||||
fun clearLineWrap(row: Int) {
|
||||
lines[externalToInternalRow(row)]!!.lineWrap = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize the screen which this transcript backs. Currently, this only works if the number of columns does not
|
||||
* change or the rows expand (that is, it only works when shrinking the number of rows).
|
||||
*
|
||||
* @param newColumns The number of columns the screen should have.
|
||||
* @param newRows The number of rows the screen should have.
|
||||
* @param cursor An int[2] containing the (column, row) cursor location.
|
||||
*/
|
||||
fun resize(newColumns: Int, newRows: Int, newTotalRows: Int, cursor: IntArray, currentStyle: Long, altScreen: Boolean) {
|
||||
// newRows > totalRows should not normally happen since totalRows is TRANSCRIPT_ROWS (10000):
|
||||
if (newColumns == columns && newRows <= totalRows) {
|
||||
// Fast resize where just the rows changed.
|
||||
var shiftDownOfTopRow = screenRows - newRows
|
||||
if (shiftDownOfTopRow > 0 && shiftDownOfTopRow < screenRows) {
|
||||
// Shrinking. Check if we can skip blank rows at bottom below cursor.
|
||||
for (i in screenRows - 1 downTo 1) {
|
||||
if (cursor[1] >= i) break
|
||||
val r = externalToInternalRow(i)
|
||||
if (lines[r] == null || lines[r]!!.isBlank()) {
|
||||
if (--shiftDownOfTopRow == 0) break
|
||||
}
|
||||
}
|
||||
} else if (shiftDownOfTopRow < 0) {
|
||||
// Negative shift down = expanding. Only move screen up if there is transcript to show:
|
||||
val actualShift = maxOf(shiftDownOfTopRow, -activeTranscriptRows)
|
||||
if (shiftDownOfTopRow != actualShift) {
|
||||
for (i in 0 until actualShift - shiftDownOfTopRow)
|
||||
allocateFullLineIfNecessary((screenFirstRow + screenRows + i) % totalRows).clear(currentStyle)
|
||||
shiftDownOfTopRow = actualShift
|
||||
}
|
||||
}
|
||||
screenFirstRow += shiftDownOfTopRow
|
||||
screenFirstRow = if (screenFirstRow < 0) (screenFirstRow + totalRows) else (screenFirstRow % totalRows)
|
||||
totalRows = newTotalRows
|
||||
activeTranscriptRows = if (altScreen) 0 else maxOf(0, activeTranscriptRows + shiftDownOfTopRow)
|
||||
cursor[1] -= shiftDownOfTopRow
|
||||
screenRows = newRows
|
||||
} else {
|
||||
// Copy away old state and update new:
|
||||
val oldLines = lines
|
||||
lines = arrayOfNulls(newTotalRows)
|
||||
for (i in 0 until newTotalRows)
|
||||
lines[i] = TerminalRow(newColumns, currentStyle)
|
||||
|
||||
val oldActiveTranscriptRows = activeTranscriptRows
|
||||
val oldScreenFirstRow = screenFirstRow
|
||||
val oldScreenRows = screenRows
|
||||
val oldTotalRows = totalRows
|
||||
totalRows = newTotalRows
|
||||
screenRows = newRows
|
||||
activeTranscriptRows = 0
|
||||
screenFirstRow = 0
|
||||
columns = newColumns
|
||||
|
||||
var newCursorRow = -1
|
||||
var newCursorColumn = -1
|
||||
val oldCursorRow = cursor[1]
|
||||
val oldCursorColumn = cursor[0]
|
||||
var newCursorPlaced = false
|
||||
|
||||
var currentOutputExternalRow = 0
|
||||
var currentOutputExternalColumn = 0
|
||||
|
||||
var skippedBlankLines = 0
|
||||
for (externalOldRow in -oldActiveTranscriptRows until oldScreenRows) {
|
||||
var internalOldRow = oldScreenFirstRow + externalOldRow
|
||||
internalOldRow = if (internalOldRow < 0) (oldTotalRows + internalOldRow) else (internalOldRow % oldTotalRows)
|
||||
|
||||
val oldLine = oldLines[internalOldRow]
|
||||
val cursorAtThisRow = externalOldRow == oldCursorRow
|
||||
if (oldLine == null || (!(!newCursorPlaced && cursorAtThisRow)) && oldLine.isBlank()) {
|
||||
skippedBlankLines++
|
||||
continue
|
||||
} else if (skippedBlankLines > 0) {
|
||||
for (i in 0 until skippedBlankLines) {
|
||||
if (currentOutputExternalRow == screenRows - 1) {
|
||||
scrollDownOneLine(0, screenRows, currentStyle)
|
||||
} else {
|
||||
currentOutputExternalRow++
|
||||
}
|
||||
currentOutputExternalColumn = 0
|
||||
}
|
||||
skippedBlankLines = 0
|
||||
}
|
||||
|
||||
var lastNonSpaceIndex = 0
|
||||
var justToCursor = false
|
||||
if (cursorAtThisRow || oldLine.lineWrap) {
|
||||
lastNonSpaceIndex = oldLine.spaceUsed
|
||||
if (cursorAtThisRow) justToCursor = true
|
||||
} else {
|
||||
for (i in 0 until oldLine.spaceUsed)
|
||||
// NEWLY INTRODUCED BUG! Should not index oldLine.styles with char indices
|
||||
if (oldLine.text[i] != ' '/* || oldLine.styles[i] != currentStyle */)
|
||||
lastNonSpaceIndex = i + 1
|
||||
}
|
||||
|
||||
var currentOldCol = 0
|
||||
var styleAtCol = 0L
|
||||
var i = 0
|
||||
while (i < lastNonSpaceIndex) {
|
||||
val c = oldLine.text[i]
|
||||
val codePoint: Int
|
||||
if (Character.isHighSurrogate(c)) {
|
||||
i++
|
||||
codePoint = Character.toCodePoint(c, oldLine.text[i])
|
||||
} else {
|
||||
codePoint = c.code
|
||||
}
|
||||
val displayWidth = WcWidth.width(codePoint)
|
||||
if (displayWidth > 0) styleAtCol = oldLine.getStyle(currentOldCol)
|
||||
|
||||
if (currentOutputExternalColumn + displayWidth > columns) {
|
||||
setLineWrap(currentOutputExternalRow)
|
||||
if (currentOutputExternalRow == screenRows - 1) {
|
||||
if (newCursorPlaced) newCursorRow--
|
||||
scrollDownOneLine(0, screenRows, currentStyle)
|
||||
} else {
|
||||
currentOutputExternalRow++
|
||||
}
|
||||
currentOutputExternalColumn = 0
|
||||
}
|
||||
|
||||
val offsetDueToCombiningChar = if (displayWidth <= 0 && currentOutputExternalColumn > 0) 1 else 0
|
||||
val outputColumn = currentOutputExternalColumn - offsetDueToCombiningChar
|
||||
setChar(outputColumn, currentOutputExternalRow, codePoint, styleAtCol)
|
||||
|
||||
if (displayWidth > 0) {
|
||||
if (oldCursorRow == externalOldRow && oldCursorColumn == currentOldCol) {
|
||||
newCursorColumn = currentOutputExternalColumn
|
||||
newCursorRow = currentOutputExternalRow
|
||||
newCursorPlaced = true
|
||||
}
|
||||
currentOldCol += displayWidth
|
||||
currentOutputExternalColumn += displayWidth
|
||||
if (justToCursor && newCursorPlaced) break
|
||||
}
|
||||
i++
|
||||
}
|
||||
if (externalOldRow != (oldScreenRows - 1) && !oldLine.lineWrap) {
|
||||
if (currentOutputExternalRow == screenRows - 1) {
|
||||
if (newCursorPlaced) newCursorRow--
|
||||
scrollDownOneLine(0, screenRows, currentStyle)
|
||||
} else {
|
||||
currentOutputExternalRow++
|
||||
}
|
||||
currentOutputExternalColumn = 0
|
||||
}
|
||||
}
|
||||
|
||||
cursor[0] = newCursorColumn
|
||||
cursor[1] = newCursorRow
|
||||
}
|
||||
|
||||
// Handle cursor scrolling off screen:
|
||||
if (cursor[0] < 0 || cursor[1] < 0) {
|
||||
cursor[0] = 0
|
||||
cursor[1] = 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Block copy lines and associated metadata from one location to another in the circular buffer, taking wraparound
|
||||
* into account.
|
||||
*
|
||||
* @param srcInternal The first line to be copied.
|
||||
* @param len The number of lines to be copied.
|
||||
*/
|
||||
private fun blockCopyLinesDown(srcInternal: Int, len: Int) {
|
||||
if (len == 0) return
|
||||
|
||||
val start = len - 1
|
||||
val lineToBeOverWritten = lines[(srcInternal + start + 1) % totalRows]
|
||||
for (i in start downTo 0)
|
||||
lines[(srcInternal + i + 1) % totalRows] = lines[(srcInternal + i) % totalRows]
|
||||
lines[srcInternal % totalRows] = lineToBeOverWritten
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll the screen down one line. To scroll the whole screen of a 24 line screen, the arguments would be (0, 24).
|
||||
*
|
||||
* @param topMargin First line that is scrolled.
|
||||
* @param bottomMargin One line after the last line that is scrolled.
|
||||
* @param style the style for the newly exposed line.
|
||||
*/
|
||||
fun scrollDownOneLine(topMargin: Int, bottomMargin: Int, style: Long) {
|
||||
if (topMargin > bottomMargin - 1 || topMargin < 0 || bottomMargin > screenRows)
|
||||
throw IllegalArgumentException("topMargin=$topMargin, bottomMargin=$bottomMargin, screenRows=$screenRows")
|
||||
|
||||
blockCopyLinesDown(screenFirstRow, topMargin)
|
||||
blockCopyLinesDown(externalToInternalRow(bottomMargin), screenRows - bottomMargin)
|
||||
|
||||
screenFirstRow = (screenFirstRow + 1) % totalRows
|
||||
if (activeTranscriptRows < totalRows - screenRows) activeTranscriptRows++
|
||||
|
||||
val blankRow = externalToInternalRow(bottomMargin - 1)
|
||||
if (lines[blankRow] == null) {
|
||||
lines[blankRow] = TerminalRow(columns, style)
|
||||
} else {
|
||||
lines[blankRow]!!.clear(style)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Block copy characters from one position in the screen to another. The two positions can overlap. All characters
|
||||
* of the source and destination must be within the bounds of the screen, or else an InvalidParameterException will
|
||||
* be thrown.
|
||||
*
|
||||
* @param sx source X coordinate
|
||||
* @param sy source Y coordinate
|
||||
* @param w width
|
||||
* @param h height
|
||||
* @param dx destination X coordinate
|
||||
* @param dy destination Y coordinate
|
||||
*/
|
||||
fun blockCopy(sx: Int, sy: Int, w: Int, h: Int, dx: Int, dy: Int) {
|
||||
if (w == 0) return
|
||||
if (sx < 0 || sx + w > columns || sy < 0 || sy + h > screenRows || dx < 0 || dx + w > columns || dy < 0 || dy + h > screenRows)
|
||||
throw IllegalArgumentException()
|
||||
val copyingUp = sy > dy
|
||||
for (y in 0 until h) {
|
||||
val y2 = if (copyingUp) y else (h - (y + 1))
|
||||
val sourceRow = allocateFullLineIfNecessary(externalToInternalRow(sy + y2))
|
||||
allocateFullLineIfNecessary(externalToInternalRow(dy + y2)).copyInterval(sourceRow, sx, sx + w, dx)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Block set characters. All characters must be within the bounds of the screen, or else an
|
||||
* InvalidParameterException will be thrown. Typically this is called with a "val" argument of 32 to clear a block
|
||||
* of characters.
|
||||
*/
|
||||
fun blockSet(sx: Int, sy: Int, w: Int, h: Int, `val`: Int, style: Long) {
|
||||
if (sx < 0 || sx + w > columns || sy < 0 || sy + h > screenRows) {
|
||||
throw IllegalArgumentException(
|
||||
"Illegal arguments! blockSet($sx, $sy, $w, $h, $`val`, $columns, $screenRows)")
|
||||
}
|
||||
for (y in 0 until h)
|
||||
for (x in 0 until w)
|
||||
setChar(sx + x, sy + y, `val`, style)
|
||||
}
|
||||
|
||||
fun allocateFullLineIfNecessary(row: Int): TerminalRow {
|
||||
return lines[row] ?: TerminalRow(columns, 0).also { lines[row] = it }
|
||||
}
|
||||
|
||||
fun setChar(column: Int, row: Int, codePoint: Int, style: Long) {
|
||||
if (row < 0 || row >= screenRows || column < 0 || column >= columns)
|
||||
throw IllegalArgumentException("TerminalBuffer.setChar(): row=$row, column=$column, screenRows=$screenRows, columns=$columns")
|
||||
val internalRow = externalToInternalRow(row)
|
||||
allocateFullLineIfNecessary(internalRow).setChar(column, codePoint, style)
|
||||
}
|
||||
|
||||
fun getStyleAt(externalRow: Int, column: Int): Long {
|
||||
return allocateFullLineIfNecessary(externalToInternalRow(externalRow)).getStyle(column)
|
||||
}
|
||||
|
||||
/** Support for http://vt100.net/docs/vt510-rm/DECCARA and http://vt100.net/docs/vt510-rm/DECCARA */
|
||||
fun setOrClearEffect(bits: Int, setOrClear: Boolean, reverse: Boolean, rectangular: Boolean, leftMargin: Int, rightMargin: Int, top: Int, left: Int,
|
||||
bottom: Int, right: Int) {
|
||||
for (y in top until bottom) {
|
||||
val line = lines[externalToInternalRow(y)]!!
|
||||
val startOfLine = if (rectangular || y == top) left else leftMargin
|
||||
val endOfLine = if (rectangular || y + 1 == bottom) right else rightMargin
|
||||
for (x in startOfLine until endOfLine) {
|
||||
val currentStyle = line.getStyle(x)
|
||||
val foreColor = TextStyle.decodeForeColor(currentStyle)
|
||||
val backColor = TextStyle.decodeBackColor(currentStyle)
|
||||
var effect = TextStyle.decodeEffect(currentStyle)
|
||||
if (reverse) {
|
||||
effect = (effect and bits.inv()) or (bits and effect.inv())
|
||||
} else if (setOrClear) {
|
||||
effect = effect or bits
|
||||
} else {
|
||||
effect = effect and bits.inv()
|
||||
}
|
||||
line.styles[x] = TextStyle.encode(foreColor, backColor, effect)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearTranscript() {
|
||||
if (screenFirstRow < activeTranscriptRows) {
|
||||
Arrays.fill(lines, totalRows + screenFirstRow - activeTranscriptRows, totalRows, null)
|
||||
Arrays.fill(lines, 0, screenFirstRow, null)
|
||||
} else {
|
||||
Arrays.fill(lines, screenFirstRow - activeTranscriptRows, screenFirstRow, null)
|
||||
}
|
||||
activeTranscriptRows = 0
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,61 @@
|
||||
package com.topjohnwu.magisk.terminal
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import timber.log.Timber
|
||||
|
||||
private val busyboxPath: String by lazy {
|
||||
Shell.cmd("readlink /proc/self/exe").exec().out.firstOrNull()
|
||||
?: "/data/adb/magisk/busybox"
|
||||
}
|
||||
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
fun TerminalEmulator.appendOnMain(bytes: ByteArray, len: Int) {
|
||||
mainHandler.post {
|
||||
append(bytes, len)
|
||||
onScreenUpdate?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
fun TerminalEmulator.appendLineOnMain(line: String) {
|
||||
val bytes = "$line\r\n".toByteArray(Charsets.UTF_8)
|
||||
appendOnMain(bytes, bytes.size)
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a command as root inside a PTY (via busybox script).
|
||||
* Reads raw bytes from the process and feeds them to the terminal emulator.
|
||||
* Must be called from a background thread.
|
||||
* Returns true if the process exits with code 0.
|
||||
*/
|
||||
fun runSuCommand(emulator: TerminalEmulator, command: String): Boolean {
|
||||
return try {
|
||||
val cols = emulator.mColumns
|
||||
val rows = emulator.mRows
|
||||
val wrappedCmd = "export TERM=xterm-256color; stty cols $cols rows $rows 2>/dev/null; $command"
|
||||
|
||||
val process = ProcessBuilder(
|
||||
"su", "-c",
|
||||
"$busyboxPath script -q -c '$wrappedCmd' /dev/null"
|
||||
).redirectErrorStream(true).start()
|
||||
|
||||
process.outputStream.close()
|
||||
|
||||
val buffer = ByteArray(4096)
|
||||
process.inputStream.use { input ->
|
||||
while (true) {
|
||||
val n = input.read(buffer)
|
||||
if (n == -1) break
|
||||
emulator.appendOnMain(buffer.copyOf(n), n)
|
||||
}
|
||||
}
|
||||
|
||||
process.waitFor() == 0
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "runSuCommand failed")
|
||||
emulator.appendLineOnMain("! Error: ${e.message}")
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
package com.topjohnwu.magisk.terminal
|
||||
|
||||
import java.util.Arrays
|
||||
|
||||
/**
|
||||
* A row in a terminal, composed of a fixed number of cells.
|
||||
*
|
||||
* The text in the row is stored in a char[] array, [text], for quick access during rendering.
|
||||
*/
|
||||
class TerminalRow(private val columns: Int, style: Long) {
|
||||
|
||||
/**
|
||||
* Max combining characters that can exist in a column, that are separate from the base character
|
||||
* itself. Any additional combining characters will be ignored and not added to the column.
|
||||
*
|
||||
* There does not seem to be limit in unicode standard for max number of combination characters
|
||||
* that can be combined but such characters are primarily under 10.
|
||||
*
|
||||
* "Section 3.6 Combination" of unicode standard contains combining characters info.
|
||||
* - https://www.unicode.org/versions/Unicode15.0.0/ch03.pdf
|
||||
* - https://en.wikipedia.org/wiki/Combining_character#Unicode_ranges
|
||||
* - https://stackoverflow.com/questions/71237212/what-is-the-maximum-number-of-unicode-combined-characters-that-may-be-needed-to
|
||||
*
|
||||
* UAX15-D3 Stream-Safe Text Format limits to max 30 combining characters.
|
||||
* > The value of 30 is chosen to be significantly beyond what is required for any linguistic or technical usage.
|
||||
* > While it would have been feasible to chose a smaller number, this value provides a very wide margin,
|
||||
* > yet is well within the buffer size limits of practical implementations.
|
||||
* - https://unicode.org/reports/tr15/#Stream_Safe_Text_Format
|
||||
* - https://stackoverflow.com/a/11983435/14686958
|
||||
*
|
||||
* We choose the value 15 because it should be enough for terminal based applications and keep
|
||||
* the memory usage low for a terminal row, won't affect performance or cause terminal to
|
||||
* lag or hang, and will keep malicious applications from causing harm. The value can be
|
||||
* increased if ever needed for legitimate applications.
|
||||
*/
|
||||
companion object {
|
||||
private const val SPARE_CAPACITY_FACTOR = 1.5f
|
||||
private const val MAX_COMBINING_CHARACTERS_PER_COLUMN = 15
|
||||
}
|
||||
|
||||
/** The text filling this terminal row. */
|
||||
var text: CharArray = CharArray((SPARE_CAPACITY_FACTOR * columns).toInt())
|
||||
|
||||
/** The number of java chars used in [text]. */
|
||||
private var _spaceUsed: Short = 0
|
||||
|
||||
/** If this row has been line wrapped due to text output at the end of line. */
|
||||
var lineWrap: Boolean = false
|
||||
|
||||
/** The style bits of each cell in the row. See [TextStyle]. */
|
||||
val styles: LongArray = LongArray(columns)
|
||||
|
||||
/** If this row might contain chars with width != 1, used for deactivating fast path */
|
||||
var hasNonOneWidthOrSurrogateChars: Boolean = false
|
||||
|
||||
init {
|
||||
clear(style)
|
||||
}
|
||||
|
||||
/** NOTE: The sourceX2 is exclusive. */
|
||||
fun copyInterval(line: TerminalRow, sourceX1: Int, sourceX2: Int, destinationX: Int) {
|
||||
hasNonOneWidthOrSurrogateChars = hasNonOneWidthOrSurrogateChars or line.hasNonOneWidthOrSurrogateChars
|
||||
val x1 = line.findStartOfColumn(sourceX1)
|
||||
val x2 = line.findStartOfColumn(sourceX2)
|
||||
var startingFromSecondHalfOfWideChar = sourceX1 > 0 && line.wideDisplayCharacterStartingAt(sourceX1 - 1)
|
||||
val sourceChars = if (this === line) line.text.copyOf() else line.text
|
||||
var latestNonCombiningWidth = 0
|
||||
var destX = destinationX
|
||||
var srcX1 = sourceX1
|
||||
var i = x1
|
||||
while (i < x2) {
|
||||
val sourceChar = sourceChars[i]
|
||||
var codePoint: Int
|
||||
if (Character.isHighSurrogate(sourceChar)) {
|
||||
i++
|
||||
codePoint = Character.toCodePoint(sourceChar, sourceChars[i])
|
||||
} else {
|
||||
codePoint = sourceChar.code
|
||||
}
|
||||
if (startingFromSecondHalfOfWideChar) {
|
||||
codePoint = ' '.code
|
||||
startingFromSecondHalfOfWideChar = false
|
||||
}
|
||||
val w = WcWidth.width(codePoint)
|
||||
if (w > 0) {
|
||||
destX += latestNonCombiningWidth
|
||||
srcX1 += latestNonCombiningWidth
|
||||
latestNonCombiningWidth = w
|
||||
}
|
||||
setChar(destX, codePoint, line.getStyle(srcX1))
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
val spaceUsed: Int get() = _spaceUsed.toInt()
|
||||
|
||||
/** Note that the column may end of second half of wide character. */
|
||||
fun findStartOfColumn(column: Int): Int {
|
||||
if (column == columns) return spaceUsed
|
||||
|
||||
var currentColumn = 0
|
||||
var currentCharIndex = 0
|
||||
while (true) {
|
||||
var newCharIndex = currentCharIndex
|
||||
val c = text[newCharIndex++]
|
||||
val isHigh = Character.isHighSurrogate(c)
|
||||
val codePoint = if (isHigh) Character.toCodePoint(c, text[newCharIndex++]) else c.code
|
||||
val wcwidth = WcWidth.width(codePoint)
|
||||
if (wcwidth > 0) {
|
||||
currentColumn += wcwidth
|
||||
if (currentColumn == column) {
|
||||
while (newCharIndex < _spaceUsed) {
|
||||
if (Character.isHighSurrogate(text[newCharIndex])) {
|
||||
if (WcWidth.width(Character.toCodePoint(text[newCharIndex], text[newCharIndex + 1])) <= 0) {
|
||||
newCharIndex += 2
|
||||
} else {
|
||||
break
|
||||
}
|
||||
} else if (WcWidth.width(text[newCharIndex].code) <= 0) {
|
||||
newCharIndex++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return newCharIndex
|
||||
} else if (currentColumn > column) {
|
||||
return currentCharIndex
|
||||
}
|
||||
}
|
||||
currentCharIndex = newCharIndex
|
||||
}
|
||||
}
|
||||
|
||||
private fun wideDisplayCharacterStartingAt(column: Int): Boolean {
|
||||
var currentCharIndex = 0
|
||||
var currentColumn = 0
|
||||
while (currentCharIndex < _spaceUsed) {
|
||||
val c = text[currentCharIndex++]
|
||||
val codePoint = if (Character.isHighSurrogate(c)) Character.toCodePoint(c, text[currentCharIndex++]) else c.code
|
||||
val wcwidth = WcWidth.width(codePoint)
|
||||
if (wcwidth > 0) {
|
||||
if (currentColumn == column && wcwidth == 2) return true
|
||||
currentColumn += wcwidth
|
||||
if (currentColumn > column) return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun clear(style: Long) {
|
||||
Arrays.fill(text, ' ')
|
||||
Arrays.fill(styles, style)
|
||||
_spaceUsed = columns.toShort()
|
||||
hasNonOneWidthOrSurrogateChars = false
|
||||
}
|
||||
|
||||
// https://github.com/steven676/Android-Terminal-Emulator/commit/9a47042620bec87617f0b4f5d50568535668fe26
|
||||
fun setChar(columnToSet: Int, codePoint: Int, style: Long) {
|
||||
if (columnToSet < 0 || columnToSet >= styles.size)
|
||||
throw IllegalArgumentException("TerminalRow.setChar(): columnToSet=$columnToSet, codePoint=$codePoint, style=$style")
|
||||
|
||||
styles[columnToSet] = style
|
||||
|
||||
val newCodePointDisplayWidth = WcWidth.width(codePoint)
|
||||
|
||||
// Fast path when we don't have any chars with width != 1
|
||||
if (!hasNonOneWidthOrSurrogateChars) {
|
||||
if (codePoint >= Character.MIN_SUPPLEMENTARY_CODE_POINT || newCodePointDisplayWidth != 1) {
|
||||
hasNonOneWidthOrSurrogateChars = true
|
||||
} else {
|
||||
text[columnToSet] = codePoint.toChar()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
val newIsCombining = newCodePointDisplayWidth <= 0
|
||||
|
||||
val wasExtraColForWideChar = columnToSet > 0 && wideDisplayCharacterStartingAt(columnToSet - 1)
|
||||
|
||||
var col = columnToSet
|
||||
if (newIsCombining) {
|
||||
if (wasExtraColForWideChar) col--
|
||||
} else {
|
||||
if (wasExtraColForWideChar) setChar(col - 1, ' '.code, style)
|
||||
val overwritingWideCharInNextColumn = newCodePointDisplayWidth == 2 && wideDisplayCharacterStartingAt(col + 1)
|
||||
if (overwritingWideCharInNextColumn) setChar(col + 1, ' '.code, style)
|
||||
}
|
||||
|
||||
var textArray = text
|
||||
val oldStartOfColumnIndex = findStartOfColumn(col)
|
||||
val oldCodePointDisplayWidth = WcWidth.width(textArray, oldStartOfColumnIndex)
|
||||
|
||||
val oldCharactersUsedForColumn: Int
|
||||
if (col + oldCodePointDisplayWidth < columns) {
|
||||
val oldEndOfColumnIndex = findStartOfColumn(col + oldCodePointDisplayWidth)
|
||||
oldCharactersUsedForColumn = oldEndOfColumnIndex - oldStartOfColumnIndex
|
||||
} else {
|
||||
oldCharactersUsedForColumn = _spaceUsed - oldStartOfColumnIndex
|
||||
}
|
||||
|
||||
if (newIsCombining) {
|
||||
val combiningCharsCount = WcWidth.zeroWidthCharsCount(textArray, oldStartOfColumnIndex, oldStartOfColumnIndex + oldCharactersUsedForColumn)
|
||||
if (combiningCharsCount >= MAX_COMBINING_CHARACTERS_PER_COLUMN)
|
||||
return
|
||||
}
|
||||
|
||||
var newCharactersUsedForColumn = Character.charCount(codePoint)
|
||||
if (newIsCombining) {
|
||||
newCharactersUsedForColumn += oldCharactersUsedForColumn
|
||||
}
|
||||
|
||||
val oldNextColumnIndex = oldStartOfColumnIndex + oldCharactersUsedForColumn
|
||||
val newNextColumnIndex = oldStartOfColumnIndex + newCharactersUsedForColumn
|
||||
|
||||
val javaCharDifference = newCharactersUsedForColumn - oldCharactersUsedForColumn
|
||||
if (javaCharDifference > 0) {
|
||||
val oldCharactersAfterColumn = _spaceUsed - oldNextColumnIndex
|
||||
if (_spaceUsed + javaCharDifference > textArray.size) {
|
||||
val newText = CharArray(textArray.size + columns)
|
||||
System.arraycopy(textArray, 0, newText, 0, oldNextColumnIndex)
|
||||
System.arraycopy(textArray, oldNextColumnIndex, newText, newNextColumnIndex, oldCharactersAfterColumn)
|
||||
text = newText
|
||||
textArray = newText
|
||||
} else {
|
||||
System.arraycopy(textArray, oldNextColumnIndex, textArray, newNextColumnIndex, oldCharactersAfterColumn)
|
||||
}
|
||||
} else if (javaCharDifference < 0) {
|
||||
System.arraycopy(textArray, oldNextColumnIndex, textArray, newNextColumnIndex, _spaceUsed - oldNextColumnIndex)
|
||||
}
|
||||
_spaceUsed = (_spaceUsed + javaCharDifference).toShort()
|
||||
|
||||
Character.toChars(codePoint, textArray, oldStartOfColumnIndex + if (newIsCombining) oldCharactersUsedForColumn else 0)
|
||||
|
||||
if (oldCodePointDisplayWidth == 2 && newCodePointDisplayWidth == 1) {
|
||||
if (_spaceUsed + 1 > textArray.size) {
|
||||
val newText = CharArray(textArray.size + columns)
|
||||
System.arraycopy(textArray, 0, newText, 0, newNextColumnIndex)
|
||||
System.arraycopy(textArray, newNextColumnIndex, newText, newNextColumnIndex + 1, _spaceUsed - newNextColumnIndex)
|
||||
text = newText
|
||||
textArray = newText
|
||||
} else {
|
||||
System.arraycopy(textArray, newNextColumnIndex, textArray, newNextColumnIndex + 1, _spaceUsed - newNextColumnIndex)
|
||||
}
|
||||
textArray[newNextColumnIndex] = ' '
|
||||
++_spaceUsed
|
||||
} else if (oldCodePointDisplayWidth == 1 && newCodePointDisplayWidth == 2) {
|
||||
if (col == columns - 1) {
|
||||
throw IllegalArgumentException("Cannot put wide character in last column")
|
||||
} else if (col == columns - 2) {
|
||||
_spaceUsed = newNextColumnIndex.toShort()
|
||||
} else {
|
||||
val newNextNextColumnIndex = newNextColumnIndex + if (Character.isHighSurrogate(textArray[newNextColumnIndex])) 2 else 1
|
||||
val nextLen = newNextNextColumnIndex - newNextColumnIndex
|
||||
System.arraycopy(textArray, newNextNextColumnIndex, textArray, newNextColumnIndex, _spaceUsed - newNextNextColumnIndex)
|
||||
_spaceUsed = (_spaceUsed - nextLen).toShort()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun isBlank(): Boolean {
|
||||
for (charIndex in 0 until spaceUsed)
|
||||
if (text[charIndex] != ' ') return false
|
||||
return true
|
||||
}
|
||||
|
||||
fun getStyle(column: Int): Long = styles[column]
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
package com.topjohnwu.magisk.terminal
|
||||
|
||||
import android.graphics.Color
|
||||
import java.util.Properties
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sqrt
|
||||
|
||||
object TextStyle {
|
||||
|
||||
const val CHARACTER_ATTRIBUTE_BOLD = 1
|
||||
const val CHARACTER_ATTRIBUTE_ITALIC = 1 shl 1
|
||||
const val CHARACTER_ATTRIBUTE_UNDERLINE = 1 shl 2
|
||||
const val CHARACTER_ATTRIBUTE_BLINK = 1 shl 3
|
||||
const val CHARACTER_ATTRIBUTE_INVERSE = 1 shl 4
|
||||
const val CHARACTER_ATTRIBUTE_INVISIBLE = 1 shl 5
|
||||
const val CHARACTER_ATTRIBUTE_STRIKETHROUGH = 1 shl 6
|
||||
const val CHARACTER_ATTRIBUTE_PROTECTED = 1 shl 7
|
||||
const val CHARACTER_ATTRIBUTE_DIM = 1 shl 8
|
||||
private const val CHARACTER_ATTRIBUTE_TRUECOLOR_FOREGROUND = 1 shl 9
|
||||
private const val CHARACTER_ATTRIBUTE_TRUECOLOR_BACKGROUND = 1 shl 10
|
||||
|
||||
const val COLOR_INDEX_FOREGROUND = 256
|
||||
const val COLOR_INDEX_BACKGROUND = 257
|
||||
const val COLOR_INDEX_CURSOR = 258
|
||||
const val NUM_INDEXED_COLORS = 259
|
||||
|
||||
val NORMAL = encode(COLOR_INDEX_FOREGROUND, COLOR_INDEX_BACKGROUND, 0)
|
||||
|
||||
fun encode(foreColor: Int, backColor: Int, effect: Int): Long {
|
||||
var result = (effect and 0b111111111).toLong()
|
||||
if (foreColor and 0xff000000.toInt() == 0xff000000.toInt()) {
|
||||
result = result or CHARACTER_ATTRIBUTE_TRUECOLOR_FOREGROUND.toLong() or ((foreColor.toLong() and 0x00ffffffL) shl 40)
|
||||
} else {
|
||||
result = result or ((foreColor.toLong() and 0b111111111L) shl 40)
|
||||
}
|
||||
if (backColor and 0xff000000.toInt() == 0xff000000.toInt()) {
|
||||
result = result or CHARACTER_ATTRIBUTE_TRUECOLOR_BACKGROUND.toLong() or ((backColor.toLong() and 0x00ffffffL) shl 16)
|
||||
} else {
|
||||
result = result or ((backColor.toLong() and 0b111111111L) shl 16)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun decodeForeColor(style: Long): Int {
|
||||
return if (style and CHARACTER_ATTRIBUTE_TRUECOLOR_FOREGROUND.toLong() == 0L) {
|
||||
((style ushr 40) and 0b111111111L).toInt()
|
||||
} else {
|
||||
0xff000000.toInt() or ((style ushr 40) and 0x00ffffffL).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
fun decodeBackColor(style: Long): Int {
|
||||
return if (style and CHARACTER_ATTRIBUTE_TRUECOLOR_BACKGROUND.toLong() == 0L) {
|
||||
((style ushr 16) and 0b111111111L).toInt()
|
||||
} else {
|
||||
0xff000000.toInt() or ((style ushr 16) and 0x00ffffffL).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
fun decodeEffect(style: Long): Int {
|
||||
return (style and 0b11111111111L).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Color scheme for a terminal with default colors, which may be overridden (and then reset) from the shell using
|
||||
* Operating System Control (OSC) sequences.
|
||||
*/
|
||||
class TerminalColorScheme {
|
||||
|
||||
val defaultColors: IntArray = IntArray(TextStyle.NUM_INDEXED_COLORS)
|
||||
|
||||
init {
|
||||
reset()
|
||||
}
|
||||
|
||||
fun updateWith(props: Properties) {
|
||||
reset()
|
||||
var cursorPropExists = false
|
||||
for ((keyObj, valueObj) in props) {
|
||||
val key = keyObj as String
|
||||
val value = valueObj as String
|
||||
val colorIndex: Int = when {
|
||||
key == "foreground" -> TextStyle.COLOR_INDEX_FOREGROUND
|
||||
key == "background" -> TextStyle.COLOR_INDEX_BACKGROUND
|
||||
key == "cursor" -> {
|
||||
cursorPropExists = true
|
||||
TextStyle.COLOR_INDEX_CURSOR
|
||||
}
|
||||
key.startsWith("color") -> {
|
||||
try {
|
||||
key.substring(5).toInt()
|
||||
} catch (_: NumberFormatException) {
|
||||
throw IllegalArgumentException("Invalid property: '$key'")
|
||||
}
|
||||
}
|
||||
else -> throw IllegalArgumentException("Invalid property: '$key'")
|
||||
}
|
||||
|
||||
val colorValue = TerminalColors.parse(value)
|
||||
if (colorValue == 0) {
|
||||
throw IllegalArgumentException("Property '$key' has invalid color: '$value'")
|
||||
}
|
||||
|
||||
defaultColors[colorIndex] = colorValue
|
||||
}
|
||||
|
||||
if (!cursorPropExists) {
|
||||
setCursorColorForBackground()
|
||||
}
|
||||
}
|
||||
|
||||
fun setCursorColorForBackground() {
|
||||
val backgroundColor = defaultColors[TextStyle.COLOR_INDEX_BACKGROUND]
|
||||
val brightness = TerminalColors.perceivedBrightness(backgroundColor)
|
||||
if (brightness > 0) {
|
||||
defaultColors[TextStyle.COLOR_INDEX_CURSOR] = if (brightness < 130) {
|
||||
0xffffffff.toInt()
|
||||
} else {
|
||||
0xff000000.toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun reset() {
|
||||
System.arraycopy(DEFAULT_COLORSCHEME, 0, defaultColors, 0, TextStyle.NUM_INDEXED_COLORS)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DEFAULT_COLORSCHEME = longArrayOf(
|
||||
// 16 original colors. First 8 are dim.
|
||||
0xff000000, // black
|
||||
0xffcd0000, // dim red
|
||||
0xff00cd00, // dim green
|
||||
0xffcdcd00, // dim yellow
|
||||
0xff6495ed, // dim blue
|
||||
0xffcd00cd, // dim magenta
|
||||
0xff00cdcd, // dim cyan
|
||||
0xffe5e5e5, // dim white
|
||||
// Second 8 are bright:
|
||||
0xff7f7f7f, // medium grey
|
||||
0xffff0000, // bright red
|
||||
0xff00ff00, // bright green
|
||||
0xffffff00, // bright yellow
|
||||
0xff5c5cff, // light blue
|
||||
0xffff00ff, // bright magenta
|
||||
0xff00ffff, // bright cyan
|
||||
0xffffffffL, // bright white
|
||||
|
||||
// 216 color cube, six shades of each color:
|
||||
0xff000000, 0xff00005f, 0xff000087, 0xff0000af, 0xff0000d7, 0xff0000ff, 0xff005f00, 0xff005f5f, 0xff005f87, 0xff005faf, 0xff005fd7, 0xff005fff,
|
||||
0xff008700, 0xff00875f, 0xff008787, 0xff0087af, 0xff0087d7, 0xff0087ff, 0xff00af00, 0xff00af5f, 0xff00af87, 0xff00afaf, 0xff00afd7, 0xff00afff,
|
||||
0xff00d700, 0xff00d75f, 0xff00d787, 0xff00d7af, 0xff00d7d7, 0xff00d7ff, 0xff00ff00, 0xff00ff5f, 0xff00ff87, 0xff00ffaf, 0xff00ffd7, 0xff00ffff,
|
||||
0xff5f0000, 0xff5f005f, 0xff5f0087, 0xff5f00af, 0xff5f00d7, 0xff5f00ff, 0xff5f5f00, 0xff5f5f5f, 0xff5f5f87, 0xff5f5faf, 0xff5f5fd7, 0xff5f5fff,
|
||||
0xff5f8700, 0xff5f875f, 0xff5f8787, 0xff5f87af, 0xff5f87d7, 0xff5f87ff, 0xff5faf00, 0xff5faf5f, 0xff5faf87, 0xff5fafaf, 0xff5fafd7, 0xff5fafff,
|
||||
0xff5fd700, 0xff5fd75f, 0xff5fd787, 0xff5fd7af, 0xff5fd7d7, 0xff5fd7ff, 0xff5fff00, 0xff5fff5f, 0xff5fff87, 0xff5fffaf, 0xff5fffd7, 0xff5fffff,
|
||||
0xff870000, 0xff87005f, 0xff870087, 0xff8700af, 0xff8700d7, 0xff8700ff, 0xff875f00, 0xff875f5f, 0xff875f87, 0xff875faf, 0xff875fd7, 0xff875fff,
|
||||
0xff878700, 0xff87875f, 0xff878787, 0xff8787af, 0xff8787d7, 0xff8787ff, 0xff87af00, 0xff87af5f, 0xff87af87, 0xff87afaf, 0xff87afd7, 0xff87afff,
|
||||
0xff87d700, 0xff87d75f, 0xff87d787, 0xff87d7af, 0xff87d7d7, 0xff87d7ff, 0xff87ff00, 0xff87ff5f, 0xff87ff87, 0xff87ffaf, 0xff87ffd7, 0xff87ffff,
|
||||
0xffaf0000, 0xffaf005f, 0xffaf0087, 0xffaf00af, 0xffaf00d7, 0xffaf00ff, 0xffaf5f00, 0xffaf5f5f, 0xffaf5f87, 0xffaf5faf, 0xffaf5fd7, 0xffaf5fff,
|
||||
0xffaf8700, 0xffaf875f, 0xffaf8787, 0xffaf87af, 0xffaf87d7, 0xffaf87ff, 0xffafaf00, 0xffafaf5f, 0xffafaf87, 0xffafafaf, 0xffafafd7, 0xffafafff,
|
||||
0xffafd700, 0xffafd75f, 0xffafd787, 0xffafd7af, 0xffafd7d7, 0xffafd7ff, 0xffafff00, 0xffafff5f, 0xffafff87, 0xffafffaf, 0xffafffd7, 0xffafffff,
|
||||
0xffd70000, 0xffd7005f, 0xffd70087, 0xffd700af, 0xffd700d7, 0xffd700ff, 0xffd75f00, 0xffd75f5f, 0xffd75f87, 0xffd75faf, 0xffd75fd7, 0xffd75fff,
|
||||
0xffd78700, 0xffd7875f, 0xffd78787, 0xffd787af, 0xffd787d7, 0xffd787ff, 0xffd7af00, 0xffd7af5f, 0xffd7af87, 0xffd7afaf, 0xffd7afd7, 0xffd7afff,
|
||||
0xffd7d700, 0xffd7d75f, 0xffd7d787, 0xffd7d7af, 0xffd7d7d7, 0xffd7d7ff, 0xffd7ff00, 0xffd7ff5f, 0xffd7ff87, 0xffd7ffaf, 0xffd7ffd7, 0xffd7ffff,
|
||||
0xffff0000, 0xffff005f, 0xffff0087, 0xffff00af, 0xffff00d7, 0xffff00ff, 0xffff5f00, 0xffff5f5f, 0xffff5f87, 0xffff5faf, 0xffff5fd7, 0xffff5fff,
|
||||
0xffff8700, 0xffff875f, 0xffff8787, 0xffff87af, 0xffff87d7, 0xffff87ff, 0xffffaf00, 0xffffaf5f, 0xffffaf87, 0xffffafaf, 0xffffafd7, 0xffffafff,
|
||||
0xffffd700, 0xffffd75f, 0xffffd787, 0xffffd7af, 0xffffd7d7, 0xffffd7ff, 0xffffff00, 0xffffff5f, 0xffffff87, 0xffffffaf, 0xffffffd7, 0xffffffffL,
|
||||
|
||||
// 24 grey scale ramp:
|
||||
0xff080808, 0xff121212, 0xff1c1c1c, 0xff262626, 0xff303030, 0xff3a3a3a, 0xff444444, 0xff4e4e4e, 0xff585858, 0xff626262, 0xff6c6c6c, 0xff767676,
|
||||
0xff808080, 0xff8a8a8a, 0xff949494, 0xff9e9e9e, 0xffa8a8a8, 0xffb2b2b2, 0xffbcbcbc, 0xffc6c6c6, 0xffd0d0d0, 0xffdadada, 0xffe4e4e4, 0xffeeeeee,
|
||||
|
||||
// COLOR_INDEX_DEFAULT_FOREGROUND, COLOR_INDEX_DEFAULT_BACKGROUND and COLOR_INDEX_DEFAULT_CURSOR:
|
||||
0xffffffffL, 0xff000000L, 0xffffffffL
|
||||
).map { it.toInt() }.toIntArray()
|
||||
}
|
||||
}
|
||||
|
||||
/** Current terminal colors (if different from default). */
|
||||
class TerminalColors {
|
||||
|
||||
val currentColors: IntArray = IntArray(TextStyle.NUM_INDEXED_COLORS)
|
||||
|
||||
init {
|
||||
reset()
|
||||
}
|
||||
|
||||
fun reset(index: Int) {
|
||||
currentColors[index] = COLOR_SCHEME.defaultColors[index]
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
System.arraycopy(COLOR_SCHEME.defaultColors, 0, currentColors, 0, TextStyle.NUM_INDEXED_COLORS)
|
||||
}
|
||||
|
||||
fun tryParseColor(intoIndex: Int, textParameter: String) {
|
||||
val c = parse(textParameter)
|
||||
if (c != 0) currentColors[intoIndex] = c
|
||||
}
|
||||
|
||||
companion object {
|
||||
val COLOR_SCHEME = TerminalColorScheme()
|
||||
|
||||
internal fun parse(c: String): Int {
|
||||
return try {
|
||||
val (skipInitial, skipBetween) = when {
|
||||
c[0] == '#' -> 1 to 0
|
||||
c.startsWith("rgb:") -> 4 to 1
|
||||
else -> return 0
|
||||
}
|
||||
val charsForColors = c.length - skipInitial - 2 * skipBetween
|
||||
if (charsForColors % 3 != 0) return 0
|
||||
val componentLength = charsForColors / 3
|
||||
val mult = 255.0 / (2.0.pow(componentLength * 4) - 1)
|
||||
|
||||
var currentPosition = skipInitial
|
||||
val rString = c.substring(currentPosition, currentPosition + componentLength)
|
||||
currentPosition += componentLength + skipBetween
|
||||
val gString = c.substring(currentPosition, currentPosition + componentLength)
|
||||
currentPosition += componentLength + skipBetween
|
||||
val bString = c.substring(currentPosition, currentPosition + componentLength)
|
||||
|
||||
val r = (rString.toInt(16) * mult).toInt()
|
||||
val g = (gString.toInt(16) * mult).toInt()
|
||||
val b = (bString.toInt(16) * mult).toInt()
|
||||
(0xFF shl 24) or (r shl 16) or (g shl 8) or b
|
||||
} catch (_: NumberFormatException) {
|
||||
0
|
||||
} catch (_: IndexOutOfBoundsException) {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
fun perceivedBrightness(color: Int): Int {
|
||||
return floor(
|
||||
sqrt(
|
||||
Color.red(color).toDouble().pow(2) * 0.241 +
|
||||
Color.green(color).toDouble().pow(2) * 0.691 +
|
||||
Color.blue(color).toDouble().pow(2) * 0.068
|
||||
)
|
||||
).toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,559 @@
|
||||
package com.topjohnwu.magisk.terminal
|
||||
|
||||
/**
|
||||
* Implementation of wcwidth(3) for Unicode 15.
|
||||
*
|
||||
* Implementation from https://github.com/jquast/wcwidth but we return 0 for unprintable characters.
|
||||
*
|
||||
* IMPORTANT:
|
||||
* Must be kept in sync with the following:
|
||||
* https://github.com/termux/wcwidth
|
||||
* https://github.com/termux/libandroid-support
|
||||
* https://github.com/termux/termux-packages/tree/master/packages/libandroid-support
|
||||
*/
|
||||
object WcWidth {
|
||||
|
||||
// From https://github.com/jquast/wcwidth/blob/master/wcwidth/table_zero.py
|
||||
// from https://github.com/jquast/wcwidth/pull/64
|
||||
// at commit 1b9b6585b0080ea5cb88dc9815796505724793fe (2022-12-16):
|
||||
private val ZERO_WIDTH = arrayOf(
|
||||
intArrayOf(0x00300, 0x0036f), // Combining Grave Accent ..Combining Latin Small Le
|
||||
intArrayOf(0x00483, 0x00489), // Combining Cyrillic Titlo..Combining Cyrillic Milli
|
||||
intArrayOf(0x00591, 0x005bd), // Hebrew Accent Etnahta ..Hebrew Point Meteg
|
||||
intArrayOf(0x005bf, 0x005bf), // Hebrew Point Rafe ..Hebrew Point Rafe
|
||||
intArrayOf(0x005c1, 0x005c2), // Hebrew Point Shin Dot ..Hebrew Point Sin Dot
|
||||
intArrayOf(0x005c4, 0x005c5), // Hebrew Mark Upper Dot ..Hebrew Mark Lower Dot
|
||||
intArrayOf(0x005c7, 0x005c7), // Hebrew Point Qamats Qata..Hebrew Point Qamats Qata
|
||||
intArrayOf(0x00610, 0x0061a), // Arabic Sign Sallallahou ..Arabic Small Kasra
|
||||
intArrayOf(0x0064b, 0x0065f), // Arabic Fathatan ..Arabic Wavy Hamza Below
|
||||
intArrayOf(0x00670, 0x00670), // Arabic Letter Superscrip..Arabic Letter Superscrip
|
||||
intArrayOf(0x006d6, 0x006dc), // Arabic Small High Ligatu..Arabic Small High Seen
|
||||
intArrayOf(0x006df, 0x006e4), // Arabic Small High Rounde..Arabic Small High Madda
|
||||
intArrayOf(0x006e7, 0x006e8), // Arabic Small High Yeh ..Arabic Small High Noon
|
||||
intArrayOf(0x006ea, 0x006ed), // Arabic Empty Centre Low ..Arabic Small Low Meem
|
||||
intArrayOf(0x00711, 0x00711), // Syriac Letter Superscrip..Syriac Letter Superscrip
|
||||
intArrayOf(0x00730, 0x0074a), // Syriac Pthaha Above ..Syriac Barrekh
|
||||
intArrayOf(0x007a6, 0x007b0), // Thaana Abafili ..Thaana Sukun
|
||||
intArrayOf(0x007eb, 0x007f3), // Nko Combining Short High..Nko Combining Double Dot
|
||||
intArrayOf(0x007fd, 0x007fd), // Nko Dantayalan ..Nko Dantayalan
|
||||
intArrayOf(0x00816, 0x00819), // Samaritan Mark In ..Samaritan Mark Dagesh
|
||||
intArrayOf(0x0081b, 0x00823), // Samaritan Mark Epentheti..Samaritan Vowel Sign A
|
||||
intArrayOf(0x00825, 0x00827), // Samaritan Vowel Sign Sho..Samaritan Vowel Sign U
|
||||
intArrayOf(0x00829, 0x0082d), // Samaritan Vowel Sign Lon..Samaritan Mark Nequdaa
|
||||
intArrayOf(0x00859, 0x0085b), // Mandaic Affrication Mark..Mandaic Gemination Mark
|
||||
intArrayOf(0x00898, 0x0089f), // Arabic Small High Word A..Arabic Half Madda Over M
|
||||
intArrayOf(0x008ca, 0x008e1), // Arabic Small High Farsi ..Arabic Small High Sign S
|
||||
intArrayOf(0x008e3, 0x00902), // Arabic Turned Damma Belo..Devanagari Sign Anusvara
|
||||
intArrayOf(0x0093a, 0x0093a), // Devanagari Vowel Sign Oe..Devanagari Vowel Sign Oe
|
||||
intArrayOf(0x0093c, 0x0093c), // Devanagari Sign Nukta ..Devanagari Sign Nukta
|
||||
intArrayOf(0x00941, 0x00948), // Devanagari Vowel Sign U ..Devanagari Vowel Sign Ai
|
||||
intArrayOf(0x0094d, 0x0094d), // Devanagari Sign Virama ..Devanagari Sign Virama
|
||||
intArrayOf(0x00951, 0x00957), // Devanagari Stress Sign U..Devanagari Vowel Sign Uu
|
||||
intArrayOf(0x00962, 0x00963), // Devanagari Vowel Sign Vo..Devanagari Vowel Sign Vo
|
||||
intArrayOf(0x00981, 0x00981), // Bengali Sign Candrabindu..Bengali Sign Candrabindu
|
||||
intArrayOf(0x009bc, 0x009bc), // Bengali Sign Nukta ..Bengali Sign Nukta
|
||||
intArrayOf(0x009c1, 0x009c4), // Bengali Vowel Sign U ..Bengali Vowel Sign Vocal
|
||||
intArrayOf(0x009cd, 0x009cd), // Bengali Sign Virama ..Bengali Sign Virama
|
||||
intArrayOf(0x009e2, 0x009e3), // Bengali Vowel Sign Vocal..Bengali Vowel Sign Vocal
|
||||
intArrayOf(0x009fe, 0x009fe), // Bengali Sandhi Mark ..Bengali Sandhi Mark
|
||||
intArrayOf(0x00a01, 0x00a02), // Gurmukhi Sign Adak Bindi..Gurmukhi Sign Bindi
|
||||
intArrayOf(0x00a3c, 0x00a3c), // Gurmukhi Sign Nukta ..Gurmukhi Sign Nukta
|
||||
intArrayOf(0x00a41, 0x00a42), // Gurmukhi Vowel Sign U ..Gurmukhi Vowel Sign Uu
|
||||
intArrayOf(0x00a47, 0x00a48), // Gurmukhi Vowel Sign Ee ..Gurmukhi Vowel Sign Ai
|
||||
intArrayOf(0x00a4b, 0x00a4d), // Gurmukhi Vowel Sign Oo ..Gurmukhi Sign Virama
|
||||
intArrayOf(0x00a51, 0x00a51), // Gurmukhi Sign Udaat ..Gurmukhi Sign Udaat
|
||||
intArrayOf(0x00a70, 0x00a71), // Gurmukhi Tippi ..Gurmukhi Addak
|
||||
intArrayOf(0x00a75, 0x00a75), // Gurmukhi Sign Yakash ..Gurmukhi Sign Yakash
|
||||
intArrayOf(0x00a81, 0x00a82), // Gujarati Sign Candrabind..Gujarati Sign Anusvara
|
||||
intArrayOf(0x00abc, 0x00abc), // Gujarati Sign Nukta ..Gujarati Sign Nukta
|
||||
intArrayOf(0x00ac1, 0x00ac5), // Gujarati Vowel Sign U ..Gujarati Vowel Sign Cand
|
||||
intArrayOf(0x00ac7, 0x00ac8), // Gujarati Vowel Sign E ..Gujarati Vowel Sign Ai
|
||||
intArrayOf(0x00acd, 0x00acd), // Gujarati Sign Virama ..Gujarati Sign Virama
|
||||
intArrayOf(0x00ae2, 0x00ae3), // Gujarati Vowel Sign Voca..Gujarati Vowel Sign Voca
|
||||
intArrayOf(0x00afa, 0x00aff), // Gujarati Sign Sukun ..Gujarati Sign Two-circle
|
||||
intArrayOf(0x00b01, 0x00b01), // Oriya Sign Candrabindu ..Oriya Sign Candrabindu
|
||||
intArrayOf(0x00b3c, 0x00b3c), // Oriya Sign Nukta ..Oriya Sign Nukta
|
||||
intArrayOf(0x00b3f, 0x00b3f), // Oriya Vowel Sign I ..Oriya Vowel Sign I
|
||||
intArrayOf(0x00b41, 0x00b44), // Oriya Vowel Sign U ..Oriya Vowel Sign Vocalic
|
||||
intArrayOf(0x00b4d, 0x00b4d), // Oriya Sign Virama ..Oriya Sign Virama
|
||||
intArrayOf(0x00b55, 0x00b56), // Oriya Sign Overline ..Oriya Ai Length Mark
|
||||
intArrayOf(0x00b62, 0x00b63), // Oriya Vowel Sign Vocalic..Oriya Vowel Sign Vocalic
|
||||
intArrayOf(0x00b82, 0x00b82), // Tamil Sign Anusvara ..Tamil Sign Anusvara
|
||||
intArrayOf(0x00bc0, 0x00bc0), // Tamil Vowel Sign Ii ..Tamil Vowel Sign Ii
|
||||
intArrayOf(0x00bcd, 0x00bcd), // Tamil Sign Virama ..Tamil Sign Virama
|
||||
intArrayOf(0x00c00, 0x00c00), // Telugu Sign Combining Ca..Telugu Sign Combining Ca
|
||||
intArrayOf(0x00c04, 0x00c04), // Telugu Sign Combining An..Telugu Sign Combining An
|
||||
intArrayOf(0x00c3c, 0x00c3c), // Telugu Sign Nukta ..Telugu Sign Nukta
|
||||
intArrayOf(0x00c3e, 0x00c40), // Telugu Vowel Sign Aa ..Telugu Vowel Sign Ii
|
||||
intArrayOf(0x00c46, 0x00c48), // Telugu Vowel Sign E ..Telugu Vowel Sign Ai
|
||||
intArrayOf(0x00c4a, 0x00c4d), // Telugu Vowel Sign O ..Telugu Sign Virama
|
||||
intArrayOf(0x00c55, 0x00c56), // Telugu Length Mark ..Telugu Ai Length Mark
|
||||
intArrayOf(0x00c62, 0x00c63), // Telugu Vowel Sign Vocali..Telugu Vowel Sign Vocali
|
||||
intArrayOf(0x00c81, 0x00c81), // Kannada Sign Candrabindu..Kannada Sign Candrabindu
|
||||
intArrayOf(0x00cbc, 0x00cbc), // Kannada Sign Nukta ..Kannada Sign Nukta
|
||||
intArrayOf(0x00cbf, 0x00cbf), // Kannada Vowel Sign I ..Kannada Vowel Sign I
|
||||
intArrayOf(0x00cc6, 0x00cc6), // Kannada Vowel Sign E ..Kannada Vowel Sign E
|
||||
intArrayOf(0x00ccc, 0x00ccd), // Kannada Vowel Sign Au ..Kannada Sign Virama
|
||||
intArrayOf(0x00ce2, 0x00ce3), // Kannada Vowel Sign Vocal..Kannada Vowel Sign Vocal
|
||||
intArrayOf(0x00d00, 0x00d01), // Malayalam Sign Combining..Malayalam Sign Candrabin
|
||||
intArrayOf(0x00d3b, 0x00d3c), // Malayalam Sign Vertical ..Malayalam Sign Circular
|
||||
intArrayOf(0x00d41, 0x00d44), // Malayalam Vowel Sign U ..Malayalam Vowel Sign Voc
|
||||
intArrayOf(0x00d4d, 0x00d4d), // Malayalam Sign Virama ..Malayalam Sign Virama
|
||||
intArrayOf(0x00d62, 0x00d63), // Malayalam Vowel Sign Voc..Malayalam Vowel Sign Voc
|
||||
intArrayOf(0x00d81, 0x00d81), // Sinhala Sign Candrabindu..Sinhala Sign Candrabindu
|
||||
intArrayOf(0x00dca, 0x00dca), // Sinhala Sign Al-lakuna ..Sinhala Sign Al-lakuna
|
||||
intArrayOf(0x00dd2, 0x00dd4), // Sinhala Vowel Sign Ketti..Sinhala Vowel Sign Ketti
|
||||
intArrayOf(0x00dd6, 0x00dd6), // Sinhala Vowel Sign Diga ..Sinhala Vowel Sign Diga
|
||||
intArrayOf(0x00e31, 0x00e31), // Thai Character Mai Han-a..Thai Character Mai Han-a
|
||||
intArrayOf(0x00e34, 0x00e3a), // Thai Character Sara I ..Thai Character Phinthu
|
||||
intArrayOf(0x00e47, 0x00e4e), // Thai Character Maitaikhu..Thai Character Yamakkan
|
||||
intArrayOf(0x00eb1, 0x00eb1), // Lao Vowel Sign Mai Kan ..Lao Vowel Sign Mai Kan
|
||||
intArrayOf(0x00eb4, 0x00ebc), // Lao Vowel Sign I ..Lao Semivowel Sign Lo
|
||||
intArrayOf(0x00ec8, 0x00ece), // Lao Tone Mai Ek ..(nil)
|
||||
intArrayOf(0x00f18, 0x00f19), // Tibetan Astrological Sig..Tibetan Astrological Sig
|
||||
intArrayOf(0x00f35, 0x00f35), // Tibetan Mark Ngas Bzung ..Tibetan Mark Ngas Bzung
|
||||
intArrayOf(0x00f37, 0x00f37), // Tibetan Mark Ngas Bzung ..Tibetan Mark Ngas Bzung
|
||||
intArrayOf(0x00f39, 0x00f39), // Tibetan Mark Tsa -phru ..Tibetan Mark Tsa -phru
|
||||
intArrayOf(0x00f71, 0x00f7e), // Tibetan Vowel Sign Aa ..Tibetan Sign Rjes Su Nga
|
||||
intArrayOf(0x00f80, 0x00f84), // Tibetan Vowel Sign Rever..Tibetan Mark Halanta
|
||||
intArrayOf(0x00f86, 0x00f87), // Tibetan Sign Lci Rtags ..Tibetan Sign Yang Rtags
|
||||
intArrayOf(0x00f8d, 0x00f97), // Tibetan Subjoined Sign L..Tibetan Subjoined Letter
|
||||
intArrayOf(0x00f99, 0x00fbc), // Tibetan Subjoined Letter..Tibetan Subjoined Letter
|
||||
intArrayOf(0x00fc6, 0x00fc6), // Tibetan Symbol Padma Gda..Tibetan Symbol Padma Gda
|
||||
intArrayOf(0x0102d, 0x01030), // Myanmar Vowel Sign I ..Myanmar Vowel Sign Uu
|
||||
intArrayOf(0x01032, 0x01037), // Myanmar Vowel Sign Ai ..Myanmar Sign Dot Below
|
||||
intArrayOf(0x01039, 0x0103a), // Myanmar Sign Virama ..Myanmar Sign Asat
|
||||
intArrayOf(0x0103d, 0x0103e), // Myanmar Consonant Sign M..Myanmar Consonant Sign M
|
||||
intArrayOf(0x01058, 0x01059), // Myanmar Vowel Sign Vocal..Myanmar Vowel Sign Vocal
|
||||
intArrayOf(0x0105e, 0x01060), // Myanmar Consonant Sign M..Myanmar Consonant Sign M
|
||||
intArrayOf(0x01071, 0x01074), // Myanmar Vowel Sign Geba ..Myanmar Vowel Sign Kayah
|
||||
intArrayOf(0x01082, 0x01082), // Myanmar Consonant Sign S..Myanmar Consonant Sign S
|
||||
intArrayOf(0x01085, 0x01086), // Myanmar Vowel Sign Shan ..Myanmar Vowel Sign Shan
|
||||
intArrayOf(0x0108d, 0x0108d), // Myanmar Sign Shan Counci..Myanmar Sign Shan Counci
|
||||
intArrayOf(0x0109d, 0x0109d), // Myanmar Vowel Sign Aiton..Myanmar Vowel Sign Aiton
|
||||
intArrayOf(0x0135d, 0x0135f), // Ethiopic Combining Gemin..Ethiopic Combining Gemin
|
||||
intArrayOf(0x01712, 0x01714), // Tagalog Vowel Sign I ..Tagalog Sign Virama
|
||||
intArrayOf(0x01732, 0x01733), // Hanunoo Vowel Sign I ..Hanunoo Vowel Sign U
|
||||
intArrayOf(0x01752, 0x01753), // Buhid Vowel Sign I ..Buhid Vowel Sign U
|
||||
intArrayOf(0x01772, 0x01773), // Tagbanwa Vowel Sign I ..Tagbanwa Vowel Sign U
|
||||
intArrayOf(0x017b4, 0x017b5), // Khmer Vowel Inherent Aq ..Khmer Vowel Inherent Aa
|
||||
intArrayOf(0x017b7, 0x017bd), // Khmer Vowel Sign I ..Khmer Vowel Sign Ua
|
||||
intArrayOf(0x017c6, 0x017c6), // Khmer Sign Nikahit ..Khmer Sign Nikahit
|
||||
intArrayOf(0x017c9, 0x017d3), // Khmer Sign Muusikatoan ..Khmer Sign Bathamasat
|
||||
intArrayOf(0x017dd, 0x017dd), // Khmer Sign Atthacan ..Khmer Sign Atthacan
|
||||
intArrayOf(0x0180b, 0x0180d), // Mongolian Free Variation..Mongolian Free Variation
|
||||
intArrayOf(0x0180f, 0x0180f), // Mongolian Free Variation..Mongolian Free Variation
|
||||
intArrayOf(0x01885, 0x01886), // Mongolian Letter Ali Gal..Mongolian Letter Ali Gal
|
||||
intArrayOf(0x018a9, 0x018a9), // Mongolian Letter Ali Gal..Mongolian Letter Ali Gal
|
||||
intArrayOf(0x01920, 0x01922), // Limbu Vowel Sign A ..Limbu Vowel Sign U
|
||||
intArrayOf(0x01927, 0x01928), // Limbu Vowel Sign E ..Limbu Vowel Sign O
|
||||
intArrayOf(0x01932, 0x01932), // Limbu Small Letter Anusv..Limbu Small Letter Anusv
|
||||
intArrayOf(0x01939, 0x0193b), // Limbu Sign Mukphreng ..Limbu Sign Sa-i
|
||||
intArrayOf(0x01a17, 0x01a18), // Buginese Vowel Sign I ..Buginese Vowel Sign U
|
||||
intArrayOf(0x01a1b, 0x01a1b), // Buginese Vowel Sign Ae ..Buginese Vowel Sign Ae
|
||||
intArrayOf(0x01a56, 0x01a56), // Tai Tham Consonant Sign ..Tai Tham Consonant Sign
|
||||
intArrayOf(0x01a58, 0x01a5e), // Tai Tham Sign Mai Kang L..Tai Tham Consonant Sign
|
||||
intArrayOf(0x01a60, 0x01a60), // Tai Tham Sign Sakot ..Tai Tham Sign Sakot
|
||||
intArrayOf(0x01a62, 0x01a62), // Tai Tham Vowel Sign Mai ..Tai Tham Vowel Sign Mai
|
||||
intArrayOf(0x01a65, 0x01a6c), // Tai Tham Vowel Sign I ..Tai Tham Vowel Sign Oa B
|
||||
intArrayOf(0x01a73, 0x01a7c), // Tai Tham Vowel Sign Oa A..Tai Tham Sign Khuen-lue
|
||||
intArrayOf(0x01a7f, 0x01a7f), // Tai Tham Combining Crypt..Tai Tham Combining Crypt
|
||||
intArrayOf(0x01ab0, 0x01ace), // Combining Doubled Circum..Combining Latin Small Le
|
||||
intArrayOf(0x01b00, 0x01b03), // Balinese Sign Ulu Ricem ..Balinese Sign Surang
|
||||
intArrayOf(0x01b34, 0x01b34), // Balinese Sign Rerekan ..Balinese Sign Rerekan
|
||||
intArrayOf(0x01b36, 0x01b3a), // Balinese Vowel Sign Ulu ..Balinese Vowel Sign Ra R
|
||||
intArrayOf(0x01b3c, 0x01b3c), // Balinese Vowel Sign La L..Balinese Vowel Sign La L
|
||||
intArrayOf(0x01b42, 0x01b42), // Balinese Vowel Sign Pepe..Balinese Vowel Sign Pepe
|
||||
intArrayOf(0x01b6b, 0x01b73), // Balinese Musical Symbol ..Balinese Musical Symbol
|
||||
intArrayOf(0x01b80, 0x01b81), // Sundanese Sign Panyecek ..Sundanese Sign Panglayar
|
||||
intArrayOf(0x01ba2, 0x01ba5), // Sundanese Consonant Sign..Sundanese Vowel Sign Pan
|
||||
intArrayOf(0x01ba8, 0x01ba9), // Sundanese Vowel Sign Pam..Sundanese Vowel Sign Pan
|
||||
intArrayOf(0x01bab, 0x01bad), // Sundanese Sign Virama ..Sundanese Consonant Sign
|
||||
intArrayOf(0x01be6, 0x01be6), // Batak Sign Tompi ..Batak Sign Tompi
|
||||
intArrayOf(0x01be8, 0x01be9), // Batak Vowel Sign Pakpak ..Batak Vowel Sign Ee
|
||||
intArrayOf(0x01bed, 0x01bed), // Batak Vowel Sign Karo O ..Batak Vowel Sign Karo O
|
||||
intArrayOf(0x01bef, 0x01bf1), // Batak Vowel Sign U For S..Batak Consonant Sign H
|
||||
intArrayOf(0x01c2c, 0x01c33), // Lepcha Vowel Sign E ..Lepcha Consonant Sign T
|
||||
intArrayOf(0x01c36, 0x01c37), // Lepcha Sign Ran ..Lepcha Sign Nukta
|
||||
intArrayOf(0x01cd0, 0x01cd2), // Vedic Tone Karshana ..Vedic Tone Prenkha
|
||||
intArrayOf(0x01cd4, 0x01ce0), // Vedic Sign Yajurvedic Mi..Vedic Tone Rigvedic Kash
|
||||
intArrayOf(0x01ce2, 0x01ce8), // Vedic Sign Visarga Svari..Vedic Sign Visarga Anuda
|
||||
intArrayOf(0x01ced, 0x01ced), // Vedic Sign Tiryak ..Vedic Sign Tiryak
|
||||
intArrayOf(0x01cf4, 0x01cf4), // Vedic Tone Candra Above ..Vedic Tone Candra Above
|
||||
intArrayOf(0x01cf8, 0x01cf9), // Vedic Tone Ring Above ..Vedic Tone Double Ring A
|
||||
intArrayOf(0x01dc0, 0x01dff), // Combining Dotted Grave A..Combining Right Arrowhea
|
||||
intArrayOf(0x020d0, 0x020f0), // Combining Left Harpoon A..Combining Asterisk Above
|
||||
intArrayOf(0x02cef, 0x02cf1), // Coptic Combining Ni Abov..Coptic Combining Spiritu
|
||||
intArrayOf(0x02d7f, 0x02d7f), // Tifinagh Consonant Joine..Tifinagh Consonant Joine
|
||||
intArrayOf(0x02de0, 0x02dff), // Combining Cyrillic Lette..Combining Cyrillic Lette
|
||||
intArrayOf(0x0302a, 0x0302d), // Ideographic Level Tone M..Ideographic Entering Ton
|
||||
intArrayOf(0x03099, 0x0309a), // Combining Katakana-hirag..Combining Katakana-hirag
|
||||
intArrayOf(0x0a66f, 0x0a672), // Combining Cyrillic Vzmet..Combining Cyrillic Thous
|
||||
intArrayOf(0x0a674, 0x0a67d), // Combining Cyrillic Lette..Combining Cyrillic Payer
|
||||
intArrayOf(0x0a69e, 0x0a69f), // Combining Cyrillic Lette..Combining Cyrillic Lette
|
||||
intArrayOf(0x0a6f0, 0x0a6f1), // Bamum Combining Mark Koq..Bamum Combining Mark Tuk
|
||||
intArrayOf(0x0a802, 0x0a802), // Syloti Nagri Sign Dvisva..Syloti Nagri Sign Dvisva
|
||||
intArrayOf(0x0a806, 0x0a806), // Syloti Nagri Sign Hasant..Syloti Nagri Sign Hasant
|
||||
intArrayOf(0x0a80b, 0x0a80b), // Syloti Nagri Sign Anusva..Syloti Nagri Sign Anusva
|
||||
intArrayOf(0x0a825, 0x0a826), // Syloti Nagri Vowel Sign ..Syloti Nagri Vowel Sign
|
||||
intArrayOf(0x0a82c, 0x0a82c), // Syloti Nagri Sign Altern..Syloti Nagri Sign Altern
|
||||
intArrayOf(0x0a8c4, 0x0a8c5), // Saurashtra Sign Virama ..Saurashtra Sign Candrabi
|
||||
intArrayOf(0x0a8e0, 0x0a8f1), // Combining Devanagari Dig..Combining Devanagari Sig
|
||||
intArrayOf(0x0a8ff, 0x0a8ff), // Devanagari Vowel Sign Ay..Devanagari Vowel Sign Ay
|
||||
intArrayOf(0x0a926, 0x0a92d), // Kayah Li Vowel Ue ..Kayah Li Tone Calya Plop
|
||||
intArrayOf(0x0a947, 0x0a951), // Rejang Vowel Sign I ..Rejang Consonant Sign R
|
||||
intArrayOf(0x0a980, 0x0a982), // Javanese Sign Panyangga ..Javanese Sign Layar
|
||||
intArrayOf(0x0a9b3, 0x0a9b3), // Javanese Sign Cecak Telu..Javanese Sign Cecak Telu
|
||||
intArrayOf(0x0a9b6, 0x0a9b9), // Javanese Vowel Sign Wulu..Javanese Vowel Sign Suku
|
||||
intArrayOf(0x0a9bc, 0x0a9bd), // Javanese Vowel Sign Pepe..Javanese Consonant Sign
|
||||
intArrayOf(0x0a9e5, 0x0a9e5), // Myanmar Sign Shan Saw ..Myanmar Sign Shan Saw
|
||||
intArrayOf(0x0aa29, 0x0aa2e), // Cham Vowel Sign Aa ..Cham Vowel Sign Oe
|
||||
intArrayOf(0x0aa31, 0x0aa32), // Cham Vowel Sign Au ..Cham Vowel Sign Ue
|
||||
intArrayOf(0x0aa35, 0x0aa36), // Cham Consonant Sign La ..Cham Consonant Sign Wa
|
||||
intArrayOf(0x0aa43, 0x0aa43), // Cham Consonant Sign Fina..Cham Consonant Sign Fina
|
||||
intArrayOf(0x0aa4c, 0x0aa4c), // Cham Consonant Sign Fina..Cham Consonant Sign Fina
|
||||
intArrayOf(0x0aa7c, 0x0aa7c), // Myanmar Sign Tai Laing T..Myanmar Sign Tai Laing T
|
||||
intArrayOf(0x0aab0, 0x0aab0), // Tai Viet Mai Kang ..Tai Viet Mai Kang
|
||||
intArrayOf(0x0aab2, 0x0aab4), // Tai Viet Vowel I ..Tai Viet Vowel U
|
||||
intArrayOf(0x0aab7, 0x0aab8), // Tai Viet Mai Khit ..Tai Viet Vowel Ia
|
||||
intArrayOf(0x0aabe, 0x0aabf), // Tai Viet Vowel Am ..Tai Viet Tone Mai Ek
|
||||
intArrayOf(0x0aac1, 0x0aac1), // Tai Viet Tone Mai Tho ..Tai Viet Tone Mai Tho
|
||||
intArrayOf(0x0aaec, 0x0aaed), // Meetei Mayek Vowel Sign ..Meetei Mayek Vowel Sign
|
||||
intArrayOf(0x0aaf6, 0x0aaf6), // Meetei Mayek Virama ..Meetei Mayek Virama
|
||||
intArrayOf(0x0abe5, 0x0abe5), // Meetei Mayek Vowel Sign ..Meetei Mayek Vowel Sign
|
||||
intArrayOf(0x0abe8, 0x0abe8), // Meetei Mayek Vowel Sign ..Meetei Mayek Vowel Sign
|
||||
intArrayOf(0x0abed, 0x0abed), // Meetei Mayek Apun Iyek ..Meetei Mayek Apun Iyek
|
||||
intArrayOf(0x0fb1e, 0x0fb1e), // Hebrew Point Judeo-spani..Hebrew Point Judeo-spani
|
||||
intArrayOf(0x0fe00, 0x0fe0f), // Variation Selector-1 ..Variation Selector-16
|
||||
intArrayOf(0x0fe20, 0x0fe2f), // Combining Ligature Left ..Combining Cyrillic Titlo
|
||||
intArrayOf(0x101fd, 0x101fd), // Phaistos Disc Sign Combi..Phaistos Disc Sign Combi
|
||||
intArrayOf(0x102e0, 0x102e0), // Coptic Epact Thousands M..Coptic Epact Thousands M
|
||||
intArrayOf(0x10376, 0x1037a), // Combining Old Permic Let..Combining Old Permic Let
|
||||
intArrayOf(0x10a01, 0x10a03), // Kharoshthi Vowel Sign I ..Kharoshthi Vowel Sign Vo
|
||||
intArrayOf(0x10a05, 0x10a06), // Kharoshthi Vowel Sign E ..Kharoshthi Vowel Sign O
|
||||
intArrayOf(0x10a0c, 0x10a0f), // Kharoshthi Vowel Length ..Kharoshthi Sign Visarga
|
||||
intArrayOf(0x10a38, 0x10a3a), // Kharoshthi Sign Bar Abov..Kharoshthi Sign Dot Belo
|
||||
intArrayOf(0x10a3f, 0x10a3f), // Kharoshthi Virama ..Kharoshthi Virama
|
||||
intArrayOf(0x10ae5, 0x10ae6), // Manichaean Abbreviation ..Manichaean Abbreviation
|
||||
intArrayOf(0x10d24, 0x10d27), // Hanifi Rohingya Sign Har..Hanifi Rohingya Sign Tas
|
||||
intArrayOf(0x10eab, 0x10eac), // Yezidi Combining Hamza M..Yezidi Combining Madda M
|
||||
intArrayOf(0x10efd, 0x10eff), // (nil) ..(nil)
|
||||
intArrayOf(0x10f46, 0x10f50), // Sogdian Combining Dot Be..Sogdian Combining Stroke
|
||||
intArrayOf(0x10f82, 0x10f85), // Old Uyghur Combining Dot..Old Uyghur Combining Two
|
||||
intArrayOf(0x11001, 0x11001), // Brahmi Sign Anusvara ..Brahmi Sign Anusvara
|
||||
intArrayOf(0x11038, 0x11046), // Brahmi Vowel Sign Aa ..Brahmi Virama
|
||||
intArrayOf(0x11070, 0x11070), // Brahmi Sign Old Tamil Vi..Brahmi Sign Old Tamil Vi
|
||||
intArrayOf(0x11073, 0x11074), // Brahmi Vowel Sign Old Ta..Brahmi Vowel Sign Old Ta
|
||||
intArrayOf(0x1107f, 0x11081), // Brahmi Number Joiner ..Kaithi Sign Anusvara
|
||||
intArrayOf(0x110b3, 0x110b6), // Kaithi Vowel Sign U ..Kaithi Vowel Sign Ai
|
||||
intArrayOf(0x110b9, 0x110ba), // Kaithi Sign Virama ..Kaithi Sign Nukta
|
||||
intArrayOf(0x110c2, 0x110c2), // Kaithi Vowel Sign Vocali..Kaithi Vowel Sign Vocali
|
||||
intArrayOf(0x11100, 0x11102), // Chakma Sign Candrabindu ..Chakma Sign Visarga
|
||||
intArrayOf(0x11127, 0x1112b), // Chakma Vowel Sign A ..Chakma Vowel Sign Uu
|
||||
intArrayOf(0x1112d, 0x11134), // Chakma Vowel Sign Ai ..Chakma Maayyaa
|
||||
intArrayOf(0x11173, 0x11173), // Mahajani Sign Nukta ..Mahajani Sign Nukta
|
||||
intArrayOf(0x11180, 0x11181), // Sharada Sign Candrabindu..Sharada Sign Anusvara
|
||||
intArrayOf(0x111b6, 0x111be), // Sharada Vowel Sign U ..Sharada Vowel Sign O
|
||||
intArrayOf(0x111c9, 0x111cc), // Sharada Sandhi Mark ..Sharada Extra Short Vowe
|
||||
intArrayOf(0x111cf, 0x111cf), // Sharada Sign Inverted Ca..Sharada Sign Inverted Ca
|
||||
intArrayOf(0x1122f, 0x11231), // Khojki Vowel Sign U ..Khojki Vowel Sign Ai
|
||||
intArrayOf(0x11234, 0x11234), // Khojki Sign Anusvara ..Khojki Sign Anusvara
|
||||
intArrayOf(0x11236, 0x11237), // Khojki Sign Nukta ..Khojki Sign Shadda
|
||||
intArrayOf(0x1123e, 0x1123e), // Khojki Sign Sukun ..Khojki Sign Sukun
|
||||
intArrayOf(0x11241, 0x11241), // (nil) ..(nil)
|
||||
intArrayOf(0x112df, 0x112df), // Khudawadi Sign Anusvara ..Khudawadi Sign Anusvara
|
||||
intArrayOf(0x112e3, 0x112ea), // Khudawadi Vowel Sign U ..Khudawadi Sign Virama
|
||||
intArrayOf(0x11300, 0x11301), // Grantha Sign Combining A..Grantha Sign Candrabindu
|
||||
intArrayOf(0x1133b, 0x1133c), // Combining Bindu Below ..Grantha Sign Nukta
|
||||
intArrayOf(0x11340, 0x11340), // Grantha Vowel Sign Ii ..Grantha Vowel Sign Ii
|
||||
intArrayOf(0x11366, 0x1136c), // Combining Grantha Digit ..Combining Grantha Digit
|
||||
intArrayOf(0x11370, 0x11374), // Combining Grantha Letter..Combining Grantha Letter
|
||||
intArrayOf(0x11438, 0x1143f), // Newa Vowel Sign U ..Newa Vowel Sign Ai
|
||||
intArrayOf(0x11442, 0x11444), // Newa Sign Virama ..Newa Sign Anusvara
|
||||
intArrayOf(0x11446, 0x11446), // Newa Sign Nukta ..Newa Sign Nukta
|
||||
intArrayOf(0x1145e, 0x1145e), // Newa Sandhi Mark ..Newa Sandhi Mark
|
||||
intArrayOf(0x114b3, 0x114b8), // Tirhuta Vowel Sign U ..Tirhuta Vowel Sign Vocal
|
||||
intArrayOf(0x114ba, 0x114ba), // Tirhuta Vowel Sign Short..Tirhuta Vowel Sign Short
|
||||
intArrayOf(0x114bf, 0x114c0), // Tirhuta Sign Candrabindu..Tirhuta Sign Anusvara
|
||||
intArrayOf(0x114c2, 0x114c3), // Tirhuta Sign Virama ..Tirhuta Sign Nukta
|
||||
intArrayOf(0x115b2, 0x115b5), // Siddham Vowel Sign U ..Siddham Vowel Sign Vocal
|
||||
intArrayOf(0x115bc, 0x115bd), // Siddham Sign Candrabindu..Siddham Sign Anusvara
|
||||
intArrayOf(0x115bf, 0x115c0), // Siddham Sign Virama ..Siddham Sign Nukta
|
||||
intArrayOf(0x115dc, 0x115dd), // Siddham Vowel Sign Alter..Siddham Vowel Sign Alter
|
||||
intArrayOf(0x11633, 0x1163a), // Modi Vowel Sign U ..Modi Vowel Sign Ai
|
||||
intArrayOf(0x1163d, 0x1163d), // Modi Sign Anusvara ..Modi Sign Anusvara
|
||||
intArrayOf(0x1163f, 0x11640), // Modi Sign Virama ..Modi Sign Ardhacandra
|
||||
intArrayOf(0x116ab, 0x116ab), // Takri Sign Anusvara ..Takri Sign Anusvara
|
||||
intArrayOf(0x116ad, 0x116ad), // Takri Vowel Sign Aa ..Takri Vowel Sign Aa
|
||||
intArrayOf(0x116b0, 0x116b5), // Takri Vowel Sign U ..Takri Vowel Sign Au
|
||||
intArrayOf(0x116b7, 0x116b7), // Takri Sign Nukta ..Takri Sign Nukta
|
||||
intArrayOf(0x1171d, 0x1171f), // Ahom Consonant Sign Medi..Ahom Consonant Sign Medi
|
||||
intArrayOf(0x11722, 0x11725), // Ahom Vowel Sign I ..Ahom Vowel Sign Uu
|
||||
intArrayOf(0x11727, 0x1172b), // Ahom Vowel Sign Aw ..Ahom Sign Killer
|
||||
intArrayOf(0x1182f, 0x11837), // Dogra Vowel Sign U ..Dogra Sign Anusvara
|
||||
intArrayOf(0x11839, 0x1183a), // Dogra Sign Virama ..Dogra Sign Nukta
|
||||
intArrayOf(0x1193b, 0x1193c), // Dives Akuru Sign Anusvar..Dives Akuru Sign Candrab
|
||||
intArrayOf(0x1193e, 0x1193e), // Dives Akuru Virama ..Dives Akuru Virama
|
||||
intArrayOf(0x11943, 0x11943), // Dives Akuru Sign Nukta ..Dives Akuru Sign Nukta
|
||||
intArrayOf(0x119d4, 0x119d7), // Nandinagari Vowel Sign U..Nandinagari Vowel Sign V
|
||||
intArrayOf(0x119da, 0x119db), // Nandinagari Vowel Sign E..Nandinagari Vowel Sign A
|
||||
intArrayOf(0x119e0, 0x119e0), // Nandinagari Sign Virama ..Nandinagari Sign Virama
|
||||
intArrayOf(0x11a01, 0x11a0a), // Zanabazar Square Vowel S..Zanabazar Square Vowel L
|
||||
intArrayOf(0x11a33, 0x11a38), // Zanabazar Square Final C..Zanabazar Square Sign An
|
||||
intArrayOf(0x11a3b, 0x11a3e), // Zanabazar Square Cluster..Zanabazar Square Cluster
|
||||
intArrayOf(0x11a47, 0x11a47), // Zanabazar Square Subjoin..Zanabazar Square Subjoin
|
||||
intArrayOf(0x11a51, 0x11a56), // Soyombo Vowel Sign I ..Soyombo Vowel Sign Oe
|
||||
intArrayOf(0x11a59, 0x11a5b), // Soyombo Vowel Sign Vocal..Soyombo Vowel Length Mar
|
||||
intArrayOf(0x11a8a, 0x11a96), // Soyombo Final Consonant ..Soyombo Sign Anusvara
|
||||
intArrayOf(0x11a98, 0x11a99), // Soyombo Gemination Mark ..Soyombo Subjoiner
|
||||
intArrayOf(0x11c30, 0x11c36), // Bhaiksuki Vowel Sign I ..Bhaiksuki Vowel Sign Voc
|
||||
intArrayOf(0x11c38, 0x11c3d), // Bhaiksuki Vowel Sign E ..Bhaiksuki Sign Anusvara
|
||||
intArrayOf(0x11c3f, 0x11c3f), // Bhaiksuki Sign Virama ..Bhaiksuki Sign Virama
|
||||
intArrayOf(0x11c92, 0x11ca7), // Marchen Subjoined Letter..Marchen Subjoined Letter
|
||||
intArrayOf(0x11caa, 0x11cb0), // Marchen Subjoined Letter..Marchen Vowel Sign Aa
|
||||
intArrayOf(0x11cb2, 0x11cb3), // Marchen Vowel Sign U ..Marchen Vowel Sign E
|
||||
intArrayOf(0x11cb5, 0x11cb6), // Marchen Sign Anusvara ..Marchen Sign Candrabindu
|
||||
intArrayOf(0x11d31, 0x11d36), // Masaram Gondi Vowel Sign..Masaram Gondi Vowel Sign
|
||||
intArrayOf(0x11d3a, 0x11d3a), // Masaram Gondi Vowel Sign..Masaram Gondi Vowel Sign
|
||||
intArrayOf(0x11d3c, 0x11d3d), // Masaram Gondi Vowel Sign..Masaram Gondi Vowel Sign
|
||||
intArrayOf(0x11d3f, 0x11d45), // Masaram Gondi Vowel Sign..Masaram Gondi Virama
|
||||
intArrayOf(0x11d47, 0x11d47), // Masaram Gondi Ra-kara ..Masaram Gondi Ra-kara
|
||||
intArrayOf(0x11d90, 0x11d91), // Gunjala Gondi Vowel Sign..Gunjala Gondi Vowel Sign
|
||||
intArrayOf(0x11d95, 0x11d95), // Gunjala Gondi Sign Anusv..Gunjala Gondi Sign Anusv
|
||||
intArrayOf(0x11d97, 0x11d97), // Gunjala Gondi Virama ..Gunjala Gondi Virama
|
||||
intArrayOf(0x11ef3, 0x11ef4), // Makasar Vowel Sign I ..Makasar Vowel Sign U
|
||||
intArrayOf(0x11f00, 0x11f01), // (nil) ..(nil)
|
||||
intArrayOf(0x11f36, 0x11f3a), // (nil) ..(nil)
|
||||
intArrayOf(0x11f40, 0x11f40), // (nil) ..(nil)
|
||||
intArrayOf(0x11f42, 0x11f42), // (nil) ..(nil)
|
||||
intArrayOf(0x13440, 0x13440), // (nil) ..(nil)
|
||||
intArrayOf(0x13447, 0x13455), // (nil) ..(nil)
|
||||
intArrayOf(0x16af0, 0x16af4), // Bassa Vah Combining High..Bassa Vah Combining High
|
||||
intArrayOf(0x16b30, 0x16b36), // Pahawh Hmong Mark Cim Tu..Pahawh Hmong Mark Cim Ta
|
||||
intArrayOf(0x16f4f, 0x16f4f), // Miao Sign Consonant Modi..Miao Sign Consonant Modi
|
||||
intArrayOf(0x16f8f, 0x16f92), // Miao Tone Right ..Miao Tone Below
|
||||
intArrayOf(0x16fe4, 0x16fe4), // Khitan Small Script Fill..Khitan Small Script Fill
|
||||
intArrayOf(0x1bc9d, 0x1bc9e), // Duployan Thick Letter Se..Duployan Double Mark
|
||||
intArrayOf(0x1cf00, 0x1cf2d), // Znamenny Combining Mark ..Znamenny Combining Mark
|
||||
intArrayOf(0x1cf30, 0x1cf46), // Znamenny Combining Tonal..Znamenny Priznak Modifie
|
||||
intArrayOf(0x1d167, 0x1d169), // Musical Symbol Combining..Musical Symbol Combining
|
||||
intArrayOf(0x1d17b, 0x1d182), // Musical Symbol Combining..Musical Symbol Combining
|
||||
intArrayOf(0x1d185, 0x1d18b), // Musical Symbol Combining..Musical Symbol Combining
|
||||
intArrayOf(0x1d1aa, 0x1d1ad), // Musical Symbol Combining..Musical Symbol Combining
|
||||
intArrayOf(0x1d242, 0x1d244), // Combining Greek Musical ..Combining Greek Musical
|
||||
intArrayOf(0x1da00, 0x1da36), // Signwriting Head Rim ..Signwriting Air Sucking
|
||||
intArrayOf(0x1da3b, 0x1da6c), // Signwriting Mouth Closed..Signwriting Excitement
|
||||
intArrayOf(0x1da75, 0x1da75), // Signwriting Upper Body T..Signwriting Upper Body T
|
||||
intArrayOf(0x1da84, 0x1da84), // Signwriting Location Hea..Signwriting Location Hea
|
||||
intArrayOf(0x1da9b, 0x1da9f), // Signwriting Fill Modifie..Signwriting Fill Modifie
|
||||
intArrayOf(0x1daa1, 0x1daaf), // Signwriting Rotation Mod..Signwriting Rotation Mod
|
||||
intArrayOf(0x1e000, 0x1e006), // Combining Glagolitic Let..Combining Glagolitic Let
|
||||
intArrayOf(0x1e008, 0x1e018), // Combining Glagolitic Let..Combining Glagolitic Let
|
||||
intArrayOf(0x1e01b, 0x1e021), // Combining Glagolitic Let..Combining Glagolitic Let
|
||||
intArrayOf(0x1e023, 0x1e024), // Combining Glagolitic Let..Combining Glagolitic Let
|
||||
intArrayOf(0x1e026, 0x1e02a), // Combining Glagolitic Let..Combining Glagolitic Let
|
||||
intArrayOf(0x1e08f, 0x1e08f), // (nil) ..(nil)
|
||||
intArrayOf(0x1e130, 0x1e136), // Nyiakeng Puachue Hmong T..Nyiakeng Puachue Hmong T
|
||||
intArrayOf(0x1e2ae, 0x1e2ae), // Toto Sign Rising Tone ..Toto Sign Rising Tone
|
||||
intArrayOf(0x1e2ec, 0x1e2ef), // Wancho Tone Tup ..Wancho Tone Koini
|
||||
intArrayOf(0x1e4ec, 0x1e4ef), // (nil) ..(nil)
|
||||
intArrayOf(0x1e8d0, 0x1e8d6), // Mende Kikakui Combining ..Mende Kikakui Combining
|
||||
intArrayOf(0x1e944, 0x1e94a), // Adlam Alif Lengthener ..Adlam Nukta
|
||||
intArrayOf(0xe0100, 0xe01ef), // Variation Selector-17 ..Variation Selector-256
|
||||
)
|
||||
|
||||
// https://github.com/jquast/wcwidth/blob/master/wcwidth/table_wide.py
|
||||
// from https://github.com/jquast/wcwidth/pull/64
|
||||
// at commit 1b9b6585b0080ea5cb88dc9815796505724793fe (2022-12-16):
|
||||
private val WIDE_EASTASIAN = arrayOf(
|
||||
intArrayOf(0x01100, 0x0115f), // Hangul Choseong Kiyeok ..Hangul Choseong Filler
|
||||
intArrayOf(0x0231a, 0x0231b), // Watch ..Hourglass
|
||||
intArrayOf(0x02329, 0x0232a), // Left-pointing Angle Brac..Right-pointing Angle Bra
|
||||
intArrayOf(0x023e9, 0x023ec), // Black Right-pointing Dou..Black Down-pointing Doub
|
||||
intArrayOf(0x023f0, 0x023f0), // Alarm Clock ..Alarm Clock
|
||||
intArrayOf(0x023f3, 0x023f3), // Hourglass With Flowing S..Hourglass With Flowing S
|
||||
intArrayOf(0x025fd, 0x025fe), // White Medium Small Squar..Black Medium Small Squar
|
||||
intArrayOf(0x02614, 0x02615), // Umbrella With Rain Drops..Hot Beverage
|
||||
intArrayOf(0x02648, 0x02653), // Aries ..Pisces
|
||||
intArrayOf(0x0267f, 0x0267f), // Wheelchair Symbol ..Wheelchair Symbol
|
||||
intArrayOf(0x02693, 0x02693), // Anchor ..Anchor
|
||||
intArrayOf(0x026a1, 0x026a1), // High Voltage Sign ..High Voltage Sign
|
||||
intArrayOf(0x026aa, 0x026ab), // Medium White Circle ..Medium Black Circle
|
||||
intArrayOf(0x026bd, 0x026be), // Soccer Ball ..Baseball
|
||||
intArrayOf(0x026c4, 0x026c5), // Snowman Without Snow ..Sun Behind Cloud
|
||||
intArrayOf(0x026ce, 0x026ce), // Ophiuchus ..Ophiuchus
|
||||
intArrayOf(0x026d4, 0x026d4), // No Entry ..No Entry
|
||||
intArrayOf(0x026ea, 0x026ea), // Church ..Church
|
||||
intArrayOf(0x026f2, 0x026f3), // Fountain ..Flag In Hole
|
||||
intArrayOf(0x026f5, 0x026f5), // Sailboat ..Sailboat
|
||||
intArrayOf(0x026fa, 0x026fa), // Tent ..Tent
|
||||
intArrayOf(0x026fd, 0x026fd), // Fuel Pump ..Fuel Pump
|
||||
intArrayOf(0x02705, 0x02705), // White Heavy Check Mark ..White Heavy Check Mark
|
||||
intArrayOf(0x0270a, 0x0270b), // Raised Fist ..Raised Hand
|
||||
intArrayOf(0x02728, 0x02728), // Sparkles ..Sparkles
|
||||
intArrayOf(0x0274c, 0x0274c), // Cross Mark ..Cross Mark
|
||||
intArrayOf(0x0274e, 0x0274e), // Negative Squared Cross M..Negative Squared Cross M
|
||||
intArrayOf(0x02753, 0x02755), // Black Question Mark Orna..White Exclamation Mark O
|
||||
intArrayOf(0x02757, 0x02757), // Heavy Exclamation Mark S..Heavy Exclamation Mark S
|
||||
intArrayOf(0x02795, 0x02797), // Heavy Plus Sign ..Heavy Division Sign
|
||||
intArrayOf(0x027b0, 0x027b0), // Curly Loop ..Curly Loop
|
||||
intArrayOf(0x027bf, 0x027bf), // Double Curly Loop ..Double Curly Loop
|
||||
intArrayOf(0x02b1b, 0x02b1c), // Black Large Square ..White Large Square
|
||||
intArrayOf(0x02b50, 0x02b50), // White Medium Star ..White Medium Star
|
||||
intArrayOf(0x02b55, 0x02b55), // Heavy Large Circle ..Heavy Large Circle
|
||||
intArrayOf(0x02e80, 0x02e99), // Cjk Radical Repeat ..Cjk Radical Rap
|
||||
intArrayOf(0x02e9b, 0x02ef3), // Cjk Radical Choke ..Cjk Radical C-simplified
|
||||
intArrayOf(0x02f00, 0x02fd5), // Kangxi Radical One ..Kangxi Radical Flute
|
||||
intArrayOf(0x02ff0, 0x02ffb), // Ideographic Description ..Ideographic Description
|
||||
intArrayOf(0x03000, 0x0303e), // Ideographic Space ..Ideographic Variation In
|
||||
intArrayOf(0x03041, 0x03096), // Hiragana Letter Small A ..Hiragana Letter Small Ke
|
||||
intArrayOf(0x03099, 0x030ff), // Combining Katakana-hirag..Katakana Digraph Koto
|
||||
intArrayOf(0x03105, 0x0312f), // Bopomofo Letter B ..Bopomofo Letter Nn
|
||||
intArrayOf(0x03131, 0x0318e), // Hangul Letter Kiyeok ..Hangul Letter Araeae
|
||||
intArrayOf(0x03190, 0x031e3), // Ideographic Annotation L..Cjk Stroke Q
|
||||
intArrayOf(0x031f0, 0x0321e), // Katakana Letter Small Ku..Parenthesized Korean Cha
|
||||
intArrayOf(0x03220, 0x03247), // Parenthesized Ideograph ..Circled Ideograph Koto
|
||||
intArrayOf(0x03250, 0x04dbf), // Partnership Sign ..Cjk Unified Ideograph-4d
|
||||
intArrayOf(0x04e00, 0x0a48c), // Cjk Unified Ideograph-4e..Yi Syllable Yyr
|
||||
intArrayOf(0x0a490, 0x0a4c6), // Yi Radical Qot ..Yi Radical Ke
|
||||
intArrayOf(0x0a960, 0x0a97c), // Hangul Choseong Tikeut-m..Hangul Choseong Ssangyeo
|
||||
intArrayOf(0x0ac00, 0x0d7a3), // Hangul Syllable Ga ..Hangul Syllable Hih
|
||||
intArrayOf(0x0f900, 0x0faff), // Cjk Compatibility Ideogr..(nil)
|
||||
intArrayOf(0x0fe10, 0x0fe19), // Presentation Form For Ve..Presentation Form For Ve
|
||||
intArrayOf(0x0fe30, 0x0fe52), // Presentation Form For Ve..Small Full Stop
|
||||
intArrayOf(0x0fe54, 0x0fe66), // Small Semicolon ..Small Equals Sign
|
||||
intArrayOf(0x0fe68, 0x0fe6b), // Small Reverse Solidus ..Small Commercial At
|
||||
intArrayOf(0x0ff01, 0x0ff60), // Fullwidth Exclamation Ma..Fullwidth Right White Pa
|
||||
intArrayOf(0x0ffe0, 0x0ffe6), // Fullwidth Cent Sign ..Fullwidth Won Sign
|
||||
intArrayOf(0x16fe0, 0x16fe4), // Tangut Iteration Mark ..Khitan Small Script Fill
|
||||
intArrayOf(0x16ff0, 0x16ff1), // Vietnamese Alternate Rea..Vietnamese Alternate Rea
|
||||
intArrayOf(0x17000, 0x187f7), // (nil) ..(nil)
|
||||
intArrayOf(0x18800, 0x18cd5), // Tangut Component-001 ..Khitan Small Script Char
|
||||
intArrayOf(0x18d00, 0x18d08), // (nil) ..(nil)
|
||||
intArrayOf(0x1aff0, 0x1aff3), // Katakana Letter Minnan T..Katakana Letter Minnan T
|
||||
intArrayOf(0x1aff5, 0x1affb), // Katakana Letter Minnan T..Katakana Letter Minnan N
|
||||
intArrayOf(0x1affd, 0x1affe), // Katakana Letter Minnan N..Katakana Letter Minnan N
|
||||
intArrayOf(0x1b000, 0x1b122), // Katakana Letter Archaic ..Katakana Letter Archaic
|
||||
intArrayOf(0x1b132, 0x1b132), // (nil) ..(nil)
|
||||
intArrayOf(0x1b150, 0x1b152), // Hiragana Letter Small Wi..Hiragana Letter Small Wo
|
||||
intArrayOf(0x1b155, 0x1b155), // (nil) ..(nil)
|
||||
intArrayOf(0x1b164, 0x1b167), // Katakana Letter Small Wi..Katakana Letter Small N
|
||||
intArrayOf(0x1b170, 0x1b2fb), // Nushu Character-1b170 ..Nushu Character-1b2fb
|
||||
intArrayOf(0x1f004, 0x1f004), // Mahjong Tile Red Dragon ..Mahjong Tile Red Dragon
|
||||
intArrayOf(0x1f0cf, 0x1f0cf), // Playing Card Black Joker..Playing Card Black Joker
|
||||
intArrayOf(0x1f18e, 0x1f18e), // Negative Squared Ab ..Negative Squared Ab
|
||||
intArrayOf(0x1f191, 0x1f19a), // Squared Cl ..Squared Vs
|
||||
intArrayOf(0x1f200, 0x1f202), // Square Hiragana Hoka ..Squared Katakana Sa
|
||||
intArrayOf(0x1f210, 0x1f23b), // Squared Cjk Unified Ideo..Squared Cjk Unified Ideo
|
||||
intArrayOf(0x1f240, 0x1f248), // Tortoise Shell Bracketed..Tortoise Shell Bracketed
|
||||
intArrayOf(0x1f250, 0x1f251), // Circled Ideograph Advant..Circled Ideograph Accept
|
||||
intArrayOf(0x1f260, 0x1f265), // Rounded Symbol For Fu ..Rounded Symbol For Cai
|
||||
intArrayOf(0x1f300, 0x1f320), // Cyclone ..Shooting Star
|
||||
intArrayOf(0x1f32d, 0x1f335), // Hot Dog ..Cactus
|
||||
intArrayOf(0x1f337, 0x1f37c), // Tulip ..Baby Bottle
|
||||
intArrayOf(0x1f37e, 0x1f393), // Bottle With Popping Cork..Graduation Cap
|
||||
intArrayOf(0x1f3a0, 0x1f3ca), // Carousel Horse ..Swimmer
|
||||
intArrayOf(0x1f3cf, 0x1f3d3), // Cricket Bat And Ball ..Table Tennis Paddle And
|
||||
intArrayOf(0x1f3e0, 0x1f3f0), // House Building ..European Castle
|
||||
intArrayOf(0x1f3f4, 0x1f3f4), // Waving Black Flag ..Waving Black Flag
|
||||
intArrayOf(0x1f3f8, 0x1f43e), // Badminton Racquet And Sh..Paw Prints
|
||||
intArrayOf(0x1f440, 0x1f440), // Eyes ..Eyes
|
||||
intArrayOf(0x1f442, 0x1f4fc), // Ear ..Videocassette
|
||||
intArrayOf(0x1f4ff, 0x1f53d), // Prayer Beads ..Down-pointing Small Red
|
||||
intArrayOf(0x1f54b, 0x1f54e), // Kaaba ..Menorah With Nine Branch
|
||||
intArrayOf(0x1f550, 0x1f567), // Clock Face One Oclock ..Clock Face Twelve-thirty
|
||||
intArrayOf(0x1f57a, 0x1f57a), // Man Dancing ..Man Dancing
|
||||
intArrayOf(0x1f595, 0x1f596), // Reversed Hand With Middl..Raised Hand With Part Be
|
||||
intArrayOf(0x1f5a4, 0x1f5a4), // Black Heart ..Black Heart
|
||||
intArrayOf(0x1f5fb, 0x1f64f), // Mount Fuji ..Person With Folded Hands
|
||||
intArrayOf(0x1f680, 0x1f6c5), // Rocket ..Left Luggage
|
||||
intArrayOf(0x1f6cc, 0x1f6cc), // Sleeping Accommodation ..Sleeping Accommodation
|
||||
intArrayOf(0x1f6d0, 0x1f6d2), // Place Of Worship ..Shopping Trolley
|
||||
intArrayOf(0x1f6d5, 0x1f6d7), // Hindu Temple ..Elevator
|
||||
intArrayOf(0x1f6dc, 0x1f6df), // (nil) ..Ring Buoy
|
||||
intArrayOf(0x1f6eb, 0x1f6ec), // Airplane Departure ..Airplane Arriving
|
||||
intArrayOf(0x1f6f4, 0x1f6fc), // Scooter ..Roller Skate
|
||||
intArrayOf(0x1f7e0, 0x1f7eb), // Large Orange Circle ..Large Brown Square
|
||||
intArrayOf(0x1f7f0, 0x1f7f0), // Heavy Equals Sign ..Heavy Equals Sign
|
||||
intArrayOf(0x1f90c, 0x1f93a), // Pinched Fingers ..Fencer
|
||||
intArrayOf(0x1f93c, 0x1f945), // Wrestlers ..Goal Net
|
||||
intArrayOf(0x1f947, 0x1f9ff), // First Place Medal ..Nazar Amulet
|
||||
intArrayOf(0x1fa70, 0x1fa7c), // Ballet Shoes ..Crutch
|
||||
intArrayOf(0x1fa80, 0x1fa88), // Yo-yo ..(nil)
|
||||
intArrayOf(0x1fa90, 0x1fabd), // Ringed Planet ..(nil)
|
||||
intArrayOf(0x1fabf, 0x1fac5), // (nil) ..Person With Crown
|
||||
intArrayOf(0x1face, 0x1fadb), // (nil) ..(nil)
|
||||
intArrayOf(0x1fae0, 0x1fae8), // Melting Face ..(nil)
|
||||
intArrayOf(0x1faf0, 0x1faf8), // Hand With Index Finger A..(nil)
|
||||
intArrayOf(0x20000, 0x2fffd), // Cjk Unified Ideograph-20..(nil)
|
||||
intArrayOf(0x30000, 0x3fffd), // Cjk Unified Ideograph-30..(nil)
|
||||
)
|
||||
|
||||
private fun intable(table: Array<IntArray>, c: Int): Boolean {
|
||||
if (c < table[0][0]) return false
|
||||
var bot = 0
|
||||
var top = table.size - 1
|
||||
while (top >= bot) {
|
||||
val mid = (bot + top) / 2
|
||||
if (table[mid][1] < c) {
|
||||
bot = mid + 1
|
||||
} else if (table[mid][0] > c) {
|
||||
top = mid - 1
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** Return the terminal display width of a code point: 0, 1 or 2. */
|
||||
fun width(ucs: Int): Int {
|
||||
if (ucs == 0 ||
|
||||
ucs == 0x034F ||
|
||||
(ucs in 0x200B..0x200F) ||
|
||||
ucs == 0x2028 ||
|
||||
ucs == 0x2029 ||
|
||||
(ucs in 0x202A..0x202E) ||
|
||||
(ucs in 0x2060..0x2063)) {
|
||||
return 0
|
||||
}
|
||||
|
||||
// C0/C1 control characters
|
||||
// Termux change: Return 0 instead of -1.
|
||||
if (ucs < 32 || (ucs in 0x07F until 0x0A0)) return 0
|
||||
|
||||
if (intable(ZERO_WIDTH, ucs)) return 0
|
||||
|
||||
return if (intable(WIDE_EASTASIAN, ucs)) 2 else 1
|
||||
}
|
||||
|
||||
/** The width at an index position in a java char array. */
|
||||
fun width(chars: CharArray, index: Int): Int {
|
||||
val c = chars[index]
|
||||
return if (Character.isHighSurrogate(c)) width(Character.toCodePoint(c, chars[index + 1])) else width(c.code)
|
||||
}
|
||||
|
||||
/**
|
||||
* The zero width characters count like combining characters in the `chars` array from start
|
||||
* index to end index (exclusive).
|
||||
*/
|
||||
fun zeroWidthCharsCount(chars: CharArray, start: Int, end: Int): Int {
|
||||
if (start < 0 || start >= chars.size) return 0
|
||||
var count = 0
|
||||
var i = start
|
||||
while (i < end && i < chars.size) {
|
||||
if (Character.isHighSurrogate(chars[i])) {
|
||||
if (width(Character.toCodePoint(chars[i], chars[i + 1])) <= 0) {
|
||||
count++
|
||||
}
|
||||
i += 2
|
||||
} else {
|
||||
if (width(chars[i].code) <= 0) {
|
||||
count++
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.ui.terminal.TerminalComposeView
|
||||
import com.topjohnwu.magisk.ui.terminal.TerminalScreen
|
||||
import top.yukonga.miuix.kmp.basic.Icon
|
||||
import top.yukonga.miuix.kmp.basic.IconButton
|
||||
import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior
|
||||
@@ -94,14 +94,11 @@ fun FlashScreen(viewModel: FlashViewModel, action: String, onBack: () -> Unit) {
|
||||
popupHost = { }
|
||||
) { padding ->
|
||||
if (useTerminal) {
|
||||
val session by viewModel.termSession.collectAsState()
|
||||
TerminalComposeView(
|
||||
session = session,
|
||||
TerminalScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
onViewCreated = { viewModel.setTerminalView(it) },
|
||||
onEmulatorReady = { viewModel.onEmulatorReady() },
|
||||
onEmulatorCreated = { viewModel.onEmulatorCreated(it) },
|
||||
)
|
||||
} else {
|
||||
val items = viewModel.consoleItems
|
||||
|
||||
@@ -4,8 +4,6 @@ import android.net.Uri
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.core.net.toFile
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.termux.terminal.TerminalSession
|
||||
import com.termux.view.TerminalView
|
||||
import com.topjohnwu.magisk.arch.BaseViewModel
|
||||
import com.topjohnwu.magisk.core.AppContext
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
@@ -20,7 +18,9 @@ import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.displayName
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||
import com.topjohnwu.magisk.ui.terminal.TerminalSessionCallback
|
||||
import com.topjohnwu.magisk.terminal.TerminalEmulator
|
||||
import com.topjohnwu.magisk.terminal.appendLineOnMain
|
||||
import com.topjohnwu.magisk.terminal.runSuCommand
|
||||
import com.topjohnwu.superuser.CallbackList
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
@@ -50,22 +50,14 @@ class FlashViewModel : BaseViewModel() {
|
||||
var flashAction: String = ""
|
||||
var flashUri: Uri? = null
|
||||
|
||||
// --- TerminalView mode (FLASH_ZIP) ---
|
||||
// --- TerminalScreen mode (FLASH_ZIP) ---
|
||||
|
||||
private val _termSession = MutableStateFlow<TerminalSession?>(null)
|
||||
val termSession: StateFlow<TerminalSession?> = _termSession.asStateFlow()
|
||||
private var emulator: TerminalEmulator? = null
|
||||
private val emulatorReady = CompletableDeferred<TerminalEmulator>()
|
||||
|
||||
private val emulatorReady = CompletableDeferred<Unit>()
|
||||
val sessionCallback = TerminalSessionCallback()
|
||||
|
||||
fun setTerminalView(view: TerminalView) {
|
||||
sessionCallback.terminalView = view
|
||||
}
|
||||
|
||||
fun onEmulatorReady() {
|
||||
if (!emulatorReady.isCompleted) {
|
||||
emulatorReady.complete(Unit)
|
||||
}
|
||||
fun onEmulatorCreated(emu: TerminalEmulator) {
|
||||
emulator = emu
|
||||
emulatorReady.complete(emu)
|
||||
}
|
||||
|
||||
// --- LazyColumn mode (MagiskInstaller) ---
|
||||
@@ -90,7 +82,7 @@ class FlashViewModel : BaseViewModel() {
|
||||
when (action) {
|
||||
Const.Value.FLASH_ZIP -> {
|
||||
uri ?: return@launch
|
||||
flashZipWithPty(uri)
|
||||
flashZip(uri)
|
||||
}
|
||||
Const.Value.UNINSTALL -> {
|
||||
_showReboot.value = false
|
||||
@@ -127,15 +119,8 @@ class FlashViewModel : BaseViewModel() {
|
||||
_flashState.value = if (success) State.SUCCESS else State.FAILED
|
||||
}
|
||||
|
||||
private suspend fun flashZipWithPty(uri: Uri) {
|
||||
val session = TerminalSession(
|
||||
"/system/bin/sh", "/",
|
||||
arrayOf("sh", "-c", "exec sleep 2147483647"),
|
||||
arrayOf("TERM=xterm-256color"),
|
||||
5000, sessionCallback
|
||||
)
|
||||
_termSession.value = session
|
||||
emulatorReady.await()
|
||||
private suspend fun flashZip(uri: Uri) {
|
||||
val emu = emulatorReady.await()
|
||||
|
||||
val installDir = File(AppContext.cacheDir, "flash")
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
@@ -169,46 +154,28 @@ class FlashViewModel : BaseViewModel() {
|
||||
|
||||
val (error, prepResult) = result
|
||||
if (prepResult == null) {
|
||||
writeToPty(session, "! ${error ?: "Installation failed"}")
|
||||
emu.appendLineOnMain("! ${error ?: "Installation failed"}")
|
||||
_flashState.value = State.FAILED
|
||||
return
|
||||
}
|
||||
|
||||
val (dir, zipFile, displayName) = prepResult
|
||||
val ptyPath = getPtyPath(session)
|
||||
if (ptyPath == null) {
|
||||
_flashState.value = State.FAILED
|
||||
return
|
||||
}
|
||||
|
||||
val success = withContext(Dispatchers.IO) {
|
||||
Shell.cmd(
|
||||
"(export TERM=xterm-256color; " +
|
||||
runSuCommand(
|
||||
emu,
|
||||
"echo '- Installing $displayName'; " +
|
||||
"sh $dir/update-binary dummy 1 '${zipFile.absolutePath}'; " +
|
||||
"EXIT=\$?; " +
|
||||
"if [ \$EXIT -ne 0 ]; then echo '! Installation failed'; fi; " +
|
||||
"exit \$EXIT) <>$ptyPath >&0 2>&0"
|
||||
).exec().isSuccess
|
||||
"exit \$EXIT"
|
||||
)
|
||||
}
|
||||
|
||||
Shell.cmd("cd /", "rm -rf $dir ${Const.TMPDIR}").submit()
|
||||
_flashState.value = if (success) State.SUCCESS else State.FAILED
|
||||
}
|
||||
|
||||
private suspend fun getPtyPath(session: TerminalSession): String? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
Shell.cmd("readlink /proc/${session.pid}/fd/0").exec().out.firstOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun writeToPty(session: TerminalSession, message: String) {
|
||||
val ptyPath = getPtyPath(session) ?: return
|
||||
withContext(Dispatchers.IO) {
|
||||
Shell.cmd("echo '$message' >$ptyPath").exec()
|
||||
}
|
||||
}
|
||||
|
||||
fun saveLog() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val name = "magisk_install_log_%s.log".format(
|
||||
@@ -216,7 +183,7 @@ class FlashViewModel : BaseViewModel() {
|
||||
)
|
||||
val file = MediaStoreUtils.getFile(name)
|
||||
file.uri.outputStream().bufferedWriter().use { writer ->
|
||||
val transcript = _termSession.value?.emulator?.screen?.transcriptText
|
||||
val transcript = emulator?.screen?.transcriptText
|
||||
if (transcript != null) {
|
||||
writer.write(transcript)
|
||||
} else {
|
||||
@@ -233,9 +200,4 @@ class FlashViewModel : BaseViewModel() {
|
||||
}
|
||||
|
||||
fun restartPressed() = reboot()
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
_termSession.value?.finishIfRunning()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.ui.terminal.TerminalComposeView
|
||||
import com.topjohnwu.magisk.ui.terminal.TerminalScreen
|
||||
import top.yukonga.miuix.kmp.basic.Icon
|
||||
import top.yukonga.miuix.kmp.basic.IconButton
|
||||
import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior
|
||||
@@ -24,7 +24,6 @@ import com.topjohnwu.magisk.core.R as CoreR
|
||||
@Composable
|
||||
fun ActionScreen(viewModel: ActionViewModel, actionName: String, onBack: () -> Unit) {
|
||||
val actionState by viewModel.actionState.collectAsState()
|
||||
val session by viewModel.termSession.collectAsState()
|
||||
val finished = actionState != ActionViewModel.State.RUNNING
|
||||
|
||||
val scrollBehavior = MiuixScrollBehavior()
|
||||
@@ -63,13 +62,11 @@ fun ActionScreen(viewModel: ActionViewModel, actionName: String, onBack: () -> U
|
||||
},
|
||||
popupHost = { }
|
||||
) { padding ->
|
||||
TerminalComposeView(
|
||||
session = session,
|
||||
TerminalScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
onViewCreated = { viewModel.setTerminalView(it) },
|
||||
onEmulatorReady = { viewModel.onEmulatorReady() },
|
||||
onEmulatorCreated = { viewModel.onEmulatorCreated(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
package com.topjohnwu.magisk.ui.module
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.termux.terminal.TerminalSession
|
||||
import com.termux.view.TerminalView
|
||||
import com.topjohnwu.magisk.arch.BaseViewModel
|
||||
import com.topjohnwu.magisk.core.ktx.synchronized
|
||||
import com.topjohnwu.magisk.core.ktx.timeFormatStandard
|
||||
import com.topjohnwu.magisk.core.ktx.toTime
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||
import com.topjohnwu.magisk.ui.terminal.TerminalSessionCallback
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.magisk.terminal.TerminalEmulator
|
||||
import com.topjohnwu.magisk.terminal.runSuCommand
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -28,50 +25,26 @@ class ActionViewModel : BaseViewModel() {
|
||||
private val _actionState = MutableStateFlow(State.RUNNING)
|
||||
val actionState: StateFlow<State> = _actionState.asStateFlow()
|
||||
|
||||
private val _termSession = MutableStateFlow<TerminalSession?>(null)
|
||||
val termSession: StateFlow<TerminalSession?> = _termSession.asStateFlow()
|
||||
|
||||
var actionId: String = ""
|
||||
var actionName: String = ""
|
||||
|
||||
private val logItems = mutableListOf<String>().synchronized()
|
||||
private val emulatorReady = CompletableDeferred<Unit>()
|
||||
private var emulator: TerminalEmulator? = null
|
||||
private val emulatorReady = CompletableDeferred<TerminalEmulator>()
|
||||
|
||||
val sessionCallback = TerminalSessionCallback()
|
||||
|
||||
fun setTerminalView(view: TerminalView) {
|
||||
sessionCallback.terminalView = view
|
||||
}
|
||||
|
||||
fun onEmulatorReady() {
|
||||
if (!emulatorReady.isCompleted) {
|
||||
emulatorReady.complete(Unit)
|
||||
}
|
||||
fun onEmulatorCreated(emu: TerminalEmulator) {
|
||||
emulator = emu
|
||||
emulatorReady.complete(emu)
|
||||
}
|
||||
|
||||
fun startRunAction() {
|
||||
viewModelScope.launch {
|
||||
val session = TerminalSession(
|
||||
"/system/bin/sh", "/",
|
||||
arrayOf("sh", "-c", "exec sleep 2147483647"),
|
||||
arrayOf("TERM=xterm-256color"),
|
||||
5000, sessionCallback
|
||||
)
|
||||
_termSession.value = session
|
||||
emulatorReady.await()
|
||||
|
||||
val ptyPath = withContext(Dispatchers.IO) {
|
||||
Shell.cmd("readlink /proc/${session.pid}/fd/0").exec().out.firstOrNull()
|
||||
}
|
||||
if (ptyPath == null) {
|
||||
_actionState.value = State.FAILED
|
||||
return@launch
|
||||
}
|
||||
val emu = emulatorReady.await()
|
||||
|
||||
val success = withContext(Dispatchers.IO) {
|
||||
Shell.cmd(
|
||||
"(export TERM=xterm-256color; run_action '$actionId') <>$ptyPath >&0 2>&0"
|
||||
).exec().isSuccess
|
||||
runSuCommand(
|
||||
emu,
|
||||
"cd /data/adb/modules/$actionId && sh ./action.sh"
|
||||
)
|
||||
}
|
||||
|
||||
_actionState.value = if (success) State.SUCCESS else State.FAILED
|
||||
@@ -86,24 +59,12 @@ class ActionViewModel : BaseViewModel() {
|
||||
)
|
||||
val file = MediaStoreUtils.getFile(name)
|
||||
file.uri.outputStream().bufferedWriter().use { writer ->
|
||||
val transcript = _termSession.value?.emulator?.screen?.transcriptText
|
||||
val transcript = emulator?.screen?.transcriptText
|
||||
if (transcript != null) {
|
||||
writer.write(transcript)
|
||||
} else {
|
||||
synchronized(logItems) {
|
||||
logItems.forEach {
|
||||
writer.write(it)
|
||||
writer.newLine()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
showSnackbar(file.toString())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
_termSession.value?.finishIfRunning()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
package com.topjohnwu.magisk.ui.terminal
|
||||
|
||||
import android.graphics.Color
|
||||
import android.graphics.Typeface
|
||||
import android.util.TypedValue
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import com.termux.terminal.TerminalSession
|
||||
import com.termux.view.TerminalView
|
||||
import com.termux.view.TerminalViewClient
|
||||
|
||||
@Composable
|
||||
fun TerminalComposeView(
|
||||
session: TerminalSession?,
|
||||
modifier: Modifier = Modifier,
|
||||
onViewCreated: (TerminalView) -> Unit = {},
|
||||
onEmulatorReady: () -> Unit = {},
|
||||
) {
|
||||
AndroidView(
|
||||
factory = { context ->
|
||||
val textSizePx = TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_SP, 12f, context.resources.displayMetrics
|
||||
).toInt()
|
||||
TerminalView(context, null).apply {
|
||||
setBackgroundColor(Color.BLACK)
|
||||
setTextSize(textSizePx)
|
||||
setTypeface(Typeface.MONOSPACE)
|
||||
keepScreenOn = true
|
||||
setTerminalViewClient(ReadOnlyTerminalViewClient(onEmulatorReady))
|
||||
onViewCreated(this)
|
||||
}
|
||||
},
|
||||
update = { view ->
|
||||
if (session != null && view.mTermSession != session) {
|
||||
view.attachSession(session)
|
||||
}
|
||||
},
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
private class ReadOnlyTerminalViewClient(
|
||||
private val onEmulatorReady: () -> Unit,
|
||||
) : TerminalViewClient {
|
||||
override fun onScale(scale: Float) = 1.0f
|
||||
override fun onSingleTapUp(e: MotionEvent) {}
|
||||
override fun shouldBackButtonBeMappedToEscape() = false
|
||||
override fun shouldEnforceCharBasedInput() = false
|
||||
override fun shouldUseCtrlSpaceWorkaround() = false
|
||||
override fun isTerminalViewSelected() = true
|
||||
override fun copyModeChanged(copyMode: Boolean) {}
|
||||
override fun onKeyDown(keyCode: Int, e: KeyEvent, session: TerminalSession) = false
|
||||
override fun onKeyUp(keyCode: Int, e: KeyEvent) = false
|
||||
override fun onLongPress(event: MotionEvent) = false
|
||||
override fun readControlKey() = false
|
||||
override fun readAltKey() = false
|
||||
override fun readShiftKey() = false
|
||||
override fun readFnKey() = false
|
||||
override fun onCodePoint(codePoint: Int, ctrlDown: Boolean, session: TerminalSession) = false
|
||||
override fun onEmulatorSet() { onEmulatorReady() }
|
||||
override fun logError(tag: String, message: String) {}
|
||||
override fun logWarn(tag: String, message: String) {}
|
||||
override fun logInfo(tag: String, message: String) {}
|
||||
override fun logDebug(tag: String, message: String) {}
|
||||
override fun logVerbose(tag: String, message: String) {}
|
||||
override fun logStackTraceWithMessage(tag: String, message: String, e: Exception) {}
|
||||
override fun logStackTrace(tag: String, e: Exception) {}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
package com.topjohnwu.magisk.ui.terminal
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.Typeface
|
||||
import com.topjohnwu.magisk.terminal.TerminalBuffer
|
||||
import com.topjohnwu.magisk.terminal.TerminalEmulator
|
||||
import com.topjohnwu.magisk.terminal.TerminalRow
|
||||
import com.topjohnwu.magisk.terminal.TextStyle
|
||||
import com.topjohnwu.magisk.terminal.WcWidth
|
||||
|
||||
/**
|
||||
* Renderer of a [TerminalEmulator] into a [Canvas].
|
||||
*
|
||||
* Saves font metrics, so needs to be recreated each time the typeface or font size changes.
|
||||
*/
|
||||
class TerminalRenderer(
|
||||
textSize: Int,
|
||||
typeface: Typeface,
|
||||
) {
|
||||
val textSize: Int = textSize
|
||||
val typeface: Typeface = typeface
|
||||
private val textPaint = Paint()
|
||||
|
||||
/** The width of a single mono spaced character obtained by [Paint.measureText] on a single 'X'. */
|
||||
val fontWidth: Float
|
||||
|
||||
/** The [Paint.getFontSpacing]. See http://www.fampennings.nl/maarten/android/08numgrid/font.png */
|
||||
val fontLineSpacing: Int
|
||||
|
||||
/** The [Paint.ascent]. See http://www.fampennings.nl/maarten/android/08numgrid/font.png */
|
||||
private val fontAscent: Int
|
||||
|
||||
/** The [fontLineSpacing] + [fontAscent]. */
|
||||
val fontLineSpacingAndAscent: Int
|
||||
|
||||
private val asciiMeasures = FloatArray(127)
|
||||
|
||||
init {
|
||||
textPaint.typeface = typeface
|
||||
textPaint.isAntiAlias = true
|
||||
textPaint.textSize = textSize.toFloat()
|
||||
|
||||
fontLineSpacing = kotlin.math.ceil(textPaint.fontSpacing).toInt()
|
||||
fontAscent = kotlin.math.ceil(textPaint.ascent()).toInt()
|
||||
fontLineSpacingAndAscent = fontLineSpacing + fontAscent
|
||||
fontWidth = textPaint.measureText("X")
|
||||
|
||||
val sb = StringBuilder(" ")
|
||||
for (i in asciiMeasures.indices) {
|
||||
sb[0] = i.toChar()
|
||||
asciiMeasures[i] = textPaint.measureText(sb, 0, 1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the terminal to a canvas with at a specified row scroll, and an optional rectangular selection.
|
||||
*/
|
||||
fun render(
|
||||
mEmulator: TerminalEmulator,
|
||||
canvas: Canvas,
|
||||
topRow: Int,
|
||||
selectionY1: Int,
|
||||
selectionY2: Int,
|
||||
selectionX1: Int,
|
||||
selectionX2: Int,
|
||||
) {
|
||||
val reverseVideo = mEmulator.isReverseVideo
|
||||
val endRow = topRow + mEmulator.mRows
|
||||
val columns = mEmulator.mColumns
|
||||
val cursorCol = mEmulator.cursorCol
|
||||
val cursorRow = mEmulator.cursorRow
|
||||
val cursorVisible = mEmulator.shouldCursorBeVisible()
|
||||
val screen = mEmulator.screen
|
||||
val palette = mEmulator.mColors.currentColors
|
||||
val cursorShape = mEmulator.cursorStyle
|
||||
|
||||
if (reverseVideo) {
|
||||
canvas.drawColor(palette[TextStyle.COLOR_INDEX_FOREGROUND], PorterDuff.Mode.SRC)
|
||||
}
|
||||
|
||||
var heightOffset = fontLineSpacingAndAscent.toFloat()
|
||||
for (row in topRow until endRow) {
|
||||
heightOffset += fontLineSpacing
|
||||
|
||||
val cursorX = if (row == cursorRow && cursorVisible) cursorCol else -1
|
||||
var selx1 = -1
|
||||
var selx2 = -1
|
||||
if (row in selectionY1..selectionY2) {
|
||||
if (row == selectionY1) selx1 = selectionX1
|
||||
selx2 = if (row == selectionY2) selectionX2 else mEmulator.mColumns
|
||||
}
|
||||
|
||||
val lineObject = screen.allocateFullLineIfNecessary(screen.externalToInternalRow(row))
|
||||
val line = lineObject.text
|
||||
val charsUsedInLine = lineObject.spaceUsed
|
||||
|
||||
var lastRunStyle = 0L
|
||||
var lastRunInsideCursor = false
|
||||
var lastRunInsideSelection = false
|
||||
var lastRunStartColumn = -1
|
||||
var lastRunStartIndex = 0
|
||||
var lastRunFontWidthMismatch = false
|
||||
var currentCharIndex = 0
|
||||
var measuredWidthForRun = 0f
|
||||
|
||||
var column = 0
|
||||
while (column < columns) {
|
||||
val charAtIndex = line[currentCharIndex]
|
||||
val charIsHighsurrogate = Character.isHighSurrogate(charAtIndex)
|
||||
val charsForCodePoint = if (charIsHighsurrogate) 2 else 1
|
||||
val codePoint = if (charIsHighsurrogate) {
|
||||
Character.toCodePoint(charAtIndex, line[currentCharIndex + 1])
|
||||
} else {
|
||||
charAtIndex.code
|
||||
}
|
||||
val codePointWcWidth = WcWidth.width(codePoint)
|
||||
val insideCursor = cursorX == column || (codePointWcWidth == 2 && cursorX == column + 1)
|
||||
val insideSelection = column >= selx1 && column <= selx2
|
||||
val style = lineObject.getStyle(column)
|
||||
|
||||
// Check if the measured text width for this code point is not the same as that expected by wcwidth().
|
||||
// This could happen for some fonts which are not truly monospace, or for more exotic characters such as
|
||||
// smileys which android font renders as wide.
|
||||
// If this is detected, we draw this code point scaled to match what wcwidth() expects.
|
||||
val measuredCodePointWidth = if (codePoint < asciiMeasures.size) {
|
||||
asciiMeasures[codePoint]
|
||||
} else {
|
||||
textPaint.measureText(line, currentCharIndex, charsForCodePoint)
|
||||
}
|
||||
val fontWidthMismatch = kotlin.math.abs(measuredCodePointWidth / fontWidth - codePointWcWidth.toFloat()) > 0.01f
|
||||
|
||||
if (style != lastRunStyle || insideCursor != lastRunInsideCursor || insideSelection != lastRunInsideSelection ||
|
||||
fontWidthMismatch || lastRunFontWidthMismatch
|
||||
) {
|
||||
if (column != 0) {
|
||||
val columnWidthSinceLastRun = column - lastRunStartColumn
|
||||
val charsSinceLastRun = currentCharIndex - lastRunStartIndex
|
||||
val cursorColor = if (lastRunInsideCursor) mEmulator.mColors.currentColors[TextStyle.COLOR_INDEX_CURSOR] else 0
|
||||
var invertCursorTextColor = false
|
||||
if (lastRunInsideCursor && cursorShape == TerminalEmulator.TERMINAL_CURSOR_STYLE_BLOCK) {
|
||||
invertCursorTextColor = true
|
||||
}
|
||||
drawTextRun(
|
||||
canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun,
|
||||
lastRunStartIndex, charsSinceLastRun, measuredWidthForRun,
|
||||
cursorColor, cursorShape, lastRunStyle,
|
||||
reverseVideo || invertCursorTextColor || lastRunInsideSelection,
|
||||
)
|
||||
}
|
||||
measuredWidthForRun = 0f
|
||||
lastRunStyle = style
|
||||
lastRunInsideCursor = insideCursor
|
||||
lastRunInsideSelection = insideSelection
|
||||
lastRunStartColumn = column
|
||||
lastRunStartIndex = currentCharIndex
|
||||
lastRunFontWidthMismatch = fontWidthMismatch
|
||||
}
|
||||
measuredWidthForRun += measuredCodePointWidth
|
||||
column += codePointWcWidth
|
||||
currentCharIndex += charsForCodePoint
|
||||
while (currentCharIndex < charsUsedInLine && WcWidth.width(line, currentCharIndex) <= 0) {
|
||||
// Eat combining chars so that they are treated as part of the last non-combining code point,
|
||||
// instead of e.g. being considered inside the cursor in the next run.
|
||||
currentCharIndex += if (Character.isHighSurrogate(line[currentCharIndex])) 2 else 1
|
||||
}
|
||||
}
|
||||
|
||||
val columnWidthSinceLastRun = columns - lastRunStartColumn
|
||||
val charsSinceLastRun = currentCharIndex - lastRunStartIndex
|
||||
val cursorColor = if (lastRunInsideCursor) mEmulator.mColors.currentColors[TextStyle.COLOR_INDEX_CURSOR] else 0
|
||||
var invertCursorTextColor = false
|
||||
if (lastRunInsideCursor && cursorShape == TerminalEmulator.TERMINAL_CURSOR_STYLE_BLOCK) {
|
||||
invertCursorTextColor = true
|
||||
}
|
||||
drawTextRun(
|
||||
canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun,
|
||||
lastRunStartIndex, charsSinceLastRun, measuredWidthForRun,
|
||||
cursorColor, cursorShape, lastRunStyle,
|
||||
reverseVideo || invertCursorTextColor || lastRunInsideSelection,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawTextRun(
|
||||
canvas: Canvas,
|
||||
text: CharArray,
|
||||
palette: IntArray,
|
||||
y: Float,
|
||||
startColumn: Int,
|
||||
runWidthColumns: Int,
|
||||
startCharIndex: Int,
|
||||
runWidthChars: Int,
|
||||
mes: Float,
|
||||
cursor: Int,
|
||||
cursorStyle: Int,
|
||||
textStyle: Long,
|
||||
reverseVideo: Boolean,
|
||||
) {
|
||||
var foreColor = TextStyle.decodeForeColor(textStyle)
|
||||
val effect = TextStyle.decodeEffect(textStyle)
|
||||
var backColor = TextStyle.decodeBackColor(textStyle)
|
||||
val bold = (effect and (TextStyle.CHARACTER_ATTRIBUTE_BOLD or TextStyle.CHARACTER_ATTRIBUTE_BLINK)) != 0
|
||||
val underline = (effect and TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE) != 0
|
||||
val italic = (effect and TextStyle.CHARACTER_ATTRIBUTE_ITALIC) != 0
|
||||
val strikeThrough = (effect and TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH) != 0
|
||||
val dim = (effect and TextStyle.CHARACTER_ATTRIBUTE_DIM) != 0
|
||||
|
||||
if ((foreColor and 0xff000000.toInt()) != 0xff000000.toInt()) {
|
||||
// Let bold have bright colors if applicable (one of the first 8):
|
||||
if (bold && foreColor in 0..7) foreColor += 8
|
||||
foreColor = palette[foreColor]
|
||||
}
|
||||
|
||||
if ((backColor and 0xff000000.toInt()) != 0xff000000.toInt()) {
|
||||
backColor = palette[backColor]
|
||||
}
|
||||
|
||||
// Reverse video here if _one and only one_ of the reverse flags are set:
|
||||
val reverseVideoHere = reverseVideo xor ((effect and TextStyle.CHARACTER_ATTRIBUTE_INVERSE) != 0)
|
||||
if (reverseVideoHere) {
|
||||
val tmp = foreColor
|
||||
foreColor = backColor
|
||||
backColor = tmp
|
||||
}
|
||||
|
||||
var left = startColumn * fontWidth
|
||||
var right = left + runWidthColumns * fontWidth
|
||||
|
||||
var adjustedMes = mes / fontWidth
|
||||
var savedMatrix = false
|
||||
if (kotlin.math.abs(adjustedMes - runWidthColumns) > 0.01) {
|
||||
canvas.save()
|
||||
canvas.scale(runWidthColumns / adjustedMes, 1f)
|
||||
left *= adjustedMes / runWidthColumns
|
||||
right *= adjustedMes / runWidthColumns
|
||||
savedMatrix = true
|
||||
}
|
||||
|
||||
if (backColor != palette[TextStyle.COLOR_INDEX_BACKGROUND]) {
|
||||
// Only draw non-default background.
|
||||
textPaint.color = backColor
|
||||
canvas.drawRect(left, y - fontLineSpacingAndAscent + fontAscent, right, y, textPaint)
|
||||
}
|
||||
|
||||
if (cursor != 0) {
|
||||
textPaint.color = cursor
|
||||
var cursorHeight = (fontLineSpacingAndAscent - fontAscent).toFloat()
|
||||
if (cursorStyle == TerminalEmulator.TERMINAL_CURSOR_STYLE_UNDERLINE) cursorHeight /= 4f
|
||||
else if (cursorStyle == TerminalEmulator.TERMINAL_CURSOR_STYLE_BAR) right -= ((right - left) * 3) / 4f
|
||||
canvas.drawRect(left, y - cursorHeight, right, y, textPaint)
|
||||
}
|
||||
|
||||
if ((effect and TextStyle.CHARACTER_ATTRIBUTE_INVISIBLE) == 0) {
|
||||
if (dim) {
|
||||
var red = 0xFF and (foreColor shr 16)
|
||||
var green = 0xFF and (foreColor shr 8)
|
||||
var blue = 0xFF and foreColor
|
||||
// Dim color handling used by libvte which in turn took it from xterm
|
||||
// (https://bug735245.bugzilla-attachments.gnome.org/attachment.cgi?id=284267):
|
||||
red = red * 2 / 3
|
||||
green = green * 2 / 3
|
||||
blue = blue * 2 / 3
|
||||
foreColor = -0x1000000 or (red shl 16) or (green shl 8) or blue
|
||||
}
|
||||
|
||||
textPaint.isFakeBoldText = bold
|
||||
textPaint.isUnderlineText = underline
|
||||
textPaint.textSkewX = if (italic) -0.35f else 0f
|
||||
textPaint.isStrikeThruText = strikeThrough
|
||||
textPaint.color = foreColor
|
||||
|
||||
// The text alignment is the default Paint.Align.LEFT.
|
||||
canvas.drawTextRun(
|
||||
text, startCharIndex, runWidthChars, startCharIndex, runWidthChars,
|
||||
left, y - fontLineSpacingAndAscent, false, textPaint,
|
||||
)
|
||||
}
|
||||
|
||||
if (savedMatrix) canvas.restore()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.topjohnwu.magisk.ui.terminal
|
||||
|
||||
import android.graphics.Typeface
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.rememberScrollableState
|
||||
import androidx.compose.foundation.gestures.scrollable
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
||||
import androidx.compose.ui.graphics.nativeCanvas
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.topjohnwu.magisk.terminal.TerminalEmulator
|
||||
import kotlin.math.max
|
||||
|
||||
@Composable
|
||||
fun TerminalScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
onEmulatorCreated: (TerminalEmulator) -> Unit = {},
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
val renderer = remember {
|
||||
val textSizePx = with(density) { 12.sp.toPx().toInt() }
|
||||
TerminalRenderer(textSizePx, Typeface.MONOSPACE)
|
||||
}
|
||||
|
||||
var emulator by remember { mutableStateOf<TerminalEmulator?>(null) }
|
||||
var updateTick by remember { mutableIntStateOf(0) }
|
||||
var topRow by remember { mutableIntStateOf(0) }
|
||||
var scrolledToBottom by remember { mutableStateOf(true) }
|
||||
|
||||
BoxWithConstraints(modifier = modifier) {
|
||||
val widthPx = constraints.maxWidth
|
||||
val heightPx = constraints.maxHeight
|
||||
val cols = max(4, (widthPx / renderer.fontWidth).toInt())
|
||||
val rows = max(4, (heightPx - renderer.fontLineSpacingAndAscent) / renderer.fontLineSpacing)
|
||||
val lineHeight = renderer.fontLineSpacing.toFloat()
|
||||
|
||||
LaunchedEffect(cols, rows) {
|
||||
val emu = emulator
|
||||
if (emu == null) {
|
||||
val newEmu = TerminalEmulator(cols, rows, renderer.fontWidth.toInt(), renderer.fontLineSpacing, null)
|
||||
newEmu.onScreenUpdate = {
|
||||
if (scrolledToBottom) topRow = 0
|
||||
updateTick++
|
||||
}
|
||||
emulator = newEmu
|
||||
onEmulatorCreated(newEmu)
|
||||
} else {
|
||||
emu.resize(cols, rows, renderer.fontWidth.toInt(), renderer.fontLineSpacing)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black)
|
||||
.scrollable(
|
||||
orientation = Orientation.Vertical,
|
||||
state = rememberScrollableState { delta ->
|
||||
val emu = emulator ?: return@rememberScrollableState 0f
|
||||
val minTop = -emu.screen.activeTranscriptRows
|
||||
val rowDelta = -(delta / lineHeight).toInt()
|
||||
if (rowDelta != 0) {
|
||||
val newTopRow = (topRow + rowDelta).coerceIn(minTop, 0)
|
||||
topRow = newTopRow
|
||||
scrolledToBottom = newTopRow >= 0
|
||||
}
|
||||
delta
|
||||
}
|
||||
)
|
||||
.drawBehind {
|
||||
@Suppress("UNUSED_EXPRESSION")
|
||||
updateTick
|
||||
val emu = emulator ?: return@drawBehind
|
||||
drawIntoCanvas { canvas ->
|
||||
renderer.render(emu, canvas.nativeCanvas, topRow, -1, -1, -1, -1)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package com.topjohnwu.magisk.ui.terminal
|
||||
|
||||
import com.termux.terminal.TerminalSession
|
||||
import com.termux.terminal.TerminalSessionClient
|
||||
import com.termux.view.TerminalView
|
||||
import timber.log.Timber
|
||||
|
||||
class TerminalSessionCallback(
|
||||
private val onFinished: (TerminalSession) -> Unit = {},
|
||||
) : TerminalSessionClient {
|
||||
|
||||
var terminalView: TerminalView? = null
|
||||
|
||||
override fun onTextChanged(changedSession: TerminalSession) {
|
||||
terminalView?.onScreenUpdated()
|
||||
}
|
||||
|
||||
override fun onTitleChanged(changedSession: TerminalSession) {}
|
||||
|
||||
override fun onSessionFinished(finishedSession: TerminalSession) {
|
||||
onFinished(finishedSession)
|
||||
}
|
||||
|
||||
override fun onCopyTextToClipboard(session: TerminalSession, text: String) {}
|
||||
|
||||
override fun onPasteTextFromClipboard(session: TerminalSession?) {}
|
||||
|
||||
override fun onBell(session: TerminalSession) {}
|
||||
|
||||
override fun onColorsChanged(session: TerminalSession) {}
|
||||
|
||||
override fun onTerminalCursorStateChange(state: Boolean) {}
|
||||
|
||||
override fun getTerminalCursorStyle(): Int = 0
|
||||
|
||||
override fun logError(tag: String, message: String) { Timber.tag(tag).e(message) }
|
||||
override fun logWarn(tag: String, message: String) { Timber.tag(tag).w(message) }
|
||||
override fun logInfo(tag: String, message: String) { Timber.tag(tag).i(message) }
|
||||
override fun logDebug(tag: String, message: String) { Timber.tag(tag).d(message) }
|
||||
override fun logVerbose(tag: String, message: String) { Timber.tag(tag).v(message) }
|
||||
|
||||
override fun logStackTraceWithMessage(tag: String, message: String, e: Exception) {
|
||||
Timber.tag(tag).e(e, message)
|
||||
}
|
||||
|
||||
override fun logStackTrace(tag: String, e: Exception) {
|
||||
Timber.tag(tag).e(e)
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,6 @@ activity-compose = "1.12.4"
|
||||
miuix = "0.8.5"
|
||||
navigation3 = "1.1.0-alpha05"
|
||||
navigationevent = "1.0.2"
|
||||
termux-terminal = "0.118.0"
|
||||
|
||||
[libraries]
|
||||
bcpkix = { module = "org.bouncycastle:bcpkix-jdk18on", version = "1.83" }
|
||||
commons-compress = { module = "org.apache.commons:commons-compress", version = "1.28.0" }
|
||||
@@ -64,9 +62,6 @@ navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", ver
|
||||
navigationevent-compose = { module = "androidx.navigationevent:navigationevent-compose", version.ref = "navigationevent" }
|
||||
lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "lifecycle" }
|
||||
|
||||
# Terminal
|
||||
termux-terminal-view = { module = "com.termux.termux-app:terminal-view", version.ref = "termux-terminal" }
|
||||
|
||||
# Build plugins
|
||||
android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "android" }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user