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:
LoveSy
2026-03-05 22:28:52 +08:00
committed by topjohnwu
parent 162b84661b
commit 4d758f871b
17 changed files with 3587 additions and 249 deletions
-2
View File
@@ -52,6 +52,4 @@ dependencies {
implementation(libs.navigationevent.compose)
implementation(libs.lifecycle.viewmodel.navigation3)
// Terminal
implementation(libs.termux.terminal.view)
}
-2
View File
@@ -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)
}
}
-5
View File
@@ -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" }