From dc57745ccd4544f3cdc42f900b2eac186551a577 Mon Sep 17 00:00:00 2001 From: saicharankandukuri Date: Fri, 6 Nov 2026 12:11:12 +0530 Subject: [PATCH 01/14] feat(terminal): show tab shutdown lifecycle Track close state per tab and expose graceful, forced, and failed shutdown phases to the terminal UI. Disable tab interaction while shutdown is in progress, keep failed closes retryable, and label the final-tab action as stopping Linux. Generated-by: LLM --- .../runtime/RuntimeSupervisorService.kt | 68 +++++++-- .../udroid/runtime/TerminalTabs.kt | 38 +++++ .../org/randomcoder/udroid/ui/TerminalPage.kt | 142 ++++++++++++------ .../udroid/runtime/TerminalTabRegistryTest.kt | 19 +++ 4 files changed, 209 insertions(+), 58 deletions(-) diff --git a/app/src/main/java/org/randomcoder/udroid/runtime/RuntimeSupervisorService.kt b/app/src/main/java/org/randomcoder/udroid/runtime/RuntimeSupervisorService.kt index 9ae0210..7b25f0a 100644 --- a/app/src/main/java/org/randomcoder/udroid/runtime/RuntimeSupervisorService.kt +++ b/app/src/main/java/org/randomcoder/udroid/runtime/RuntimeSupervisorService.kt @@ -61,7 +61,6 @@ class RuntimeSupervisorService : Service() { private val binder = RuntimeBinder() private val mainHandler = Handler(Looper.getMainLooper()) private val terminalTabs = TerminalTabRegistry() - private val closingTerminalIds = mutableSetOf() private var nextTerminalNumber = 1 private var terminalCreationInFlight = false private val ownedDesktop = AtomicReference(null) @@ -257,7 +256,6 @@ class RuntimeSupervisorService : Service() { attachedViews.clear() mainHandler.removeCallbacksAndMessages(null) terminalTabs.clear().forEach { it.value.finishIfRunning() } - closingTerminalIds.clear() pendingDesktopRestart.set(null) desktopLaunchToken.set(null) ownedDesktop.getAndSet(null)?.let { terminateDesktopProcess(it, OsConstants.SIGKILL) } @@ -289,6 +287,8 @@ class RuntimeSupervisorService : Service() { pid = tab.value.pid.takeIf { it > 0 }?.toLong(), running = tab.value.isRunning, active = tab.id == terminalTabs.activeId, + closeState = tab.closeState, + closeError = tab.closeError, ) } @@ -375,6 +375,12 @@ class RuntimeSupervisorService : Service() { } fun selectTerminalTab(id: String): Boolean { + val requested = terminalTabs.get(id) ?: return false + if (requested.closeState == TerminalTabCloseState.CLOSING || + requested.closeState == TerminalTabCloseState.FORCE_CLOSING + ) { + return false + } val tab = terminalTabs.select(id) ?: return false attachedViews.forEach { view -> if (view.mTermSession !== tab.value) view.attachSession(tab.value) @@ -404,13 +410,14 @@ class RuntimeSupervisorService : Service() { } fun closeTerminalTab(id: String): Boolean { - val tab = terminalTabs.get(id) ?: return false + if (terminalTabs.get(id) == null) return false if (terminalTabs.size() == 1) { stopRuntime(userRequested = true) return true } - closingTerminalIds += id - terminateTerminalTab(tab) + val closingTab = terminalTabs.beginClose(id) ?: return false + publishTerminalCloseState("Closing ${closingTab.title}") + terminateTerminalTab(closingTab) return true } @@ -1443,7 +1450,6 @@ class RuntimeSupervisorService : Service() { return } terminalTabs.clear().forEach { it.value.finishIfRunning() } - closingTerminalIds.clear() nextTerminalNumber = 1 val bootId = UUID.randomUUID().toString() @@ -1636,7 +1642,7 @@ class RuntimeSupervisorService : Service() { private fun handleSessionFinished(session: TerminalSession) { val tab = terminalTabs.findByValue(session) ?: return - val individuallyClosed = closingTerminalIds.remove(tab.id) + val individuallyClosed = tab.closeState != TerminalTabCloseState.OPEN terminalTabs.remove(tab.id) val exitCode = session.exitStatus val beforeExit = app.runtimeState.current() @@ -1749,7 +1755,7 @@ class RuntimeSupervisorService : Service() { return } sessions.forEach { tab -> - closingTerminalIds += tab.id + terminalTabs.beginClose(tab.id) terminateTerminalTab(tab) } } @@ -1771,28 +1777,61 @@ class RuntimeSupervisorService : Service() { bootId = app.runtimeState.current().bootId, fields = mapOf("tab_id" to tab.id, "child_pid" to session.pid), ) + forceCloseTerminalTab(tab, session) + return } mainHandler.postDelayed( { if (terminalTabs.get(tab.id)?.value === session && session.isRunning) { + forceCloseTerminalTab(tab, session) + } + }, + GRACEFUL_STOP_TIMEOUT_MS, + ) + } + + private fun forceCloseTerminalTab( + tab: TerminalTab, + session: TerminalSession, + ) { + if (terminalTabs.get(tab.id)?.value !== session || !session.isRunning) return + terminalTabs.markForceClosing(tab.id) + publishTerminalCloseState("${tab.title} did not close; forcing it to stop") + app.journal.append( + component = "supervisor", + severity = "warning", + event = "terminal_force_stop", + message = "${tab.title} did not exit after SIGTERM; forcing it to stop", + bootId = app.runtimeState.current().bootId, + fields = mapOf("tab_id" to tab.id, "child_pid" to session.pid), + ) + session.finishIfRunning() + mainHandler.postDelayed( + { + if (terminalTabs.get(tab.id)?.value === session && session.isRunning) { + val message = "${tab.title} is still running after force stop" + terminalTabs.markCloseFailed(tab.id, message) + publishTerminalCloseState(message) app.journal.append( component = "supervisor", - severity = "warning", - event = "terminal_force_stop", - message = "${tab.title} ignored SIGTERM; forcing it to stop", + severity = "error", + event = "terminal_stop_failed", + message = message, bootId = app.runtimeState.current().bootId, fields = mapOf("tab_id" to tab.id, "child_pid" to session.pid), ) - session.finishIfRunning() } }, - GRACEFUL_STOP_TIMEOUT_MS, + FORCE_STOP_TIMEOUT_MS, ) } + private fun publishTerminalCloseState(message: String) { + publishState(app.runtimeState.update { it.copy(message = message) }) + } + private fun publishStoppedState() { terminalTabs.clear() - closingTerminalIds.clear() val stopped = app.runtimeState.update { it.copy( @@ -2044,6 +2083,7 @@ class RuntimeSupervisorService : Service() { private const val NOTIFICATION_CHANNEL = "runtime-supervisor" private const val NOTIFICATION_ID = 1001 private const val GRACEFUL_STOP_TIMEOUT_MS = 3_000L + private const val FORCE_STOP_TIMEOUT_MS = 3_000L private const val TRANSCRIPT_ROWS = 8_000 private const val DEFAULT_COLUMNS = 80 private const val DEFAULT_ROWS = 24 diff --git a/app/src/main/java/org/randomcoder/udroid/runtime/TerminalTabs.kt b/app/src/main/java/org/randomcoder/udroid/runtime/TerminalTabs.kt index 054847c..73b1b21 100644 --- a/app/src/main/java/org/randomcoder/udroid/runtime/TerminalTabs.kt +++ b/app/src/main/java/org/randomcoder/udroid/runtime/TerminalTabs.kt @@ -7,8 +7,17 @@ data class TerminalTabSnapshot( val pid: Long?, val running: Boolean, val active: Boolean, + val closeState: TerminalTabCloseState = TerminalTabCloseState.OPEN, + val closeError: String? = null, ) +enum class TerminalTabCloseState { + OPEN, + CLOSING, + FORCE_CLOSING, + FAILED, +} + fun terminalDistroTitle(rawName: String?): String = when { rawName.isNullOrBlank() -> "Linux" @@ -29,6 +38,8 @@ internal data class TerminalTab( var title: String, val rootfsName: String, val value: T, + var closeState: TerminalTabCloseState = TerminalTabCloseState.OPEN, + var closeError: String? = null, ) internal class TerminalTabRegistry { @@ -68,6 +79,33 @@ internal class TerminalTabRegistry { return tab } + fun beginClose(id: String): TerminalTab? { + val tab = tabs[id] ?: return null + if (tab.closeState == TerminalTabCloseState.CLOSING || + tab.closeState == TerminalTabCloseState.FORCE_CLOSING + ) { + return null + } + tab.closeState = TerminalTabCloseState.CLOSING + tab.closeError = null + return tab + } + + fun markForceClosing(id: String): TerminalTab? = + tabs[id]?.also { + it.closeState = TerminalTabCloseState.FORCE_CLOSING + it.closeError = null + } + + fun markCloseFailed( + id: String, + message: String, + ): TerminalTab? = + tabs[id]?.also { + it.closeState = TerminalTabCloseState.FAILED + it.closeError = message + } + fun remove(id: String): TerminalTab? { val index = tabs.keys.indexOf(id) val removed = tabs.remove(id) ?: return null diff --git a/app/src/main/java/org/randomcoder/udroid/ui/TerminalPage.kt b/app/src/main/java/org/randomcoder/udroid/ui/TerminalPage.kt index de7f9a6..7af8046 100644 --- a/app/src/main/java/org/randomcoder/udroid/ui/TerminalPage.kt +++ b/app/src/main/java/org/randomcoder/udroid/ui/TerminalPage.kt @@ -77,6 +77,7 @@ import org.randomcoder.udroid.runtime.InstalledRootfs import org.randomcoder.udroid.runtime.RuntimePhase import org.randomcoder.udroid.runtime.RuntimeSnapshot import org.randomcoder.udroid.runtime.RuntimeSupervisorService +import org.randomcoder.udroid.runtime.TerminalTabCloseState import org.randomcoder.udroid.runtime.TerminalTabSnapshot import org.randomcoder.udroid.runtime.terminalDistroTitle import kotlin.math.roundToInt @@ -141,13 +142,18 @@ fun InteractiveTerminalPage( tabs = tabs, onSelect = { id -> service?.selectTerminalTab(id) }, onRename = { tab -> renamingTab = tab }, - onClose = { id -> service?.closeTerminalTab(id) }, + onClose = { id -> + if (service?.closeTerminalTab(id) != true) { + tabError = "Could not close terminal" + } + }, creatingTab = creatingTab, onClone = { createTab(activeTab?.rootfsName) }, ) } - tabError?.let { message -> + val closeError = tabs.firstOrNull { it.closeState == TerminalTabCloseState.FAILED }?.closeError + (tabError ?: closeError)?.let { message -> Text( text = message, modifier = @@ -219,7 +225,10 @@ private fun TerminalSessionBar( onChooseDistro: () -> Unit, onStop: () -> Unit, ) { - val running = session?.isRunning == true && !stopping + val tabClosing = + activeTab?.closeState == TerminalTabCloseState.CLOSING || + activeTab?.closeState == TerminalTabCloseState.FORCE_CLOSING + val running = session?.isRunning == true && !stopping && !tabClosing Row( modifier = Modifier @@ -267,6 +276,12 @@ private fun TerminalSessionBar( Text( when { stopping -> "Stopping terminal…" + activeTab?.closeState == TerminalTabCloseState.CLOSING -> + "Closing terminal…" + activeTab?.closeState == TerminalTabCloseState.FORCE_CLOSING -> + "Force closing terminal…" + activeTab?.closeState == TerminalTabCloseState.FAILED -> + activeTab.closeError ?: "Could not close terminal" running -> "root • ${Build.SUPPORTED_ABIS.firstOrNull().orEmpty()}" + " • PID ${session?.pid}" @@ -361,6 +376,7 @@ private fun TerminalTabsRow( onSelect = { onSelect(tab.id) }, onRename = { onRename(tab) }, onClose = { onClose(tab.id) }, + stopsLinux = tabs.size == 1, ) } } @@ -402,14 +418,18 @@ private fun TerminalTabChip( onSelect: () -> Unit, onRename: () -> Unit, onClose: () -> Unit, + stopsLinux: Boolean, ) { var menuExpanded by remember { mutableStateOf(false) } + val closing = + tab.closeState == TerminalTabCloseState.CLOSING || + tab.closeState == TerminalTabCloseState.FORCE_CLOSING Surface( modifier = modifier .width(144.dp) .height(46.dp) - .clickable(onClick = onSelect), + .clickable(enabled = !closing, onClick = onSelect), color = if (tab.active) UdroidTerminal else UdroidTerminalRaised, shape = RoundedCornerShape(topStart = 10.dp, topEnd = 10.dp), border = @@ -423,54 +443,88 @@ private fun TerminalTabChip( modifier = Modifier.padding(start = 10.dp, end = 1.dp), verticalAlignment = Alignment.CenterVertically, ) { - Icon( - imageVector = Icons.Outlined.Terminal, - contentDescription = null, - modifier = Modifier.size(16.dp), - tint = if (tab.active) UdroidTerminalGreen else UdroidTerminalMuted, - ) + if (closing) { + CircularProgressIndicator( + modifier = Modifier.size(16.dp), + color = UdroidTerminalGreen, + strokeWidth = 2.dp, + ) + } else { + Icon( + imageVector = Icons.Outlined.Terminal, + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = + if (tab.closeState == TerminalTabCloseState.FAILED) { + MaterialTheme.colorScheme.error + } else if (tab.active) { + UdroidTerminalGreen + } else { + UdroidTerminalMuted + }, + ) + } Spacer(Modifier.width(7.dp)) Text( - text = tab.title, + text = + when (tab.closeState) { + TerminalTabCloseState.CLOSING -> "${tab.title} · closing" + TerminalTabCloseState.FORCE_CLOSING -> "${tab.title} · force closing" + TerminalTabCloseState.FAILED -> "${tab.title} · close failed" + TerminalTabCloseState.OPEN -> tab.title + }, modifier = Modifier.weight(1f), color = if (tab.active) UdroidTerminalText else UdroidTerminalMuted, style = MaterialTheme.typography.labelMedium, maxLines = 1, overflow = TextOverflow.Ellipsis, ) - Box { - IconButton( - modifier = Modifier.size(36.dp), - onClick = { menuExpanded = true }, - ) { - Icon( - imageVector = Icons.Outlined.MoreVert, - contentDescription = "${tab.title} settings", - modifier = Modifier.size(18.dp), - tint = UdroidTerminalMuted, - ) - } - DropdownMenu( - expanded = menuExpanded, - onDismissRequest = { menuExpanded = false }, - ) { - DropdownMenuItem( - text = { Text("Rename") }, - onClick = { - menuExpanded = false - onRename() - }, - ) - DropdownMenuItem( - text = { Text("Close terminal") }, - leadingIcon = { - Icon(Icons.Outlined.Close, contentDescription = null) - }, - onClick = { - menuExpanded = false - onClose() - }, - ) + if (!closing) { + Box { + IconButton( + modifier = Modifier.size(36.dp), + onClick = { menuExpanded = true }, + ) { + Icon( + imageVector = Icons.Outlined.MoreVert, + contentDescription = "${tab.title} settings", + modifier = Modifier.size(18.dp), + tint = UdroidTerminalMuted, + ) + } + DropdownMenu( + expanded = menuExpanded, + onDismissRequest = { menuExpanded = false }, + ) { + DropdownMenuItem( + text = { Text("Rename") }, + onClick = { + menuExpanded = false + onRename() + }, + ) + DropdownMenuItem( + text = { + Text( + when { + tab.closeState == TerminalTabCloseState.FAILED -> "Retry close" + stopsLinux -> "Stop Linux" + else -> "Close tab" + }, + ) + }, + leadingIcon = { + Icon( + if (stopsLinux) Icons.Filled.StopCircle else Icons.Outlined.Close, + contentDescription = null, + ) + }, + onClick = { + menuExpanded = false + onClose() + }, + ) + } } } } diff --git a/app/src/test/java/org/randomcoder/udroid/runtime/TerminalTabRegistryTest.kt b/app/src/test/java/org/randomcoder/udroid/runtime/TerminalTabRegistryTest.kt index 0ba2812..5b5324c 100644 --- a/app/src/test/java/org/randomcoder/udroid/runtime/TerminalTabRegistryTest.kt +++ b/app/src/test/java/org/randomcoder/udroid/runtime/TerminalTabRegistryTest.kt @@ -73,6 +73,25 @@ class TerminalTabRegistryTest { assertEquals("debian-bookworm", registry.get("debian")?.rootfsName) } + @Test + fun `closing state prevents duplicate close and supports retry after failure`() { + val registry = TerminalTabRegistry() + registry.add(tab("one")) + + assertEquals(TerminalTabCloseState.CLOSING, registry.beginClose("one")?.closeState) + assertNull(registry.beginClose("one")) + assertEquals( + TerminalTabCloseState.FORCE_CLOSING, + registry.markForceClosing("one")?.closeState, + ) + assertEquals( + TerminalTabCloseState.FAILED, + registry.markCloseFailed("one", "Could not close")?.closeState, + ) + assertEquals(TerminalTabCloseState.CLOSING, registry.beginClose("one")?.closeState) + assertNull(registry.get("one")?.closeError) + } + @Test fun `rootfs names become readable distro titles`() { assertEquals("Ubuntu Jammy", terminalDistroTitle("udroid-jammy-raw")) From 242bbdf88f4d001aa27e564fb3436419c798d2b7 Mon Sep 17 00:00:00 2001 From: saicharankandukuri Date: Fri, 6 Nov 2026 12:30:16 +0530 Subject: [PATCH 02/14] fix(runtime): support process monitors in PRoot Generate a complete synthetic CPU topology and boot time when Android blocks the host proc data. Mount safe empty views for restricted network, storage, battery, filesystem, and swap metrics so htop and bpytop can remain operational. Validated with testDebugUnitTest, lintDev, assembleDev, and device timeout probes for htop and bpytop. Generated-by: LLM --- .../udroid/runtime/ProotMountProfiles.kt | 140 +++++++++++++----- .../udroid/runtime/ProotMountProfilesTest.kt | 40 ++++- 2 files changed, 140 insertions(+), 40 deletions(-) diff --git a/app/src/main/java/org/randomcoder/udroid/runtime/ProotMountProfiles.kt b/app/src/main/java/org/randomcoder/udroid/runtime/ProotMountProfiles.kt index 0c2688c..08194f1 100644 --- a/app/src/main/java/org/randomcoder/udroid/runtime/ProotMountProfiles.kt +++ b/app/src/main/java/org/randomcoder/udroid/runtime/ProotMountProfiles.kt @@ -150,12 +150,13 @@ object ProotMountResolver { } else { emptyList() } - val selinuxMount = - prepareSelinuxMount(runtimeDirectory(context, rootfs).resolve("sysdata")) - ?.takeIf { - profile.isDefaultEnabled("android.sys") && - !isShadowedByCustomTarget(it.guestTarget, customTargets) - } + val systemCompatibilityMounts = + if (profile.isDefaultEnabled("android.sys")) { + prepareSystemCompatibilityMounts(runtimeDirectory(context, rootfs).resolve("sysdata")) + .filterNot { isShadowedByCustomTarget(it.guestTarget, customTargets) } + } else { + emptyList() + } val sharedMemoryMount = prepareSharedMemoryMount(runtimeDirectory(context, rootfs).resolve("shm")) .takeIf { !isShadowedByCustomTarget(it.guestTarget, customTargets) } @@ -164,7 +165,8 @@ object ProotMountResolver { sessionMounts = sessionMounts + compatibilityMounts + - listOfNotNull(selinuxMount, sharedMemoryMount), + systemCompatibilityMounts + + listOfNotNull(sharedMemoryMount), ) } @@ -253,49 +255,63 @@ object ProotMountResolver { @Synchronized internal fun prepareCompatibilityMounts( directory: File, + nowEpochSeconds: Long = System.currentTimeMillis() / 1_000L, + cpuCount: Int = Runtime.getRuntime().availableProcessors(), readable: (String) -> Boolean = ::isReadable, ): List { if (!prepareDirectory(directory)) return emptyList() - return COMPATIBILITY_FILES.mapNotNull { entry -> + val files = compatibilityFiles(nowEpochSeconds, cpuCount.coerceAtLeast(1)).mapNotNull { entry -> if (readable(entry.guestTarget)) return@mapNotNull null val source = File(directory, entry.name) - val staging = runCatching { - Files.createTempFile(directory.toPath(), ".compat-", ".tmp").toFile() - }.getOrNull() ?: return@mapNotNull null - try { - FileOutputStream(staging).use { output -> - output.write(entry.contents.toByteArray(Charsets.UTF_8)) - output.fd.sync() - } - moveReplacing(staging, source) - } catch (_: Exception) { - return@mapNotNull null - } finally { - staging.delete() - } - if (!Files.isRegularFile(source.toPath(), LinkOption.NOFOLLOW_LINKS)) { - return@mapNotNull null - } + if (!writeCompatibilityFile(directory, source, entry.contents)) return@mapNotNull null ResolvedProotMount( hostSource = source.absolutePath, guestTarget = entry.guestTarget, origin = "runtime:compatibility", ) } + val procNet = + if (readable("/proc/net/dev")) { + null + } else { + val source = File(directory, "proc_net") + val dev = File(source, "dev") + if ( + prepareDirectory(source) && + writeCompatibilityFile(source, dev, PROC_NET_DEV_HEADER) + ) { + ResolvedProotMount( + hostSource = source.absolutePath, + guestTarget = "/proc/net", + origin = "runtime:compatibility", + ) + } else { + null + } + } + return files + listOfNotNull(procNet) } - private fun prepareSelinuxMount(directory: File): ResolvedProotMount? { - if (!prepareDirectory(directory)) return null - val empty = File(directory, "sys_empty") - if (Files.isSymbolicLink(empty.toPath()) || (empty.exists() && !empty.isDirectory)) { - if (!empty.delete()) return null + @Synchronized + internal fun prepareSystemCompatibilityMounts( + directory: File, + readable: (String) -> Boolean = ::isDirectoryReadable, + ): List { + if (!prepareDirectory(directory)) return emptyList() + return listOf( + Triple("sys_empty", "/sys/fs/selinux", false), + Triple("sys_block", "/sys/block", true), + Triple("power_supply", "/sys/class/power_supply", true), + ).mapNotNull { (name, guestTarget, checkReadable) -> + if (checkReadable && readable(guestTarget)) return@mapNotNull null + val source = File(directory, name) + if (!prepareDirectory(source)) return@mapNotNull null + ResolvedProotMount( + hostSource = source.absolutePath, + guestTarget = guestTarget, + origin = "runtime:compatibility", + ) } - if (!empty.mkdirs() && !empty.isDirectory) return null - return ResolvedProotMount( - hostSource = empty.absolutePath, - guestTarget = "/sys/fs/selinux", - origin = "runtime:compatibility", - ) } private fun prepareDirectory(directory: File): Boolean = @@ -324,6 +340,8 @@ object ProotMountResolver { private fun isReadable(path: String): Boolean = runCatching { File(path).inputStream().use { it.read() } }.isSuccess + private fun isDirectoryReadable(path: String): Boolean = File(path).list() != null + fun defaults( x11SocketDirectory: String? = null, audioAuthDirectory: String? = null, @@ -379,17 +397,35 @@ object ProotMountResolver { val contents: String, ) - private val COMPATIBILITY_FILES = + private fun compatibilityFiles( + nowEpochSeconds: Long, + cpuCount: Int, + ) = listOf( CompatibilityFile("loadavg", "/proc/loadavg", "0.00 0.00 0.00 1/1 1\n"), - CompatibilityFile("stat", "/proc/stat", "cpu 1 0 1 1 0 0 0 0 0 0\n"), + CompatibilityFile( + "stat", + "/proc/stat", + buildString { + append("cpu $cpuCount 0 $cpuCount $cpuCount 0 0 0 0 0 0\n") + repeat(cpuCount) { index -> + append("cpu$index 1 0 1 1 0 0 0 0 0 0\n") + } + append("btime $nowEpochSeconds\n") + }, + ), CompatibilityFile("uptime", "/proc/uptime", "0.00 0.00\n"), CompatibilityFile( "version", "/proc/version", "Linux version 5.4.0-udroid-faked (udroid@android)\n", ), - CompatibilityFile("vmstat", "/proc/vmstat", "nr_free_pages 0\n"), + CompatibilityFile( + "vmstat", + "/proc/vmstat", + "nr_free_pages 0\npswpin 0\npswpout 0\n", + ), + CompatibilityFile("filesystems", "/proc/filesystems", ""), CompatibilityFile("cap_last_cap", "/proc/sys/kernel/cap_last_cap", "40\n"), CompatibilityFile( "inotify_max_user_watches", @@ -401,7 +437,33 @@ object ProotMountResolver { CompatibilityFile("pci_devices", "/proc/bus/pci/devices", ""), ) + private fun writeCompatibilityFile( + directory: File, + target: File, + contents: String, + ): Boolean { + val staging = runCatching { + Files.createTempFile(directory.toPath(), ".compat-", ".tmp").toFile() + }.getOrNull() ?: return false + return try { + FileOutputStream(staging).use { output -> + output.write(contents.toByteArray(Charsets.UTF_8)) + output.fd.sync() + } + moveReplacing(staging, target) + Files.isRegularFile(target.toPath(), LinkOption.NOFOLLOW_LINKS) + } catch (_: Exception) { + false + } finally { + staging.delete() + } + } + private val SAFE_SYSTEM_ID = Regex("[A-Za-z0-9][A-Za-z0-9._-]{0,95}") + private const val PROC_NET_DEV_HEADER = + "Inter-| Receive | Transmit\n" + + " face |bytes packets errs drop fifo frame compressed multicast|" + + "bytes packets errs drop fifo colls carrier compressed\n" } class ProotMountProfileStore(context: Context) { diff --git a/app/src/test/java/org/randomcoder/udroid/runtime/ProotMountProfilesTest.kt b/app/src/test/java/org/randomcoder/udroid/runtime/ProotMountProfilesTest.kt index 42a721d..480ecc3 100644 --- a/app/src/test/java/org/randomcoder/udroid/runtime/ProotMountProfilesTest.kt +++ b/app/src/test/java/org/randomcoder/udroid/runtime/ProotMountProfilesTest.kt @@ -116,7 +116,11 @@ class ProotMountProfilesTest { Files.createSymbolicLink(directory.resolve("pci_devices").toPath(), victim.toPath()) val mounts = - ProotMountResolver.prepareCompatibilityMounts(directory) { path -> + ProotMountResolver.prepareCompatibilityMounts( + directory = directory, + nowEpochSeconds = 1_234L, + cpuCount = 2, + ) { path -> path == "/proc/version" } @@ -126,15 +130,49 @@ class ProotMountProfilesTest { }) assertEquals("preserve", victim.readText()) assertFalse(Files.isSymbolicLink(directory.resolve("pci_devices").toPath())) + assertEquals( + "cpu 2 0 2 2 0 0 0 0 0 0\n" + + "cpu0 1 0 1 1 0 0 0 0 0 0\n" + + "cpu1 1 0 1 1 0 0 0 0 0 0\n" + + "btime 1234\n", + mounts.single { it.guestTarget == "/proc/stat" }.let { + java.io.File(it.hostSource).readText() + }, + ) + assertEquals( + "Inter-| Receive | Transmit\n" + + " face |bytes packets errs drop fifo frame compressed multicast|" + + "bytes packets errs drop fifo colls carrier compressed\n", + mounts.single { it.guestTarget == "/proc/net" } + .let { java.io.File(it.hostSource, "dev").readText() }, + ) assertEquals( "4096\n", mounts.single { it.guestTarget.endsWith("max_user_watches") }.let { java.io.File(it.hostSource).readText() }, ) + assertEquals( + "nr_free_pages 0\npswpin 0\npswpout 0\n", + mounts.single { it.guestTarget == "/proc/vmstat" } + .let { java.io.File(it.hostSource).readText() }, + ) assertTrue(mounts.all { it.origin == "runtime:compatibility" }) } + @Test + fun `restricted system metric directories receive empty compatibility mounts`() { + val directory = Files.createTempDirectory("udroid-sysdata").toFile() + + val mounts = ProotMountResolver.prepareSystemCompatibilityMounts(directory) { false } + + assertEquals( + listOf("/sys/fs/selinux", "/sys/block", "/sys/class/power_supply"), + mounts.map(ResolvedProotMount::guestTarget), + ) + assertTrue(mounts.all { java.io.File(it.hostSource).isDirectory }) + } + @Test fun `parent custom mount shadows automatic child mounts`() { assertTrue( From 38a5e391db49a746e0dda5026397603911d14f3f Mon Sep 17 00:00:00 2001 From: saicharankandukuri Date: Fri, 6 Nov 2026 13:37:10 +0530 Subject: [PATCH 03/14] feat(terminal): show OSC titles in tabs Display shell and application OSC titles while preserving manual tab names as explicit overrides. Add a Follow shell title action, sanitize untrusted title text, and coalesce animated title refreshes without persisting each frame. Validated with testDebugUnitTest, lintDev, assembleDev, and the animated OSC title probe on Pixel. Generated-by: LLM --- .../org/randomcoder/udroid/MainActivity.kt | 7 ++- .../runtime/RuntimeSupervisorService.kt | 46 +++++++++++++++++-- .../udroid/runtime/TerminalTabs.kt | 39 ++++++++++++++-- .../org/randomcoder/udroid/ui/TerminalPage.kt | 16 ++++++- .../udroid/runtime/TerminalTabRegistryTest.kt | 39 +++++++++++++++- 5 files changed, 137 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/org/randomcoder/udroid/MainActivity.kt b/app/src/main/java/org/randomcoder/udroid/MainActivity.kt index 0b0e349..8d78d59 100644 --- a/app/src/main/java/org/randomcoder/udroid/MainActivity.kt +++ b/app/src/main/java/org/randomcoder/udroid/MainActivity.kt @@ -206,7 +206,11 @@ class MainActivity : ComponentActivity() { context: Context?, intent: Intent?, ) { - refreshFromDisk() + if (intent?.action == RuntimeSupervisorService.ACTION_TERMINAL_TABS_CHANGED) { + snapshot = snapshot.copy(updatedAtEpochMs = System.currentTimeMillis()) + } else { + refreshFromDisk() + } } } @@ -344,6 +348,7 @@ class MainActivity : ComponentActivity() { stateReceiver, IntentFilter().apply { addAction(RuntimeSupervisorService.ACTION_STATE_CHANGED) + addAction(RuntimeSupervisorService.ACTION_TERMINAL_TABS_CHANGED) addAction(InstallerService.ACTION_STATE_CHANGED) addAction(AppUpdateContract.ACTION_STATE_CHANGED) }, diff --git a/app/src/main/java/org/randomcoder/udroid/runtime/RuntimeSupervisorService.kt b/app/src/main/java/org/randomcoder/udroid/runtime/RuntimeSupervisorService.kt index 7b25f0a..19df66d 100644 --- a/app/src/main/java/org/randomcoder/udroid/runtime/RuntimeSupervisorService.kt +++ b/app/src/main/java/org/randomcoder/udroid/runtime/RuntimeSupervisorService.kt @@ -63,6 +63,7 @@ class RuntimeSupervisorService : Service() { private val terminalTabs = TerminalTabRegistry() private var nextTerminalNumber = 1 private var terminalCreationInFlight = false + private var terminalTabsRefreshPending = false private val ownedDesktop = AtomicReference(null) private val ownedGfxstreamHost = AtomicReference(null) private val ownedVirglHosts = ConcurrentHashMap() @@ -93,6 +94,13 @@ class RuntimeSupervisorService : Service() { override fun onTitleChanged(changedSession: TerminalSession) { onTextChanged(changedSession) + val title = normalizeTerminalTitle(changedSession.title) + mainHandler.post { + val tab = terminalTabs.findByValue(changedSession) ?: return@post + if (terminalTabs.updateShellTitle(tab.id, title) != null) { + scheduleTerminalTabsChanged() + } + } } override fun onSessionFinished(finishedSession: TerminalSession) { @@ -283,6 +291,7 @@ class RuntimeSupervisorService : Service() { TerminalTabSnapshot( id = tab.id, title = tab.title, + hasCustomTitle = tab.customTitle != null, rootfsName = tab.rootfsName, pid = tab.value.pid.takeIf { it > 0 }?.toLong(), running = tab.value.isRunning, @@ -394,8 +403,7 @@ class RuntimeSupervisorService : Service() { id: String, requestedTitle: String, ): Boolean { - val title = requestedTitle.trim().take(MAX_TERMINAL_TITLE_CHARS) - if (title.isBlank()) return false + val title = normalizeTerminalTitle(requestedTitle) ?: return false val tab = terminalTabs.rename(id, title) ?: return false publishTerminalTabState("Renamed terminal to ${tab.title}") app.journal.append( @@ -409,6 +417,20 @@ class RuntimeSupervisorService : Service() { return true } + fun followTerminalShellTitle(id: String): Boolean { + val tab = terminalTabs.followShellTitle(id) ?: return false + publishTerminalTabState("Following shell title for ${tab.title}") + app.journal.append( + component = "terminal", + severity = "info", + event = "tab_title_followed", + message = "Terminal tab now follows its shell title", + bootId = app.runtimeState.current().bootId, + fields = mapOf("tab_id" to id, "title" to tab.title), + ) + return true + } + fun closeTerminalTab(id: String): Boolean { if (terminalTabs.get(id) == null) return false if (terminalTabs.size() == 1) { @@ -1583,7 +1605,7 @@ class RuntimeSupervisorService : Service() { val tab = TerminalTab( id = session.mHandle, - title = title, + defaultTitle = title, rootfsName = launch.rootfs.name, value = session, ) @@ -1830,6 +1852,20 @@ class RuntimeSupervisorService : Service() { publishState(app.runtimeState.update { it.copy(message = message) }) } + private fun scheduleTerminalTabsChanged() { + if (terminalTabsRefreshPending) return + terminalTabsRefreshPending = true + mainHandler.postDelayed( + { + terminalTabsRefreshPending = false + sendBroadcast( + Intent(ACTION_TERMINAL_TABS_CHANGED).setPackage(packageName), + ) + }, + TERMINAL_TITLE_REFRESH_INTERVAL_MS, + ) + } + private fun publishStoppedState() { terminalTabs.clear() val stopped = @@ -2072,6 +2108,8 @@ class RuntimeSupervisorService : Service() { companion object { const val ACTION_STATE_CHANGED = "org.randomcoder.udroid.action.STATE_CHANGED" + const val ACTION_TERMINAL_TABS_CHANGED = + "org.randomcoder.udroid.action.TERMINAL_TABS_CHANGED" const val EXTRA_PHASE = "phase" const val EXTRA_UPDATED_AT = "updated-at" const val EXTRA_ROOTFS_NAME = "rootfs-name" @@ -2084,6 +2122,7 @@ class RuntimeSupervisorService : Service() { private const val NOTIFICATION_ID = 1001 private const val GRACEFUL_STOP_TIMEOUT_MS = 3_000L private const val FORCE_STOP_TIMEOUT_MS = 3_000L + private const val TERMINAL_TITLE_REFRESH_INTERVAL_MS = 50L private const val TRANSCRIPT_ROWS = 8_000 private const val DEFAULT_COLUMNS = 80 private const val DEFAULT_ROWS = 24 @@ -2092,7 +2131,6 @@ class RuntimeSupervisorService : Service() { private const val MAX_APP_OUTPUT_CHARS = 2_000 private const val MAX_APP_OUTPUT_LINES = 200 private const val MAX_DESKTOP_OUTPUT_LINES = 400 - private const val MAX_TERMINAL_TITLE_CHARS = 40 private const val DISPLAY_NUMBER = 0 fun start( diff --git a/app/src/main/java/org/randomcoder/udroid/runtime/TerminalTabs.kt b/app/src/main/java/org/randomcoder/udroid/runtime/TerminalTabs.kt index 73b1b21..809290e 100644 --- a/app/src/main/java/org/randomcoder/udroid/runtime/TerminalTabs.kt +++ b/app/src/main/java/org/randomcoder/udroid/runtime/TerminalTabs.kt @@ -3,6 +3,7 @@ package org.randomcoder.udroid.runtime data class TerminalTabSnapshot( val id: String, val title: String, + val hasCustomTitle: Boolean, val rootfsName: String, val pid: Long?, val running: Boolean, @@ -35,12 +36,17 @@ fun terminalDistroTitle(rawName: String?): String = internal data class TerminalTab( val id: String, - var title: String, + val defaultTitle: String, val rootfsName: String, val value: T, + var shellTitle: String? = null, + var customTitle: String? = null, var closeState: TerminalTabCloseState = TerminalTabCloseState.OPEN, var closeError: String? = null, -) +) { + val title: String + get() = customTitle ?: shellTitle ?: defaultTitle +} internal class TerminalTabRegistry { private val tabs = LinkedHashMap>() @@ -75,7 +81,23 @@ internal class TerminalTabRegistry { title: String, ): TerminalTab? { val tab = tabs[id] ?: return null - tab.title = title + tab.customTitle = title + return tab + } + + fun updateShellTitle( + id: String, + title: String?, + ): TerminalTab? { + val tab = tabs[id] ?: return null + if (tab.shellTitle == title) return null + tab.shellTitle = title + return tab + } + + fun followShellTitle(id: String): TerminalTab? { + val tab = tabs[id]?.takeIf { it.customTitle != null } ?: return null + tab.customTitle = null return tab } @@ -124,3 +146,14 @@ internal class TerminalTabRegistry { fun size(): Int = tabs.size } + +internal fun normalizeTerminalTitle(rawTitle: String?): String? = + rawTitle + ?.filter { character -> + !character.isISOControl() && + Character.getType(character) != Character.FORMAT.toInt() + }?.trim() + ?.take(MAX_TERMINAL_TITLE_CHARS) + ?.ifEmpty { null } + +internal const val MAX_TERMINAL_TITLE_CHARS = 40 diff --git a/app/src/main/java/org/randomcoder/udroid/ui/TerminalPage.kt b/app/src/main/java/org/randomcoder/udroid/ui/TerminalPage.kt index 7af8046..bd08cf1 100644 --- a/app/src/main/java/org/randomcoder/udroid/ui/TerminalPage.kt +++ b/app/src/main/java/org/randomcoder/udroid/ui/TerminalPage.kt @@ -79,6 +79,7 @@ import org.randomcoder.udroid.runtime.RuntimeSnapshot import org.randomcoder.udroid.runtime.RuntimeSupervisorService import org.randomcoder.udroid.runtime.TerminalTabCloseState import org.randomcoder.udroid.runtime.TerminalTabSnapshot +import org.randomcoder.udroid.runtime.MAX_TERMINAL_TITLE_CHARS import org.randomcoder.udroid.runtime.terminalDistroTitle import kotlin.math.roundToInt @@ -142,6 +143,7 @@ fun InteractiveTerminalPage( tabs = tabs, onSelect = { id -> service?.selectTerminalTab(id) }, onRename = { tab -> renamingTab = tab }, + onFollowTitle = { id -> service?.followTerminalShellTitle(id) }, onClose = { id -> if (service?.closeTerminalTab(id) != true) { tabError = "Could not close terminal" @@ -333,6 +335,7 @@ private fun TerminalTabsRow( tabs: List, onSelect: (String) -> Unit, onRename: (TerminalTabSnapshot) -> Unit, + onFollowTitle: (String) -> Unit, onClose: (String) -> Unit, creatingTab: Boolean, onClone: () -> Unit, @@ -375,6 +378,7 @@ private fun TerminalTabsRow( tab = tab, onSelect = { onSelect(tab.id) }, onRename = { onRename(tab) }, + onFollowTitle = { onFollowTitle(tab.id) }, onClose = { onClose(tab.id) }, stopsLinux = tabs.size == 1, ) @@ -417,6 +421,7 @@ private fun TerminalTabChip( tab: TerminalTabSnapshot, onSelect: () -> Unit, onRename: () -> Unit, + onFollowTitle: () -> Unit, onClose: () -> Unit, stopsLinux: Boolean, ) { @@ -503,6 +508,15 @@ private fun TerminalTabChip( onRename() }, ) + if (tab.hasCustomTitle) { + DropdownMenuItem( + text = { Text("Follow shell title") }, + onClick = { + menuExpanded = false + onFollowTitle() + }, + ) + } DropdownMenuItem( text = { Text( @@ -642,7 +656,7 @@ private fun RenameTerminalDialog( text = { OutlinedTextField( value = title, - onValueChange = { title = it.take(40) }, + onValueChange = { title = it.take(MAX_TERMINAL_TITLE_CHARS) }, label = { Text("Terminal name") }, singleLine = true, ) diff --git a/app/src/test/java/org/randomcoder/udroid/runtime/TerminalTabRegistryTest.kt b/app/src/test/java/org/randomcoder/udroid/runtime/TerminalTabRegistryTest.kt index 5b5324c..8f52aae 100644 --- a/app/src/test/java/org/randomcoder/udroid/runtime/TerminalTabRegistryTest.kt +++ b/app/src/test/java/org/randomcoder/udroid/runtime/TerminalTabRegistryTest.kt @@ -51,6 +51,43 @@ class TerminalTabRegistryTest { assertEquals("Terminal two", registry.get("two")?.title) } + @Test + fun `custom title overrides shell updates until automatic titles are restored`() { + val registry = TerminalTabRegistry() + registry.add(tab("one")) + + registry.updateShellTitle("one", "root@localhost: ~") + assertEquals("root@localhost: ~", registry.get("one")?.title) + + registry.rename("one", "Server") + registry.updateShellTitle("one", "htop") + assertEquals("Server", registry.get("one")?.title) + + registry.followShellTitle("one") + assertEquals("htop", registry.get("one")?.title) + } + + @Test + fun `empty shell title falls back to default title`() { + val registry = TerminalTabRegistry() + registry.add(tab("one")) + registry.updateShellTitle("one", "htop") + + registry.updateShellTitle("one", null) + + assertEquals("Terminal one", registry.get("one")?.title) + } + + @Test + fun `terminal titles drop controls and remain bounded`() { + val title = "\u001B]0;\u202Eroot@localhost\n" + "x".repeat(50) + + assertEquals( + " ]0;root@localhost".trim() + "x".repeat(23), + normalizeTerminalTitle(title), + ) + } + @Test fun `removing last tab clears active id`() { val registry = TerminalTabRegistry() @@ -104,7 +141,7 @@ class TerminalTabRegistryTest { ) = TerminalTab( id = id, - title = "Terminal $id", + defaultTitle = "Terminal $id", rootfsName = rootfsName, value = Any(), ) From a984ea5903d68c21767dcec12e53d36742deeef1 Mon Sep 17 00:00:00 2001 From: saicharankandukuri Date: Fri, 6 Nov 2026 15:03:34 +0530 Subject: [PATCH 04/14] feat(terminal): add color profile management Persist built-in and custom terminal palettes and apply them to active sessions, terminal chrome, and system bars. Add a compact searchable profile picker with editing, Termux/Ghostty/iTerm2 import, Termux export, tests, user documentation, and palette attribution. --- THIRD_PARTY_NOTICES.md | 11 + .../org/randomcoder/udroid/MainActivity.kt | 24 +- .../runtime/RuntimeSupervisorService.kt | 26 +- .../udroid/runtime/TerminalAppearance.kt | 243 +++++++ .../udroid/runtime/TerminalProfileCodec.kt | 133 ++++ .../org/randomcoder/udroid/ui/AppShell.kt | 41 +- .../org/randomcoder/udroid/ui/DesignSystem.kt | 142 +++- .../org/randomcoder/udroid/ui/TerminalPage.kt | 37 +- .../udroid/ui/TerminalSettingsPage.kt | 665 ++++++++++++++++++ .../udroid/runtime/TerminalAppearanceTest.kt | 58 ++ .../runtime/TerminalProfileCodecTest.kt | 84 +++ .../udroid/ui/TerminalSettingsPageTest.kt | 23 + docs/USER_GUIDE.md | 41 ++ 13 files changed, 1485 insertions(+), 43 deletions(-) create mode 100644 app/src/main/java/org/randomcoder/udroid/runtime/TerminalAppearance.kt create mode 100644 app/src/main/java/org/randomcoder/udroid/runtime/TerminalProfileCodec.kt create mode 100644 app/src/main/java/org/randomcoder/udroid/ui/TerminalSettingsPage.kt create mode 100644 app/src/test/java/org/randomcoder/udroid/runtime/TerminalAppearanceTest.kt create mode 100644 app/src/test/java/org/randomcoder/udroid/runtime/TerminalProfileCodecTest.kt create mode 100644 app/src/test/java/org/randomcoder/udroid/ui/TerminalSettingsPageTest.kt diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 6775cab..56dbb12 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -59,6 +59,17 @@ Termux application code is not part of this checkpoint. The source is built inside this Gradle project so the APK and its native PTY bridge are reproducible from the checked-in tree. +## Terminal color profiles + +- Project: +- Source commit: `79338cb19f4b86cae4d2e81e6de60ba7f613bb9b` +- Imported profiles: Catppuccin Mocha, Gruvbox Dark, Nord, Tokyo Night, + Rosé Pine, and Ubuntu +- License: GNU General Public License version 3 + +The built-in values follow the corresponding Termux:Styling color profile +files. Theme names may identify their respective upstream projects. + ## Termux:X11 - Project: diff --git a/app/src/main/java/org/randomcoder/udroid/MainActivity.kt b/app/src/main/java/org/randomcoder/udroid/MainActivity.kt index 8d78d59..a079046 100644 --- a/app/src/main/java/org/randomcoder/udroid/MainActivity.kt +++ b/app/src/main/java/org/randomcoder/udroid/MainActivity.kt @@ -22,6 +22,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.toArgb import androidx.core.content.ContextCompat +import androidx.core.graphics.ColorUtils import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -67,12 +68,13 @@ import org.randomcoder.udroid.runtime.RuntimePhase import org.randomcoder.udroid.runtime.RuntimeSnapshot import org.randomcoder.udroid.runtime.RuntimeSupervisorService import org.randomcoder.udroid.runtime.RootfsStorageUsage +import org.randomcoder.udroid.runtime.TerminalProfileStore +import org.randomcoder.udroid.runtime.TerminalProfiles import org.randomcoder.udroid.runtime.VirglServerMode import org.randomcoder.udroid.ui.UdroidApp import org.randomcoder.udroid.ui.UdroidCanvas import org.randomcoder.udroid.ui.UdroidDarkCanvas import org.randomcoder.udroid.ui.UdroidDestination -import org.randomcoder.udroid.ui.UdroidTerminal import org.randomcoder.udroid.ui.UdroidTheme import org.randomcoder.udroid.ui.workspaceJourney import org.randomcoder.udroid.update.AppUpdateContract @@ -248,6 +250,9 @@ class MainActivity : ComponentActivity() { linuxApplicationMessage = linuxApplicationMessage, showInstallTerminal = showInstallTerminal, runtimeService = runtimeService, + onTerminalAppearanceChanged = { + applySystemBars(UdroidDestination.TERMINAL) + }, onDestinationSelected = ::selectDestination, onPrimaryDestinationSelected = ::selectPrimaryDestination, onStart = { @@ -400,19 +405,30 @@ class MainActivity : ComponentActivity() { val terminal = destination == UdroidDestination.TERMINAL || destination == UdroidDestination.DESKTOP - val terminalScrim = UdroidTerminal.toArgb() + val terminalScrim = + if (destination == UdroidDestination.TERMINAL) { + TerminalProfileStore(this).load().selectedProfile.background + } else { + TerminalProfiles.udroid.background + } + val terminalStyle = + if (ColorUtils.calculateLuminance(terminalScrim) < 0.5) { + SystemBarStyle.dark(terminalScrim) + } else { + SystemBarStyle.light(terminalScrim, TerminalProfiles.udroid.background) + } val lightScrim = UdroidCanvas.toArgb() val darkScrim = UdroidDarkCanvas.toArgb() enableEdgeToEdge( statusBarStyle = if (terminal) { - SystemBarStyle.dark(terminalScrim) + terminalStyle } else { SystemBarStyle.auto(lightScrim, darkScrim) }, navigationBarStyle = if (terminal) { - SystemBarStyle.dark(terminalScrim) + terminalStyle } else { SystemBarStyle.auto(lightScrim, darkScrim) }, diff --git a/app/src/main/java/org/randomcoder/udroid/runtime/RuntimeSupervisorService.kt b/app/src/main/java/org/randomcoder/udroid/runtime/RuntimeSupervisorService.kt index 19df66d..89c5b77 100644 --- a/app/src/main/java/org/randomcoder/udroid/runtime/RuntimeSupervisorService.kt +++ b/app/src/main/java/org/randomcoder/udroid/runtime/RuntimeSupervisorService.kt @@ -527,6 +527,16 @@ class RuntimeSupervisorService : Service() { currentTerminalSession()?.takeIf(TerminalSession::isRunning)?.write(text) } + fun updateTerminalAppearance(profile: TerminalColorProfile) { + mainHandler.post { + configureTerminalColors(profile) + terminalTabs.all().forEach { tab -> + tab.value.emulator?.mColors?.reset() + tab.value.onColorsChanged() + } + } + } + fun requestX11RendererConnection(callback: (ParcelFileDescriptor?) -> Unit) { x11Controller.requestRendererConnection(callback) } @@ -1649,15 +1659,17 @@ class RuntimeSupervisorService : Service() { publishState(next) } - private fun configureTerminalColors() { + private fun configureTerminalColors( + profile: TerminalColorProfile = TerminalProfileStore(this).load().selectedProfile, + ) { TerminalColors.COLOR_SCHEME.updateWith( Properties().apply { - setProperty("foreground", "#E3E7EF") - setProperty("background", "#11131F") - setProperty("cursor", "#43D292") - setProperty("color2", "#43D292") - setProperty("color3", "#E8B86D") - setProperty("color4", "#65A7D8") + setProperty("foreground", formatTerminalColor(profile.foreground)) + setProperty("background", formatTerminalColor(profile.background)) + setProperty("cursor", formatTerminalColor(profile.cursor)) + profile.ansiColors.forEachIndexed { index, color -> + setProperty("color$index", formatTerminalColor(color)) + } }, ) } diff --git a/app/src/main/java/org/randomcoder/udroid/runtime/TerminalAppearance.kt b/app/src/main/java/org/randomcoder/udroid/runtime/TerminalAppearance.kt new file mode 100644 index 0000000..2ebba45 --- /dev/null +++ b/app/src/main/java/org/randomcoder/udroid/runtime/TerminalAppearance.kt @@ -0,0 +1,243 @@ +package org.randomcoder.udroid.runtime + +import android.content.Context +import androidx.annotation.ColorInt +import java.util.Base64 +import java.util.Locale + +const val TERMINAL_ANSI_COLOR_COUNT = 16 + +data class TerminalColorProfile( + val id: String, + val name: String, + val background: Int, + val foreground: Int, + val cursor: Int, + val ansiColors: List, +) { + init { + require(name.isNotBlank()) { "Profile name cannot be blank" } + require(ansiColors.size == TERMINAL_ANSI_COLOR_COUNT) { + "A terminal profile requires 16 ANSI colors" + } + } +} + +data class TerminalProfileState( + val selectedProfileId: String, + val userProfiles: List, +) { + val profiles: List + get() = TerminalProfiles.builtIns + userProfiles + + val selectedProfile: TerminalColorProfile + get() = profiles.firstOrNull { it.id == selectedProfileId } ?: TerminalProfiles.udroid +} + +object TerminalProfiles { + val udroid = + TerminalColorProfile( + id = "builtin:udroid", + name = "uDroid", + background = rgb("11131F"), + foreground = rgb("E3E7EF"), + cursor = rgb("43D292"), + ansiColors = + palette( + "000000", "CD0000", "43D292", "E8B86D", "65A7D8", "CD00CD", "00CDCD", "E5E5E5", + "7F7F7F", "FF0000", "00FF00", "FFFF00", "5C5CFF", "FF00FF", "00FFFF", "FFFFFF", + ), + ) + + private val solarizedAnsi = + palette( + "073642", "DC322F", "859900", "B58900", "268BD2", "D33682", "2AA198", "EEE8D5", + "002B36", "CB4B16", "586E75", "657B83", "839496", "6C71C4", "93A1A1", "FDF6E3", + ) + + val builtIns = + listOf( + udroid, + TerminalColorProfile( + id = "builtin:breeze", + name = "Breeze", + background = rgb("232627"), + foreground = rgb("FCFCFC"), + cursor = rgb("1D99F3"), + ansiColors = + palette( + "232627", "ED1515", "11D116", "F67400", "1D99F3", "9B59B6", "1ABC9C", "FCFCFC", + "7F8C8D", "C0392B", "1CDC9A", "FDBC4B", "3DAEE9", "8E44AD", "16A085", "FFFFFF", + ), + ), + TerminalColorProfile( + id = "builtin:solarized-dark", + name = "Solarized Dark", + background = rgb("002B36"), + foreground = rgb("839496"), + cursor = rgb("268BD2"), + ansiColors = solarizedAnsi, + ), + TerminalColorProfile( + id = "builtin:solarized-light", + name = "Solarized Light", + background = rgb("FDF6E3"), + foreground = rgb("657B83"), + cursor = rgb("268BD2"), + ansiColors = solarizedAnsi, + ), + TerminalColorProfile( + id = "builtin:dracula", + name = "Dracula", + background = rgb("282A36"), + foreground = rgb("F8F8F2"), + cursor = rgb("BD93F9"), + ansiColors = + palette( + "21222C", "FF5555", "50FA7B", "F1FA8C", "BD93F9", "FF79C6", "8BE9FD", "F8F8F2", + "6272A4", "FF6E6E", "69FF94", "FFFFA5", "D6ACFF", "FF92DF", "A4FFFF", "FFFFFF", + ), + ), + TerminalColorProfile( + id = "builtin:catppuccin-mocha", + name = "Catppuccin Mocha", + background = rgb("1E1E2E"), + foreground = rgb("CDD6F4"), + cursor = rgb("F5E0DC"), + ansiColors = + palette( + "45475A", "F38BA8", "A6E3A1", "F9E2AF", "89B4FA", "F5C2E7", "94E2D5", "BAC2DE", + "585B70", "F38BA8", "A6E3A1", "F9E2AF", "89B4FA", "F5C2E7", "94E2D5", "A6ADC8", + ), + ), + TerminalColorProfile( + id = "builtin:gruvbox-dark", + name = "Gruvbox Dark", + background = rgb("1D2021"), + foreground = rgb("EBDBB2"), + cursor = rgb("EBDBB2"), + ansiColors = + palette( + "282828", "CC241D", "98971A", "D79921", "458588", "B16286", "689D6A", "A89984", + "928374", "FB4934", "B8BB26", "FABD2F", "83A598", "D3869B", "8EC07C", "EBDBB2", + ), + ), + TerminalColorProfile( + id = "builtin:nord", + name = "Nord", + background = rgb("2E3440"), + foreground = rgb("D8DEE9"), + cursor = rgb("D8DEE9"), + ansiColors = + palette( + "3B4252", "BF616A", "A3BE8C", "EBCB8B", "81A1C1", "B48EAD", "88C0D0", "E5E8F0", + "4C566A", "BF616A", "A3BE8C", "EBCB8B", "81A1C1", "B48EAD", "8FBCBB", "ECEFF4", + ), + ), + TerminalColorProfile( + id = "builtin:tokyo-night", + name = "Tokyo Night", + background = rgb("1A1B26"), + foreground = rgb("C0CAF5"), + cursor = rgb("C0CAF5"), + ansiColors = + palette( + "15161E", "F7768E", "9ECE6A", "E0AF68", "7AA2F7", "BB9AF7", "7DCFFF", "A9B1D6", + "414868", "F7768E", "9ECE6A", "E0AF68", "7AA2F7", "BB9AF7", "7DCFFF", "C0CAF5", + ), + ), + TerminalColorProfile( + id = "builtin:rose-pine", + name = "Rosé Pine", + background = rgb("191724"), + foreground = rgb("E0DEF4"), + cursor = rgb("6E6A86"), + ansiColors = + palette( + "26233A", "EB6F92", "31748F", "F6C177", "9CCFD8", "C4A7E7", "EBBCBA", "E0DEF4", + "6E6A86", "EB6F92", "31748F", "F6C177", "9CCFD8", "C4A7E7", "EBBCBA", "E0DEF4", + ), + ), + TerminalColorProfile( + id = "builtin:ubuntu", + name = "Ubuntu", + background = rgb("300A24"), + foreground = rgb("FFFFFF"), + cursor = rgb("FFFFFF"), + ansiColors = + palette( + "2E3436", "CC0000", "4E9A06", "C4A000", "3465A4", "75507B", "06989A", "D3D7CF", + "555753", "EF2929", "8AE234", "FCE94F", "729FCF", "AD7FA8", "34E2E2", "EEEEEC", + ), + ), + ) +} + +class TerminalProfileStore(context: Context) { + private val preferences = + context.applicationContext.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) + + fun load(): TerminalProfileState = + TerminalProfileState( + selectedProfileId = preferences.getString(KEY_SELECTED, null) ?: TerminalProfiles.udroid.id, + userProfiles = + preferences + .getStringSet(KEY_USER_PROFILES, emptySet()) + .orEmpty() + .mapNotNull(::decodeTerminalProfile) + .sortedBy(TerminalColorProfile::name), + ) + + fun save(state: TerminalProfileState) { + preferences + .edit() + .putString(KEY_SELECTED, state.selectedProfile.id) + .putStringSet(KEY_USER_PROFILES, state.userProfiles.map(::encodeTerminalProfile).toSet()) + .apply() + } + + private companion object { + const val PREFERENCES_NAME = "terminal-profiles" + const val KEY_SELECTED = "selected-profile" + const val KEY_USER_PROFILES = "user-profiles" + } +} + +internal fun parseTerminalColor(value: String): Int? { + val hex = value.trim().removePrefix("#") + if (hex.length != 6 || hex.any { it.digitToIntOrNull(16) == null }) return null + return (0xFF000000L or hex.toLong(16)).toInt() +} + +internal fun formatTerminalColor( + @ColorInt color: Int, +): String = String.format(Locale.US, "#%06X", color and 0xFFFFFF) + +internal fun encodeTerminalProfile(profile: TerminalColorProfile): String = + listOf( + profile.id, + Base64.getUrlEncoder().withoutPadding().encodeToString(profile.name.toByteArray()), + formatTerminalColor(profile.background), + formatTerminalColor(profile.foreground), + formatTerminalColor(profile.cursor), + profile.ansiColors.joinToString(",", transform = ::formatTerminalColor), + ).joinToString("|") + +internal fun decodeTerminalProfile(value: String): TerminalColorProfile? { + val fields = value.split('|') + if (fields.size != 6) return null + return runCatching { + TerminalColorProfile( + id = fields[0].takeIf(String::isNotBlank) ?: error("Missing id"), + name = String(Base64.getUrlDecoder().decode(fields[1])), + background = parseTerminalColor(fields[2]) ?: error("Invalid background"), + foreground = parseTerminalColor(fields[3]) ?: error("Invalid foreground"), + cursor = parseTerminalColor(fields[4]) ?: error("Invalid cursor"), + ansiColors = fields[5].split(',').map { parseTerminalColor(it) ?: error("Invalid ANSI color") }, + ) + }.getOrNull() +} + +private fun rgb(value: String): Int = checkNotNull(parseTerminalColor(value)) + +private fun palette(vararg values: String): List = values.map(::rgb) diff --git a/app/src/main/java/org/randomcoder/udroid/runtime/TerminalProfileCodec.kt b/app/src/main/java/org/randomcoder/udroid/runtime/TerminalProfileCodec.kt new file mode 100644 index 0000000..72a97b9 --- /dev/null +++ b/app/src/main/java/org/randomcoder/udroid/runtime/TerminalProfileCodec.kt @@ -0,0 +1,133 @@ +package org.randomcoder.udroid.runtime + +import org.w3c.dom.Element +import org.xml.sax.InputSource +import java.io.StringReader +import javax.xml.parsers.DocumentBuilderFactory +import kotlin.math.roundToInt + +internal fun importTerminalProfile( + name: String, + contents: String, +): Result = + runCatching { + val profileName = name.substringBeforeLast('.').ifBlank { "Imported profile" } + if (contents.trimStart().startsWith(" + appendLine("color$index=${formatTerminalColor(color)}") + } + } + +private fun parseTextTheme( + name: String, + contents: String, +): TerminalColorProfile { + val colors = mutableMapOf() + val palette = mutableMapOf() + contents.lineSequence().forEach { source -> + val line = source.trim() + if (line.isEmpty() || line.startsWith('#')) return@forEach + GHOSTTY_PALETTE.matchEntire(line)?.let { match -> + val index = match.groupValues[1].toIntOrNull() ?: return@let + parseTerminalColor(match.groupValues[2])?.let { palette[index] = it } + return@forEach + } + TEXT_COLOR.matchEntire(line)?.let { match -> + val key = match.groupValues[1].lowercase() + val color = parseTerminalColor(match.groupValues[2]) ?: return@let + when { + key.startsWith("color") -> key.removePrefix("color").toIntOrNull()?.let { palette[it] = color } + else -> colors[key] = color + } + } + } + val foreground = colors["foreground"] ?: error("Theme is missing foreground") + val background = colors["background"] ?: error("Theme is missing background") + val ansi = (0 until TERMINAL_ANSI_COLOR_COUNT).map { palette[it] ?: error("Theme is missing color$it") } + return TerminalProfiles.udroid.copy( + id = "imported", + name = name, + background = background, + foreground = foreground, + cursor = colors["cursor"] ?: colors["cursor-color"] ?: foreground, + ansiColors = ansi, + ) +} + +private fun parseItermColors( + name: String, + contents: String, +): TerminalColorProfile { + val factory = DocumentBuilderFactory.newInstance() + runCatching { factory.isXIncludeAware = false } + runCatching { factory.isExpandEntityReferences = false } + XML_FEATURES.forEach { (feature, enabled) -> runCatching { factory.setFeature(feature, enabled) } } + val builder = factory.newDocumentBuilder().apply { + setEntityResolver { _, _ -> InputSource(StringReader("")) } + } + val root = builder.parse(InputSource(StringReader(contents))).documentElement + val dictionary = root.childElements().firstOrNull { it.tagName == "dict" } ?: error("Invalid iTerm2 palette") + val entries = dictionary.plistDictionary() + fun color(key: String): Int? = (entries[key] as? Element)?.plistColor() + val foreground = color("Foreground Color") ?: error("Palette is missing foreground") + return TerminalProfiles.udroid.copy( + id = "imported", + name = name, + background = color("Background Color") ?: error("Palette is missing background"), + foreground = foreground, + cursor = color("Cursor Color") ?: foreground, + ansiColors = + (0 until TERMINAL_ANSI_COLOR_COUNT).map { index -> + color("Ansi $index Color") ?: error("Palette is missing ANSI color $index") + }, + ) +} + +private fun Element.plistDictionary(): Map { + val children = childElements() + return buildMap { + var index = 0 + while (index + 1 < children.size) { + if (children[index].tagName == "key") put(children[index].textContent, children[index + 1]) + index += 2 + } + } +} + +private fun Element.plistColor(): Int { + val components = plistDictionary() + fun component(name: String): Int = + ((components[name]?.textContent?.toDoubleOrNull() ?: error("Color is missing $name")) * 255) + .roundToInt() + .coerceIn(0, 255) + return 0xFF000000.toInt() or + (component("Red Component") shl 16) or + (component("Green Component") shl 8) or + component("Blue Component") +} + +private fun Element.childElements(): List = + buildList { + val nodes = childNodes + for (index in 0 until nodes.length) (nodes.item(index) as? Element)?.let(::add) + } + +private val TEXT_COLOR = Regex("([A-Za-z0-9-]+)\\s*[:=]\\s*(#?[0-9A-Fa-f]{6})\\s*") +private val GHOSTTY_PALETTE = Regex("palette\\s*=\\s*(\\d+)\\s*=\\s*(#?[0-9A-Fa-f]{6})\\s*") +private val XML_FEATURES = + mapOf( + "http://xml.org/sax/features/external-general-entities" to false, + "http://xml.org/sax/features/external-parameter-entities" to false, + "http://apache.org/xml/features/nonvalidating/load-external-dtd" to false, + ) diff --git a/app/src/main/java/org/randomcoder/udroid/ui/AppShell.kt b/app/src/main/java/org/randomcoder/udroid/ui/AppShell.kt index f1c69ff..3d59148 100644 --- a/app/src/main/java/org/randomcoder/udroid/ui/AppShell.kt +++ b/app/src/main/java/org/randomcoder/udroid/ui/AppShell.kt @@ -117,6 +117,10 @@ import org.randomcoder.udroid.runtime.ProotMountProfileStore import org.randomcoder.udroid.runtime.RuntimePhase import org.randomcoder.udroid.runtime.RuntimeSnapshot import org.randomcoder.udroid.runtime.RuntimeSupervisorService +import org.randomcoder.udroid.runtime.TerminalColorProfile +import org.randomcoder.udroid.runtime.TerminalProfileState +import org.randomcoder.udroid.runtime.TerminalProfileStore +import org.randomcoder.udroid.runtime.TerminalProfiles import org.randomcoder.udroid.runtime.VirglServerMode import org.randomcoder.udroid.update.AppUpdatePhase import org.randomcoder.udroid.update.AppUpdateState @@ -201,6 +205,7 @@ fun UdroidApp( linuxApplicationMessage: String?, showInstallTerminal: Boolean, runtimeService: RuntimeSupervisorService?, + onTerminalAppearanceChanged: () -> Unit, onDestinationSelected: (UdroidDestination) -> Unit, onPrimaryDestinationSelected: (UdroidDestination) -> Unit, onStart: () -> Unit, @@ -243,6 +248,11 @@ fun UdroidApp( onInstallUpdate: () -> Unit, onOpenUpdateRelease: () -> Unit, ) { + val context = LocalContext.current + val terminalProfileStore = remember(context) { TerminalProfileStore(context) } + var terminalProfileState by remember(context) { + mutableStateOf(terminalProfileStore.load()) + } var mountConfigurationSourceSystemId by rememberSaveable { mutableStateOf(null) } var selectedMountProfileSystemId by rememberSaveable { mutableStateOf(null) } val hasInstalledLinux = installedRootfsName != null @@ -275,7 +285,13 @@ fun UdroidApp( } if (activeDestination == UdroidDestination.TERMINAL) { - UdroidTerminalTheme { + fun applyTerminalProfiles(state: TerminalProfileState) { + terminalProfileStore.save(state) + terminalProfileState = state + runtimeService?.updateTerminalAppearance(state.selectedProfile) + onTerminalAppearanceChanged() + } + UdroidTerminalTheme(terminalProfileState.selectedProfile) { BackHandler { onDestinationSelected(UdroidDestination.SYSTEM) } @@ -291,6 +307,29 @@ fun UdroidApp( snapshot = snapshot, service = runtimeService, installedRootfses = installedRootfses, + profileState = terminalProfileState, + onSelectProfile = { profileId -> + applyTerminalProfiles(terminalProfileState.copy(selectedProfileId = profileId)) + }, + onSaveProfile = { profile: TerminalColorProfile -> + val profiles = terminalProfileState.userProfiles.filterNot { it.id == profile.id } + profile + applyTerminalProfiles( + terminalProfileState.copy( + selectedProfileId = profile.id, + userProfiles = profiles, + ), + ) + }, + onDeleteProfile = { profileId -> + val selected = terminalProfileState.selectedProfileId == profileId + applyTerminalProfiles( + terminalProfileState.copy( + selectedProfileId = + if (selected) TerminalProfiles.udroid.id else terminalProfileState.selectedProfileId, + userProfiles = terminalProfileState.userProfiles.filterNot { it.id == profileId }, + ), + ) + }, onStart = onStart, onStop = onStop, onExit = { onDestinationSelected(UdroidDestination.SYSTEM) }, diff --git a/app/src/main/java/org/randomcoder/udroid/ui/DesignSystem.kt b/app/src/main/java/org/randomcoder/udroid/ui/DesignSystem.kt index 2eea8aa..34c267b 100644 --- a/app/src/main/java/org/randomcoder/udroid/ui/DesignSystem.kt +++ b/app/src/main/java/org/randomcoder/udroid/ui/DesignSystem.kt @@ -13,9 +13,15 @@ import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.lerp +import androidx.compose.ui.graphics.luminance import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp +import org.randomcoder.udroid.runtime.TerminalColorProfile +import org.randomcoder.udroid.runtime.TerminalProfiles private val UdroidLightColors = lightColorScheme( @@ -128,14 +134,77 @@ val UdroidWarning: Color val UdroidWarningSurface: Color @Composable get() = if (isSystemInDarkTheme()) Color(0xFF3B2F00) else Color(0xFFFFE08B) +private data class TerminalPalette( + val background: Color, + val surface: Color, + val raised: Color, + val line: Color, + val foreground: Color, + val muted: Color, + val accent: Color, + val accentContainer: Color, + val onAccent: Color, + val error: Color, + val onError: Color, + val errorContainer: Color, + val onErrorContainer: Color, +) + +private fun terminalPalette(profile: TerminalColorProfile): TerminalPalette { + val background = Color(profile.background) + val foreground = Color(profile.foreground) + val accent = Color(profile.cursor) + val lightBackground = background.luminance() >= 0.5f + val onAccent = + if (contrastRatio(foreground, accent) >= contrastRatio(background, accent)) { + foreground + } else { + background + } + return TerminalPalette( + background = background, + surface = lerp(background, foreground, 0.06f), + raised = lerp(background, foreground, 0.12f), + line = lerp(background, foreground, 0.18f), + foreground = foreground, + muted = lerp(background, foreground, 0.65f), + accent = accent, + accentContainer = lerp(background, accent, 0.28f), + onAccent = onAccent, + error = if (lightBackground) Color(0xFFBA1A1A) else Color(0xFFFFB4AB), + onError = if (lightBackground) Color.White else Color(0xFF690005), + errorContainer = if (lightBackground) Color(0xFFFFDAD6) else Color(0xFF93000A), + onErrorContainer = if (lightBackground) Color(0xFF93000A) else Color(0xFFFFDAD6), + ) +} + +private fun contrastRatio( + first: Color, + second: Color, +): Float { + val lighter = maxOf(first.luminance(), second.luminance()) + val darker = minOf(first.luminance(), second.luminance()) + return (lighter + 0.05f) / (darker + 0.05f) +} + +private val LocalTerminalPalette = + staticCompositionLocalOf { terminalPalette(TerminalProfiles.udroid) } + // Terminal workspace: the management shell gives way to a focused instrument. -val UdroidTerminal = Color(0xFF11131F) -val UdroidTerminalSurface = Color(0xFF181B2A) -val UdroidTerminalRaised = Color(0xFF222638) -val UdroidTerminalLine = Color(0xFF2C3145) -val UdroidTerminalText = Color(0xFFE3E7EF) -val UdroidTerminalMuted = Color(0xFF9AA2B5) -val UdroidTerminalGreen = Color(0xFF43D292) +val UdroidTerminal: Color + @Composable get() = LocalTerminalPalette.current.background +val UdroidTerminalSurface: Color + @Composable get() = LocalTerminalPalette.current.surface +val UdroidTerminalRaised: Color + @Composable get() = LocalTerminalPalette.current.raised +val UdroidTerminalLine: Color + @Composable get() = LocalTerminalPalette.current.line +val UdroidTerminalText: Color + @Composable get() = LocalTerminalPalette.current.foreground +val UdroidTerminalMuted: Color + @Composable get() = LocalTerminalPalette.current.muted +val UdroidTerminalGreen: Color + @Composable get() = LocalTerminalPalette.current.accent object UdroidSpacing { val unit = 4 @@ -186,25 +255,42 @@ fun UdroidTheme(content: @Composable () -> Unit) { } @Composable -fun UdroidTerminalTheme(content: @Composable () -> Unit) { - MaterialTheme( - colorScheme = - darkColorScheme( - primary = UdroidTerminalGreen, - onPrimary = Color(0xFF052116), - primaryContainer = Color(0xFF164A39), - onPrimaryContainer = UdroidTerminalText, - surface = UdroidTerminalSurface, - surfaceVariant = UdroidTerminalRaised, - onSurface = UdroidTerminalText, - onSurfaceVariant = UdroidTerminalMuted, - background = UdroidTerminal, - onBackground = UdroidTerminalText, - outline = UdroidTerminalLine, - error = Color(0xFFFFB4AB), - ), - typography = Typography(), - shapes = UdroidShapes, - content = content, - ) +fun UdroidTerminalTheme( + profile: TerminalColorProfile = TerminalProfiles.udroid, + content: @Composable () -> Unit, +) { + val palette = terminalPalette(profile) + CompositionLocalProvider(LocalTerminalPalette provides palette) { + MaterialTheme( + colorScheme = + darkColorScheme( + primary = palette.accent, + onPrimary = palette.onAccent, + primaryContainer = palette.accentContainer, + onPrimaryContainer = palette.foreground, + surface = palette.surface, + surfaceVariant = palette.raised, + onSurface = palette.foreground, + onSurfaceVariant = palette.muted, + background = palette.background, + onBackground = palette.foreground, + surfaceDim = palette.background, + surfaceBright = palette.raised, + surfaceContainerLowest = palette.background, + surfaceContainerLow = palette.surface, + surfaceContainer = palette.surface, + surfaceContainerHigh = palette.raised, + surfaceContainerHighest = palette.raised, + outline = palette.line, + outlineVariant = palette.line, + error = palette.error, + onError = palette.onError, + errorContainer = palette.errorContainer, + onErrorContainer = palette.onErrorContainer, + ), + typography = Typography(), + shapes = UdroidShapes, + content = content, + ) + } } diff --git a/app/src/main/java/org/randomcoder/udroid/ui/TerminalPage.kt b/app/src/main/java/org/randomcoder/udroid/ui/TerminalPage.kt index bd08cf1..05b4b66 100644 --- a/app/src/main/java/org/randomcoder/udroid/ui/TerminalPage.kt +++ b/app/src/main/java/org/randomcoder/udroid/ui/TerminalPage.kt @@ -37,6 +37,7 @@ import androidx.compose.material.icons.outlined.Add import androidx.compose.material.icons.outlined.Close import androidx.compose.material.icons.outlined.KeyboardArrowDown import androidx.compose.material.icons.outlined.MoreVert +import androidx.compose.material.icons.outlined.Palette import androidx.compose.material.icons.outlined.Terminal import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button @@ -61,7 +62,7 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.withFrameNanos import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.font.FontWeight @@ -77,6 +78,8 @@ import org.randomcoder.udroid.runtime.InstalledRootfs import org.randomcoder.udroid.runtime.RuntimePhase import org.randomcoder.udroid.runtime.RuntimeSnapshot import org.randomcoder.udroid.runtime.RuntimeSupervisorService +import org.randomcoder.udroid.runtime.TerminalColorProfile +import org.randomcoder.udroid.runtime.TerminalProfileState import org.randomcoder.udroid.runtime.TerminalTabCloseState import org.randomcoder.udroid.runtime.TerminalTabSnapshot import org.randomcoder.udroid.runtime.MAX_TERMINAL_TITLE_CHARS @@ -89,6 +92,10 @@ fun InteractiveTerminalPage( snapshot: RuntimeSnapshot, service: RuntimeSupervisorService?, installedRootfses: List, + profileState: TerminalProfileState, + onSelectProfile: (String) -> Unit, + onSaveProfile: (TerminalColorProfile) -> Unit, + onDeleteProfile: (String) -> Unit, onStart: () -> Unit, onStop: () -> Unit, onExit: () -> Unit, @@ -101,6 +108,7 @@ fun InteractiveTerminalPage( var renamingTab by remember { mutableStateOf(null) } var creatingTab by remember { mutableStateOf(false) } var showDistroPicker by remember { mutableStateOf(false) } + var showAppearance by remember { mutableStateOf(false) } val stopping = stopRequested || snapshot.phase == RuntimePhase.STOPPING fun createTab(rootfsName: String?) { @@ -119,6 +127,18 @@ fun InteractiveTerminalPage( } } + if (showAppearance) { + TerminalSettingsPage( + modifier = modifier, + state = profileState, + onBack = { showAppearance = false }, + onSelectProfile = onSelectProfile, + onSaveProfile = onSaveProfile, + onDeleteProfile = onDeleteProfile, + ) + return + } + Column( modifier = modifier @@ -132,6 +152,7 @@ fun InteractiveTerminalPage( stopping = stopping, onExit = onExit, onChooseDistro = { showDistroPicker = true }, + onShowAppearance = { showAppearance = true }, onStop = { stopRequested = true onStop() @@ -225,6 +246,7 @@ private fun TerminalSessionBar( stopping: Boolean, onExit: () -> Unit, onChooseDistro: () -> Unit, + onShowAppearance: () -> Unit, onStop: () -> Unit, ) { val tabClosing = @@ -326,6 +348,13 @@ private fun TerminalSessionBar( } else { Spacer(Modifier.width(48.dp)) } + IconButton(onClick = onShowAppearance) { + Icon( + imageVector = Icons.Outlined.Palette, + contentDescription = "Terminal appearance", + tint = UdroidTerminalMuted, + ) + } } } @@ -804,6 +833,7 @@ private fun LiveTerminal( ) { val context = LocalContext.current val density = LocalDensity.current + val terminalBackground = UdroidTerminal.toArgb() val modifiers = remember { TerminalModifierState() } val preferences = remember(context) { @@ -832,7 +862,7 @@ private fun LiveTerminal( val terminalView = remember { TerminalView(context, null).apply { - setBackgroundColor(android.graphics.Color.rgb(17, 19, 31)) + setBackgroundColor(terminalBackground) setTextSize(initialTextSize) setTypeface(Typeface.MONOSPACE) isFocusable = true @@ -853,6 +883,7 @@ private fun LiveTerminal( AndroidView( factory = { terminalView }, update = { view -> + view.setBackgroundColor(terminalBackground) if (view.mTermSession !== session) { view.attachSession(session) } @@ -923,7 +954,7 @@ private fun TerminalKey( .background( color = if (active) { - Color(0xFF164A39) + MaterialTheme.colorScheme.primaryContainer } else { UdroidTerminalRaised }, diff --git a/app/src/main/java/org/randomcoder/udroid/ui/TerminalSettingsPage.kt b/app/src/main/java/org/randomcoder/udroid/ui/TerminalSettingsPage.kt new file mode 100644 index 0000000..838fe6e --- /dev/null +++ b/app/src/main/java/org/randomcoder/udroid/ui/TerminalSettingsPage.kt @@ -0,0 +1,665 @@ +package org.randomcoder.udroid.ui + +import android.content.Context +import android.net.Uri +import android.provider.OpenableColumns +import androidx.activity.compose.BackHandler +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.ArrowBack +import androidx.compose.material.icons.outlined.Add +import androidx.compose.material.icons.outlined.Check +import androidx.compose.material.icons.rounded.Close +import androidx.compose.material.icons.rounded.MoreVert +import androidx.compose.material.icons.rounded.Search +import androidx.compose.material3.Button +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Slider +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import org.randomcoder.udroid.runtime.TerminalColorProfile +import org.randomcoder.udroid.runtime.TerminalProfileState +import org.randomcoder.udroid.runtime.exportTermuxProfile +import org.randomcoder.udroid.runtime.formatTerminalColor +import org.randomcoder.udroid.runtime.importTerminalProfile +import org.randomcoder.udroid.runtime.parseTerminalColor +import java.util.UUID +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlin.math.roundToInt + +@Composable +internal fun TerminalSettingsPage( + modifier: Modifier = Modifier, + state: TerminalProfileState, + onBack: () -> Unit, + onSelectProfile: (String) -> Unit, + onSaveProfile: (TerminalColorProfile) -> Unit, + onDeleteProfile: (String) -> Unit, +) { + val context = LocalContext.current + val scope = rememberCoroutineScope() + var editingProfile by remember { mutableStateOf(null) } + var pendingDeleteId by remember { mutableStateOf(null) } + var fileMessage by remember { mutableStateOf(null) } + var searchQuery by remember { mutableStateOf("") } + val importLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> + if (uri != null) { + scope.launch { + val result = + withContext(Dispatchers.IO) { + runCatching { + val fileName = context.displayName(uri) ?: "Imported profile" + importTerminalProfile(fileName, context.readPaletteText(uri)).getOrThrow() + } + } + result.onSuccess { profile -> + onSaveProfile(profile.copy(id = UUID.randomUUID().toString())) + fileMessage = "Imported ${profile.name}" + }.onFailure { error -> + fileMessage = "Could not import palette: ${error.message ?: "invalid file"}" + } + } + } + } + val exportLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri -> + if (uri != null) { + scope.launch { + val profile = state.selectedProfile + val result = + withContext(Dispatchers.IO) { + runCatching { + context.contentResolver.openOutputStream(uri)?.bufferedWriter()?.use { + it.write(exportTermuxProfile(profile)) + } ?: error("Could not open file") + } + } + result.onSuccess { + fileMessage = "Exported ${profile.name}" + }.onFailure { error -> + fileMessage = "Could not export palette: ${error.message ?: "write failed"}" + } + } + } + } + val editing = editingProfile + BackHandler { + if (editing != null) editingProfile = null else onBack() + } + if (editing != null) { + TerminalProfileEditor( + modifier = modifier, + initial = editing, + onBack = { editingProfile = null }, + onSave = { + onSaveProfile(it) + editingProfile = null + }, + ) + return + } + val profiles = filterTerminalProfiles(state.profiles, searchQuery) + + Column( + modifier = + modifier + .fillMaxSize() + .background(UdroidTerminal), + ) { + TerminalSettingsHeader(title = "Terminal appearance", onBack = onBack) + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + item { + Text( + "Profiles set the terminal, cursor, ANSI colors, and surrounding controls.", + color = UdroidTerminalMuted, + style = MaterialTheme.typography.bodyMedium, + ) + Spacer(Modifier.height(12.dp)) + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + OutlinedButton( + onClick = { + importLauncher.launch(arrayOf("text/*", "application/xml", "application/octet-stream")) + }, + modifier = Modifier.weight(1f), + ) { + Text("Import") + } + OutlinedButton( + onClick = { + val name = state.selectedProfile.name.replace(INVALID_FILE_NAME, "-") + exportLauncher.launch("$name.properties") + }, + modifier = Modifier.weight(1f), + ) { + Text("Export selected") + } + } + fileMessage?.let { message -> + Spacer(Modifier.height(8.dp)) + Text(message, color = UdroidTerminalMuted, style = MaterialTheme.typography.bodySmall) + } + } + item(key = "profile-search") { + OutlinedTextField( + value = searchQuery, + onValueChange = { searchQuery = it }, + modifier = Modifier.fillMaxWidth(), + placeholder = { Text("Search color profiles") }, + leadingIcon = { + Icon(Icons.Rounded.Search, contentDescription = null) + }, + trailingIcon = + if (searchQuery.isBlank()) { + null + } else { + { + IconButton(onClick = { searchQuery = "" }) { + Icon(Icons.Rounded.Close, contentDescription = "Clear search") + } + } + }, + singleLine = true, + shape = MaterialTheme.shapes.medium, + ) + } + if (profiles.isEmpty()) { + item(key = "no-profile-results") { + Text( + "No color profiles match “$searchQuery”.", + modifier = Modifier.padding(vertical = 12.dp), + color = UdroidTerminalMuted, + style = MaterialTheme.typography.bodyMedium, + ) + } + } + items(profiles, key = TerminalColorProfile::id) { profile -> + TerminalProfileCard( + profile = profile, + selected = profile.id == state.selectedProfile.id, + builtIn = profile.id.startsWith("builtin:"), + onSelect = { onSelectProfile(profile.id) }, + onEdit = { + editingProfile = + if (profile.id.startsWith("builtin:")) { + profile.copy( + id = UUID.randomUUID().toString(), + name = "${profile.name} custom", + ) + } else { + profile + } + }, + pendingDelete = pendingDeleteId == profile.id, + onDelete = { pendingDeleteId = profile.id }, + onConfirmDelete = { + onDeleteProfile(profile.id) + pendingDeleteId = null + }, + onCancelDelete = { pendingDeleteId = null }, + ) + } + item { + OutlinedButton( + onClick = { + editingProfile = + state.selectedProfile.copy( + id = UUID.randomUUID().toString(), + name = "New profile", + ) + }, + modifier = Modifier.fillMaxWidth(), + ) { + Icon(Icons.Outlined.Add, contentDescription = null) + Spacer(Modifier.size(8.dp)) + Text("New profile") + } + } + } + } +} + +@Composable +private fun TerminalSettingsHeader( + title: String, + onBack: () -> Unit, +) { + Row( + modifier = + Modifier + .fillMaxWidth() + .height(58.dp) + .background(UdroidTerminalSurface) + .padding(horizontal = 6.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + IconButton(onClick = onBack) { + Icon(Icons.AutoMirrored.Outlined.ArrowBack, contentDescription = "Back") + } + Text( + title, + color = UdroidTerminalText, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + ) + } +} + +@Composable +private fun TerminalProfileCard( + profile: TerminalColorProfile, + selected: Boolean, + builtIn: Boolean, + onSelect: () -> Unit, + onEdit: () -> Unit, + pendingDelete: Boolean, + onDelete: () -> Unit, + onConfirmDelete: () -> Unit, + onCancelDelete: () -> Unit, +) { + var menuExpanded by remember(profile.id) { mutableStateOf(false) } + Surface( + modifier = + Modifier + .fillMaxWidth() + .clickable(onClick = onSelect), + color = UdroidTerminalSurface, + shape = RoundedCornerShape(16.dp), + border = BorderStroke(1.dp, if (selected) UdroidTerminalGreen else UdroidTerminalLine), + ) { + Column( + modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Column(modifier = Modifier.weight(1f)) { + Text(profile.name, color = UdroidTerminalText, fontWeight = FontWeight.SemiBold) + Text( + if (builtIn) "Built-in profile" else "Custom profile", + color = UdroidTerminalMuted, + style = MaterialTheme.typography.labelMedium, + ) + } + if (selected) { + Icon(Icons.Outlined.Check, contentDescription = "Selected", tint = UdroidTerminalGreen) + } + Box { + IconButton(onClick = { menuExpanded = true }) { + Icon( + Icons.Rounded.MoreVert, + contentDescription = "Actions for ${profile.name}", + tint = UdroidTerminalMuted, + ) + } + DropdownMenu( + expanded = menuExpanded, + onDismissRequest = { menuExpanded = false }, + ) { + DropdownMenuItem( + text = { Text(if (builtIn) "Customize" else "Edit") }, + onClick = { + menuExpanded = false + onEdit() + }, + ) + if (!builtIn) { + DropdownMenuItem( + text = { Text("Delete") }, + onClick = { + menuExpanded = false + onDelete() + }, + ) + } + } + } + } + CompactTerminalProfilePreview(profile) + if (pendingDelete) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Text("Delete this profile?", modifier = Modifier.weight(1f), color = UdroidTerminalText) + TextButton(onClick = onCancelDelete) { Text("Cancel") } + TextButton(onClick = onConfirmDelete) { Text("Delete") } + } + } + } + } +} + +@Composable +private fun CompactTerminalProfilePreview(profile: TerminalColorProfile) { + Surface( + color = Color(profile.background), + shape = RoundedCornerShape(8.dp), + border = BorderStroke(1.dp, Color(profile.foreground).copy(alpha = 0.22f)), + ) { + AnsiPalette( + colors = profile.ansiColors, + modifier = Modifier.padding(6.dp), + swatchHeight = 7.dp, + spacing = 3.dp, + ) + } +} + +@Composable +private fun TerminalProfilePreview(profile: TerminalColorProfile) { + Surface( + color = Color(profile.background), + shape = RoundedCornerShape(10.dp), + border = BorderStroke(1.dp, Color(profile.foreground).copy(alpha = 0.22f)), + ) { + Column( + modifier = Modifier.padding(12.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + Text( + "user@linux:~$ printf 'Hello'", + color = Color(profile.foreground), + fontFamily = FontFamily.Monospace, + style = MaterialTheme.typography.bodySmall, + ) + AnsiPalette(colors = profile.ansiColors) + } + } +} + +@Composable +private fun AnsiPalette( + colors: List, + modifier: Modifier = Modifier, + selectedIndex: Int? = null, + onSelect: ((Int) -> Unit)? = null, + swatchHeight: Dp = if (onSelect == null) 14.dp else 32.dp, + spacing: Dp = 5.dp, +) { + Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(spacing)) { + repeat(2) { row -> + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(spacing), + ) { + repeat(8) { column -> + val index = row * 8 + column + Box( + modifier = + Modifier + .weight(1f) + .height(swatchHeight) + .background(Color(colors[index]), RoundedCornerShape(5.dp)) + .then( + if (onSelect == null) Modifier else Modifier.clickable { onSelect(index) }, + ), + contentAlignment = Alignment.Center, + ) { + if (selectedIndex == index) { + Text("•", color = contrastingColor(colors[index]), fontWeight = FontWeight.Bold) + } + } + } + } + } + } +} + +internal fun filterTerminalProfiles( + profiles: List, + query: String, +): List = + if (query.isBlank()) profiles else profiles.filter { it.name.contains(query.trim(), ignoreCase = true) } + +@Composable +private fun TerminalProfileEditor( + modifier: Modifier, + initial: TerminalColorProfile, + onBack: () -> Unit, + onSave: (TerminalColorProfile) -> Unit, +) { + var profile by remember(initial.id) { mutableStateOf(initial) } + var selectedColor by remember { mutableIntStateOf(BACKGROUND_COLOR) } + val color = profile.colorAt(selectedColor) + var hexText by remember(selectedColor, color) { mutableStateOf(formatTerminalColor(color)) } + Column( + modifier = + modifier + .fillMaxSize() + .background(UdroidTerminal), + ) { + TerminalSettingsHeader(title = "Edit profile", onBack = onBack) + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + item { + OutlinedTextField( + value = profile.name, + onValueChange = { profile = profile.copy(name = it.take(40)) }, + modifier = Modifier.fillMaxWidth(), + label = { Text("Profile name") }, + singleLine = true, + ) + } + item { TerminalProfilePreview(profile) } + item { + Text("Terminal colors", color = UdroidTerminalText, fontWeight = FontWeight.SemiBold) + Spacer(Modifier.height(8.dp)) + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + ProfileColorButton("Background", profile.background, selectedColor == BACKGROUND_COLOR) { + selectedColor = BACKGROUND_COLOR + } + ProfileColorButton("Text", profile.foreground, selectedColor == FOREGROUND_COLOR) { + selectedColor = FOREGROUND_COLOR + } + ProfileColorButton("Cursor", profile.cursor, selectedColor == CURSOR_COLOR) { + selectedColor = CURSOR_COLOR + } + } + } + item { + Text("ANSI palette", color = UdroidTerminalText, fontWeight = FontWeight.SemiBold) + Spacer(Modifier.height(8.dp)) + AnsiPalette( + colors = profile.ansiColors, + selectedIndex = selectedColor.takeIf { it >= 0 }, + onSelect = { selectedColor = it }, + ) + } + item { + HorizontalDivider(color = UdroidTerminalLine) + Spacer(Modifier.height(12.dp)) + Text( + colorName(selectedColor), + color = UdroidTerminalText, + fontWeight = FontWeight.SemiBold, + ) + ColorSlider("Red", color.redChannel()) { + profile = profile.withColor(selectedColor, color.withChannel(red = it)) + } + ColorSlider("Green", color.greenChannel()) { + profile = profile.withColor(selectedColor, color.withChannel(green = it)) + } + ColorSlider("Blue", color.blueChannel()) { + profile = profile.withColor(selectedColor, color.withChannel(blue = it)) + } + OutlinedTextField( + value = hexText, + onValueChange = { value -> + hexText = value.take(7) + parseTerminalColor(hexText)?.let { profile = profile.withColor(selectedColor, it) } + }, + modifier = Modifier.fillMaxWidth(), + label = { Text("Hex value") }, + singleLine = true, + isError = parseTerminalColor(hexText) == null, + ) + } + item { + Button( + onClick = { onSave(profile) }, + enabled = profile.name.isNotBlank(), + modifier = Modifier.fillMaxWidth(), + ) { + Text("Save profile") + } + } + } + } +} + +@Composable +private fun RowScope.ProfileColorButton( + label: String, + color: Int, + selected: Boolean, + onClick: () -> Unit, +) { + OutlinedButton( + onClick = onClick, + modifier = Modifier.weight(1f), + contentPadding = PaddingValues(horizontal = 8.dp), + border = BorderStroke(2.dp, if (selected) UdroidTerminalGreen else UdroidTerminalLine), + ) { + Box(Modifier.size(14.dp).background(Color(color), RoundedCornerShape(3.dp))) + Spacer(Modifier.size(6.dp)) + Text(label, maxLines = 1, style = MaterialTheme.typography.labelMedium) + } +} + +@Composable +private fun ColorSlider( + label: String, + value: Int, + onValueChange: (Int) -> Unit, +) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text(label, modifier = Modifier.weight(0.28f), color = UdroidTerminalMuted) + Slider( + value = value.toFloat(), + onValueChange = { onValueChange(it.roundToInt()) }, + modifier = Modifier.weight(0.72f), + valueRange = 0f..255f, + ) + Text(value.toString(), modifier = Modifier.padding(start = 8.dp), color = UdroidTerminalText) + } +} + +private fun TerminalColorProfile.colorAt(index: Int): Int = + when (index) { + BACKGROUND_COLOR -> background + FOREGROUND_COLOR -> foreground + CURSOR_COLOR -> cursor + else -> ansiColors[index] + } + +private fun TerminalColorProfile.withColor( + index: Int, + color: Int, +): TerminalColorProfile = + when (index) { + BACKGROUND_COLOR -> copy(background = color) + FOREGROUND_COLOR -> copy(foreground = color) + CURSOR_COLOR -> copy(cursor = color) + else -> copy(ansiColors = ansiColors.toMutableList().also { it[index] = color }) + } + +private fun colorName(index: Int): String = + when (index) { + BACKGROUND_COLOR -> "Background" + FOREGROUND_COLOR -> "Text" + CURSOR_COLOR -> "Cursor" + in 0..7 -> "ANSI $index" + else -> "Bright ANSI $index" + } + +private fun Int.redChannel(): Int = (this shr 16) and 0xFF + +private fun Int.greenChannel(): Int = (this shr 8) and 0xFF + +private fun Int.blueChannel(): Int = this and 0xFF + +private fun Int.withChannel( + red: Int = redChannel(), + green: Int = greenChannel(), + blue: Int = blueChannel(), +): Int = 0xFF000000.toInt() or (red shl 16) or (green shl 8) or blue + +private fun contrastingColor(color: Int): Color { + val luminance = (color.redChannel() * 299 + color.greenChannel() * 587 + color.blueChannel() * 114) / 1000 + return if (luminance >= 128) Color.Black else Color.White +} + +private const val BACKGROUND_COLOR = -3 +private const val FOREGROUND_COLOR = -2 +private const val CURSOR_COLOR = -1 +private const val MAX_PALETTE_FILE_CHARS = 1_048_576 +private val INVALID_FILE_NAME = Regex("[^A-Za-z0-9._ -]") + +private fun Context.displayName(uri: Uri): String? = + contentResolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)?.use { cursor -> + if (cursor.moveToFirst()) cursor.getString(0) else null + } + +private fun Context.readPaletteText(uri: Uri): String = + contentResolver.openInputStream(uri)?.bufferedReader()?.use { reader -> + buildString { + val buffer = CharArray(8192) + while (true) { + val count = reader.read(buffer) + if (count < 0) break + append(buffer, 0, count) + require(length <= MAX_PALETTE_FILE_CHARS) { "Palette file is larger than 1 MiB" } + } + } + } ?: error("Could not open file") diff --git a/app/src/test/java/org/randomcoder/udroid/runtime/TerminalAppearanceTest.kt b/app/src/test/java/org/randomcoder/udroid/runtime/TerminalAppearanceTest.kt new file mode 100644 index 0000000..0cd1d8a --- /dev/null +++ b/app/src/test/java/org/randomcoder/udroid/runtime/TerminalAppearanceTest.kt @@ -0,0 +1,58 @@ +package org.randomcoder.udroid.runtime + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertSame +import org.junit.Assert.assertThrows +import org.junit.Test + +class TerminalAppearanceTest { + @Test + fun parsesRgbHexWithOptionalHash() { + assertEquals(0xFF1122AA.toInt(), parseTerminalColor("#1122aa")) + assertEquals(0xFF43D292.toInt(), parseTerminalColor(" 43D292 ")) + } + + @Test + fun rejectsIncompleteOrNonRgbColors() { + assertNull(parseTerminalColor("#123")) + assertNull(parseTerminalColor("#12345678")) + assertNull(parseTerminalColor("#12ZZ56")) + } + + @Test + fun formatsColorsWithoutAlpha() { + assertEquals("#43D292", formatTerminalColor(0x7F43D292)) + } + + @Test + fun profileSerializationRoundTripsEveryTerminalColor() { + val profile = + TerminalProfiles.udroid.copy( + id = "custom-profile", + name = "My terminal | profile", + ) + + assertEquals(profile, decodeTerminalProfile(encodeTerminalProfile(profile))) + } + + @Test + fun malformedProfilesAreIgnored() { + assertNull(decodeTerminalProfile("not-a-profile")) + assertNull(decodeTerminalProfile(encodeTerminalProfile(TerminalProfiles.udroid).replace("#000000", "bad"))) + } + + @Test + fun profilesRequireAllSixteenAnsiColors() { + assertThrows(IllegalArgumentException::class.java) { + TerminalProfiles.udroid.copy(ansiColors = listOf(0xFF000000.toInt())) + } + } + + @Test + fun missingSelectionFallsBackToUdroid() { + val state = TerminalProfileState(selectedProfileId = "missing", userProfiles = emptyList()) + + assertSame(TerminalProfiles.udroid, state.selectedProfile) + } +} diff --git a/app/src/test/java/org/randomcoder/udroid/runtime/TerminalProfileCodecTest.kt b/app/src/test/java/org/randomcoder/udroid/runtime/TerminalProfileCodecTest.kt new file mode 100644 index 0000000..f73c3e3 --- /dev/null +++ b/app/src/test/java/org/randomcoder/udroid/runtime/TerminalProfileCodecTest.kt @@ -0,0 +1,84 @@ +package org.randomcoder.udroid.runtime + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class TerminalProfileCodecTest { + @Test + fun termuxExportCanBeImportedWithoutLosingColors() { + val source = TerminalProfiles.builtIns[1].copy(id = "source") + + val imported = importTerminalProfile("Breeze.properties", exportTermuxProfile(source)).getOrThrow() + + assertEquals("Breeze", imported.name) + assertEquals(source.background, imported.background) + assertEquals(source.foreground, imported.foreground) + assertEquals(source.cursor, imported.cursor) + assertEquals(source.ansiColors, imported.ansiColors) + } + + @Test + fun importsGhosttyPalette() { + val source = TerminalProfiles.udroid + val theme = + buildString { + appendLine("background = ${formatTerminalColor(source.background)}") + appendLine("foreground = ${formatTerminalColor(source.foreground)}") + appendLine("cursor-color = ${formatTerminalColor(source.cursor)}") + source.ansiColors.forEachIndexed { index, color -> + appendLine("palette = $index=${formatTerminalColor(color)}") + } + } + + val imported = importTerminalProfile("Ghostty theme", theme).getOrThrow() + + assertEquals(source.cursor, imported.cursor) + assertEquals(source.ansiColors, imported.ansiColors) + } + + @Test + fun importsItermColors() { + val source = TerminalProfiles.udroid + val colors = + buildMap { + put("Background Color", source.background) + put("Foreground Color", source.foreground) + put("Cursor Color", source.cursor) + source.ansiColors.forEachIndexed { index, color -> put("Ansi $index Color", color) } + } + val xml = + buildString { + append("") + colors.forEach { (key, color) -> + append("$key") + appendComponent("Red Component", (color shr 16) and 0xFF) + appendComponent("Green Component", (color shr 8) and 0xFF) + appendComponent("Blue Component", color and 0xFF) + append("") + } + append("") + } + + val imported = importTerminalProfile("uDroid.itermcolors", xml).getOrThrow() + + assertEquals(source.background, imported.background) + assertEquals(source.foreground, imported.foreground) + assertEquals(source.cursor, imported.cursor) + assertEquals(source.ansiColors, imported.ansiColors) + } + + @Test + fun rejectsIncompletePalettes() { + val result = importTerminalProfile("broken.properties", "background=#000000\nforeground=#FFFFFF") + + assertTrue(result.isFailure) + } + + private fun StringBuilder.appendComponent( + name: String, + value: Int, + ) { + append("$name${value / 255.0}") + } +} diff --git a/app/src/test/java/org/randomcoder/udroid/ui/TerminalSettingsPageTest.kt b/app/src/test/java/org/randomcoder/udroid/ui/TerminalSettingsPageTest.kt new file mode 100644 index 0000000..d8a6057 --- /dev/null +++ b/app/src/test/java/org/randomcoder/udroid/ui/TerminalSettingsPageTest.kt @@ -0,0 +1,23 @@ +package org.randomcoder.udroid.ui + +import org.junit.Assert.assertEquals +import org.junit.Test +import org.randomcoder.udroid.runtime.TerminalProfiles + +class TerminalSettingsPageTest { + @Test + fun profileSearchMatchesNamesCaseInsensitively() { + assertEquals( + listOf("Catppuccin Mocha"), + filterTerminalProfiles(TerminalProfiles.builtIns, " CATPPUCCIN ").map { it.name }, + ) + } + + @Test + fun blankProfileSearchKeepsEveryProfile() { + assertEquals( + TerminalProfiles.builtIns, + filterTerminalProfiles(TerminalProfiles.builtIns, " "), + ) + } +} diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index d991677..a25e2c8 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -45,6 +45,47 @@ The terminal stays alive when you move between uDroid pages. Use **Stop** on the system page when you want to end Linux or change settings that require it to be stopped. +### Terminal colors + +Select the palette button in **Terminal** to open **Terminal appearance**. You +can choose a built-in profile, create one, or customize a built-in profile as +an editable copy. Changes apply to the running terminal and are kept for new +terminal sessions. Built-in choices include uDroid, Breeze, Solarized, +Dracula, Catppuccin Mocha, Gruvbox Dark, Nord, Tokyo Night, Rosé Pine, and +Ubuntu. + +**Import** accepts these common palette formats: + +- Termux `colors.properties` files with `background`, `foreground`, optional + `cursor`, and `color0` through `color15`. +- Ghostty theme files with `background`, `foreground`, optional `cursor-color`, + and `palette = 0` through `palette = 15` entries. +- iTerm2 `.itermcolors` files. + +The foreground, background, and all 16 ANSI colors must be present. If the +cursor color is omitted, uDroid uses the foreground color. Other settings in a +theme file—such as fonts, key bindings, commands, or window options—are ignored +and never executed. Imported files must be no larger than 1 MiB. The imported +palette becomes an editable custom profile and is selected immediately. + +**Export selected** always writes a Termux-compatible `colors.properties` +file. Export a built-in profile first if you want a known-good template to edit +or share. + +You can find compatible palettes here: + +- [Termux:Styling colors](https://github.com/termux/termux-styling/tree/master/app/src/main/assets/colors) + are ready-to-import `colors.properties` files. +- [iTerm2 Color Schemes](https://github.com/mbadolato/iTerm2-Color-Schemes/releases) + publishes downloadable Termux, Ghostty, and iTerm2 theme archives. +- [Ghostty themes](https://ghostty.org/docs/features/theme) explains Ghostty's + theme format and built-in theme collection. + +Base16 and Base24 scheme files are not imported directly. Use a Termux, +Ghostty, or iTerm2 version of the scheme instead. Programs may temporarily +change colors in their own terminal session; the saved profile remains the +base appearance for the next session. + ### Desktop uDroid can start X11 desktop sessions installed inside Linux. From df940c36f8992ab45d36d85002f51c138010ac03 Mon Sep 17 00:00:00 2001 From: saicharankandukuri Date: Fri, 6 Nov 2026 16:19:50 +0530 Subject: [PATCH 05/14] feat(terminal): add font and text profiles Separate terminal colors from font and text settings with live previews and searchable font selection. Persist text size, line height, cursor style, and blink behavior in the live terminal. Bundle licensed monospace fonts and support validated multi-file TTF/OTF imports using embedded OpenType names. --- .../terminal-fonts/FiraCode-Regular.ttf | Bin 0 -> 289624 bytes .../terminal-fonts/JetBrainsMono-Regular.ttf | Bin 0 -> 270224 bytes app/src/main/assets/terminal-fonts/README.md | 10 + .../terminal-fonts/SourceCodePro-Regular.otf | Bin 0 -> 131128 bytes .../terminal-fonts/licenses/FiraCode-OFL.txt | 93 +++ .../licenses/JetBrainsMono-OFL.txt | 93 +++ .../licenses/SourceCodePro-OFL.md | 93 +++ .../runtime/RuntimeSupervisorService.kt | 23 +- .../udroid/runtime/TerminalTypography.kt | 352 ++++++++++ .../org/randomcoder/udroid/ui/AppShell.kt | 14 + .../org/randomcoder/udroid/ui/TerminalPage.kt | 94 ++- .../udroid/ui/TerminalSettingsPage.kt | 621 +++++++++++++++++- .../udroid/runtime/TerminalTypographyTest.kt | 91 +++ docs/USER_GUIDE.md | 31 +- third_party/termux/README.udroid.md | 8 +- .../com/termux/view/TerminalRenderer.java | 8 +- .../java/com/termux/view/TerminalView.java | 14 +- 17 files changed, 1503 insertions(+), 42 deletions(-) create mode 100644 app/src/main/assets/terminal-fonts/FiraCode-Regular.ttf create mode 100644 app/src/main/assets/terminal-fonts/JetBrainsMono-Regular.ttf create mode 100644 app/src/main/assets/terminal-fonts/README.md create mode 100644 app/src/main/assets/terminal-fonts/SourceCodePro-Regular.otf create mode 100644 app/src/main/assets/terminal-fonts/licenses/FiraCode-OFL.txt create mode 100644 app/src/main/assets/terminal-fonts/licenses/JetBrainsMono-OFL.txt create mode 100644 app/src/main/assets/terminal-fonts/licenses/SourceCodePro-OFL.md create mode 100644 app/src/main/java/org/randomcoder/udroid/runtime/TerminalTypography.kt create mode 100644 app/src/test/java/org/randomcoder/udroid/runtime/TerminalTypographyTest.kt diff --git a/app/src/main/assets/terminal-fonts/FiraCode-Regular.ttf b/app/src/main/assets/terminal-fonts/FiraCode-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..bd736851948d2d76483b434113e2d9ee35554446 GIT binary patch literal 289624 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͹< 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 JVW8mO{{dG|E=m9Z literal 0 HcmV?d00001 diff --git a/app/src/main/assets/terminal-fonts/JetBrainsMono-Regular.ttf b/app/src/main/assets/terminal-fonts/JetBrainsMono-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..711830ede02a366f8b99f88e52f3148405e67eaf GIT binary patch literal 270224 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

{Qq;dYoLXG*oL7!{wrG%~aKb=FoR7{s=k| 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

AzPi!HK1CYdMasJ%868x!dF;`ny|>LRT>eY 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

yPy_*I!!ydV|UaqYY&n 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�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 private val ownedGfxstreamHost = AtomicReference(null) private val ownedVirglHosts = ConcurrentHashMap() @@ -159,7 +161,8 @@ class RuntimeSupervisorService : Service() { attachedViews.forEach(TerminalView::invalidate) } - override fun getTerminalCursorStyle(): Int? = null + override fun getTerminalCursorStyle(): Int = + this@RuntimeSupervisorService.terminalCursorStyle override fun logError( tag: String, @@ -204,6 +207,7 @@ class RuntimeSupervisorService : Service() { override fun onCreate() { super.onCreate() + terminalCursorStyle = TerminalTypographyStore(this).load().cursorStyle.emulatorValue() createNotificationChannel() app.journal.append( component = "supervisor", @@ -537,6 +541,16 @@ class RuntimeSupervisorService : Service() { } } + fun updateTerminalTypography(state: TerminalTypographyState) { + mainHandler.post { + terminalCursorStyle = state.cursorStyle.emulatorValue() + terminalTabs.all().forEach { tab -> + tab.value.emulator?.setCursorStyle() + terminalClient.onTextChanged(tab.value) + } + } + } + fun requestX11RendererConnection(callback: (ParcelFileDescriptor?) -> Unit) { x11Controller.requestRendererConnection(callback) } @@ -2184,3 +2198,10 @@ class RuntimeSupervisorService : Service() { val mounts: List, ) } + +private fun TerminalCursorStyle.emulatorValue(): Int = + when (this) { + TerminalCursorStyle.BLOCK -> TerminalEmulator.TERMINAL_CURSOR_STYLE_BLOCK + TerminalCursorStyle.BAR -> TerminalEmulator.TERMINAL_CURSOR_STYLE_BAR + TerminalCursorStyle.UNDERLINE -> TerminalEmulator.TERMINAL_CURSOR_STYLE_UNDERLINE + } diff --git a/app/src/main/java/org/randomcoder/udroid/runtime/TerminalTypography.kt b/app/src/main/java/org/randomcoder/udroid/runtime/TerminalTypography.kt new file mode 100644 index 0000000..1e8dd5a --- /dev/null +++ b/app/src/main/java/org/randomcoder/udroid/runtime/TerminalTypography.kt @@ -0,0 +1,352 @@ +package org.randomcoder.udroid.runtime + +import android.content.Context +import android.graphics.Typeface +import android.net.Uri +import android.os.Build +import android.util.TypedValue +import java.io.File +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.nio.charset.Charset +import java.util.Base64 +import java.util.UUID + +const val TERMINAL_TEXT_SIZE_MIN_SP = 10f +const val TERMINAL_TEXT_SIZE_MAX_SP = 30f +const val TERMINAL_LINE_SPACING_MIN = 0.8f +const val TERMINAL_LINE_SPACING_MAX = 1.6f +const val TERMINAL_CURSOR_BLINK_RATE_DEFAULT_MS = 600 +const val TERMINAL_CURSOR_BLINK_RATE_MIN_MS = 100 +const val TERMINAL_CURSOR_BLINK_RATE_MAX_MS = 2000 +private val SAFE_FONT_FILE = Regex("[0-9a-f-]+\\.(ttf|otf)") + +enum class TerminalCursorStyle { + BLOCK, + BAR, + UNDERLINE, +} + +data class TerminalFontProfile( + val id: String, + val name: String, + val fileName: String?, + val assetPath: String? = null, +) + +data class TerminalTypographyState( + val selectedFontId: String = TerminalFonts.system.id, + val importedFonts: List = emptyList(), + val textSizeSp: Float = 15f, + val lineSpacing: Float = 1f, + val cursorStyle: TerminalCursorStyle = TerminalCursorStyle.BLOCK, + val cursorBlinkRateMs: Int = 0, +) { + val fonts: List + get() = TerminalFonts.builtIns + importedFonts + + val selectedFont: TerminalFontProfile + get() = fonts.firstOrNull { it.id == selectedFontId } ?: TerminalFonts.system + + fun normalized(): TerminalTypographyState = + copy( + selectedFontId = selectedFont.id, + importedFonts = importedFonts.distinctBy(TerminalFontProfile::id).sortedBy(TerminalFontProfile::name), + textSizeSp = textSizeSp.coerceIn(TERMINAL_TEXT_SIZE_MIN_SP, TERMINAL_TEXT_SIZE_MAX_SP), + lineSpacing = lineSpacing.coerceIn(TERMINAL_LINE_SPACING_MIN, TERMINAL_LINE_SPACING_MAX), + cursorBlinkRateMs = + if (cursorBlinkRateMs == 0) { + 0 + } else { + cursorBlinkRateMs.coerceIn( + TERMINAL_CURSOR_BLINK_RATE_MIN_MS, + TERMINAL_CURSOR_BLINK_RATE_MAX_MS, + ) + }, + ) +} + +object TerminalFonts { + val system = + TerminalFontProfile( + id = "builtin:monospace", + name = "System monospace", + fileName = null, + ) + val jetBrainsMono = + TerminalFontProfile( + id = "builtin:jetbrains-mono", + name = "JetBrains Mono", + fileName = null, + assetPath = "terminal-fonts/JetBrainsMono-Regular.ttf", + ) + val firaCode = + TerminalFontProfile( + id = "builtin:fira-code", + name = "Fira Code", + fileName = null, + assetPath = "terminal-fonts/FiraCode-Regular.ttf", + ) + val sourceCodePro = + TerminalFontProfile( + id = "builtin:source-code-pro", + name = "Source Code Pro", + fileName = null, + assetPath = "terminal-fonts/SourceCodePro-Regular.otf", + ) + val builtIns = listOf(system, jetBrainsMono, firaCode, sourceCodePro) +} + +class TerminalTypographyStore(context: Context) { + private val appContext = context.applicationContext + private val preferences = appContext.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) + private val fontsDirectory = File(appContext.filesDir, "terminal-fonts") + + fun load(): TerminalTypographyState { + var importedFonts = + preferences + .getStringSet(KEY_IMPORTED_FONTS, emptySet()) + .orEmpty() + .mapNotNull(::decodeTerminalFont) + .filter { font -> font.fileName?.let(::fontFile)?.isFile == true } + if (!preferences.getBoolean(KEY_EMBEDDED_NAMES_MIGRATED, false)) { + importedFonts = + importedFonts.map { font -> + val embeddedName = font.fileName?.let(::fontFile)?.let(::readOpenTypeFontName) + if (embeddedName == null) font else font.copy(name = embeddedName) + } + preferences + .edit() + .putStringSet(KEY_IMPORTED_FONTS, importedFonts.map(::encodeTerminalFont).toSet()) + .putBoolean(KEY_EMBEDDED_NAMES_MIGRATED, true) + .apply() + } + val cursorStyle = + runCatching { + TerminalCursorStyle.valueOf( + preferences.getString(KEY_CURSOR_STYLE, null) ?: TerminalCursorStyle.BLOCK.name, + ) + }.getOrDefault(TerminalCursorStyle.BLOCK) + return TerminalTypographyState( + selectedFontId = preferences.getString(KEY_SELECTED_FONT, null) ?: TerminalFonts.system.id, + importedFonts = importedFonts, + textSizeSp = + if (preferences.contains(KEY_TEXT_SIZE_SP)) { + preferences.getFloat(KEY_TEXT_SIZE_SP, DEFAULT_TEXT_SIZE_SP) + } else { + val legacyTextSizePx = + appContext + .getSharedPreferences(LEGACY_PREFERENCES_NAME, Context.MODE_PRIVATE) + .getInt(LEGACY_TEXT_SIZE_PX, -1) + if (legacyTextSizePx > 0) { + legacyTextSizePx.toSp() + } else { + DEFAULT_TEXT_SIZE_SP + } + }, + lineSpacing = preferences.getFloat(KEY_LINE_SPACING, 1f), + cursorStyle = cursorStyle, + cursorBlinkRateMs = preferences.getInt(KEY_CURSOR_BLINK_RATE, 0), + ).normalized() + } + + fun save(state: TerminalTypographyState) { + val normalized = state.normalized() + preferences + .edit() + .putString(KEY_SELECTED_FONT, normalized.selectedFontId) + .putStringSet(KEY_IMPORTED_FONTS, normalized.importedFonts.map(::encodeTerminalFont).toSet()) + .putFloat(KEY_TEXT_SIZE_SP, normalized.textSizeSp) + .putFloat(KEY_LINE_SPACING, normalized.lineSpacing) + .putString(KEY_CURSOR_STYLE, normalized.cursorStyle.name) + .putInt(KEY_CURSOR_BLINK_RATE, normalized.cursorBlinkRateMs) + .apply() + } + + fun importFont( + uri: Uri, + sourceName: String, + ): Result = + runCatching { + val extension = sourceName.substringAfterLast('.', "").lowercase() + require(extension == "ttf" || extension == "otf") { "Choose a .ttf or .otf font" } + fontsDirectory.mkdirs() + val id = UUID.randomUUID().toString() + val destination = File(fontsDirectory, "$id.$extension") + val temporary = File(fontsDirectory, ".$id.tmp") + try { + val input = appContext.contentResolver.openInputStream(uri) ?: error("Could not open font") + input.buffered().use { source -> + temporary.outputStream().buffered().use { target -> + val buffer = ByteArray(DEFAULT_BUFFER_SIZE) + var total = 0L + while (true) { + val count = source.read(buffer) + if (count < 0) break + total += count + require(total <= MAX_FONT_FILE_BYTES) { "Font is larger than 16 MiB" } + target.write(buffer, 0, count) + } + } + } + requireNotNull(Typeface.Builder(temporary).build()) { "Android could not read this font" } + val embeddedName = readOpenTypeFontName(temporary) + check(temporary.renameTo(destination)) { "Could not store font" } + TerminalFontProfile( + id = id, + name = + embeddedName + ?: sourceName.substringBeforeLast('.').ifBlank { "Imported font" }.take(80), + fileName = destination.name, + ) + } finally { + temporary.delete() + } + } + + fun loadTypeface(font: TerminalFontProfile): Typeface = + font.assetPath + ?.let { runCatching { Typeface.Builder(appContext.assets, it).build() }.getOrNull() } + ?: font.fileName + ?.let(::fontFile) + ?.takeIf(File::isFile) + ?.let { runCatching { Typeface.Builder(it).build() }.getOrNull() } + ?: Typeface.MONOSPACE + + fun delete(font: TerminalFontProfile) { + font.fileName?.let(::fontFile)?.delete() + } + + private fun fontFile(fileName: String): File { + require(SAFE_FONT_FILE.matches(fileName)) { "Invalid font file" } + return File(fontsDirectory, fileName) + } + + private fun Int.toSp(): Float { + val metrics = appContext.resources.displayMetrics + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + TypedValue.deriveDimension(TypedValue.COMPLEX_UNIT_SP, toFloat(), metrics) + } else { + @Suppress("DEPRECATION") + val scaledDensity = metrics.scaledDensity + this / scaledDensity + } + } + + private companion object { + const val PREFERENCES_NAME = "terminal-typography" + const val KEY_SELECTED_FONT = "selected-font" + const val KEY_IMPORTED_FONTS = "imported-fonts" + const val KEY_TEXT_SIZE_SP = "text-size-sp" + const val KEY_LINE_SPACING = "line-spacing" + const val KEY_CURSOR_STYLE = "cursor-style" + const val KEY_CURSOR_BLINK_RATE = "cursor-blink-rate" + const val KEY_EMBEDDED_NAMES_MIGRATED = "embedded-names-migrated-v1" + const val LEGACY_PREFERENCES_NAME = "terminal-view" + const val LEGACY_TEXT_SIZE_PX = "text-size-px" + const val DEFAULT_TEXT_SIZE_SP = 15f + const val MAX_FONT_FILE_BYTES = 16L * 1024 * 1024 + } +} + +internal fun encodeTerminalFont(font: TerminalFontProfile): String = + listOf( + font.id, + Base64.getUrlEncoder().withoutPadding().encodeToString(font.name.toByteArray()), + font.fileName.orEmpty(), + ).joinToString("|") + +internal fun decodeTerminalFont(value: String): TerminalFontProfile? { + val fields = value.split('|') + if (fields.size != 3 || fields[0].isBlank() || fields[2].isBlank()) return null + return runCatching { + TerminalFontProfile( + id = fields[0], + name = String(Base64.getUrlDecoder().decode(fields[1])).takeIf(String::isNotBlank) ?: error("Missing name"), + fileName = fields[2].takeIf(SAFE_FONT_FILE::matches) + ?: error("Invalid file"), + ) + }.getOrNull() +} + +internal fun readOpenTypeFontName(file: File): String? = + runCatching { + val bytes = file.readBytes() + val buffer = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN) + if (bytes.size < 12) return null + val tableCount = buffer.unsignedShort(4) + if (12L + tableCount * 16L > bytes.size) return null + + var nameOffset = -1 + var nameLength = 0 + repeat(tableCount) { index -> + val record = 12 + index * 16 + if ( + buffer.get(record) == 'n'.code.toByte() && + buffer.get(record + 1) == 'a'.code.toByte() && + buffer.get(record + 2) == 'm'.code.toByte() && + buffer.get(record + 3) == 'e'.code.toByte() + ) { + val offset = buffer.unsignedInt(record + 8) + val length = buffer.unsignedInt(record + 12) + if (offset + length <= bytes.size && offset <= Int.MAX_VALUE && length <= Int.MAX_VALUE) { + nameOffset = offset.toInt() + nameLength = length.toInt() + } + } + } + if (nameOffset < 0 || nameLength < 6) return null + + val tableEnd = nameOffset.toLong() + nameLength + val recordCount = buffer.unsignedShort(nameOffset + 2) + val stringStorage = nameOffset + buffer.unsignedShort(nameOffset + 4) + if (nameOffset + 6L + recordCount * 12L > tableEnd || stringStorage !in nameOffset..tableEnd.toInt()) { + return null + } + + var bestName: String? = null + var bestRank = Int.MAX_VALUE + repeat(recordCount) { index -> + val record = nameOffset + 6 + index * 12 + val platformId = buffer.unsignedShort(record) + val languageId = buffer.unsignedShort(record + 4) + val nameId = buffer.unsignedShort(record + 6) + val nameRank = + when (nameId) { + 4 -> 0 + 16 -> 10 + 1 -> 20 + else -> return@repeat + } + val platformRank = + when { + platformId == 3 && languageId == 0x0409 -> 0 + platformId == 0 -> 1 + platformId == 3 -> 2 + platformId == 1 && languageId == 0 -> 3 + else -> return@repeat + } + val length = buffer.unsignedShort(record + 8) + val offset = stringStorage.toLong() + buffer.unsignedShort(record + 10) + if (offset + length > tableEnd || offset > Int.MAX_VALUE) return@repeat + val encoded = bytes.copyOfRange(offset.toInt(), offset.toInt() + length) + val decoded = + when (platformId) { + 0, 3 -> String(encoded, Charsets.UTF_16BE) + 1 -> String(encoded, MAC_ROMAN) + else -> return@repeat + }.replace(Regex("\\s+"), " ").trim().take(80) + val rank = nameRank + platformRank + if (decoded.isNotEmpty() && rank < bestRank) { + bestName = decoded + bestRank = rank + } + } + bestName + }.getOrNull() + +private fun ByteBuffer.unsignedShort(offset: Int): Int = getShort(offset).toInt() and 0xffff + +private fun ByteBuffer.unsignedInt(offset: Int): Long = getInt(offset).toLong() and 0xffffffffL + +private val MAC_ROMAN: Charset = runCatching { Charset.forName("x-MacRoman") }.getOrDefault(Charsets.ISO_8859_1) diff --git a/app/src/main/java/org/randomcoder/udroid/ui/AppShell.kt b/app/src/main/java/org/randomcoder/udroid/ui/AppShell.kt index 3d59148..6eb288b 100644 --- a/app/src/main/java/org/randomcoder/udroid/ui/AppShell.kt +++ b/app/src/main/java/org/randomcoder/udroid/ui/AppShell.kt @@ -121,6 +121,8 @@ import org.randomcoder.udroid.runtime.TerminalColorProfile import org.randomcoder.udroid.runtime.TerminalProfileState import org.randomcoder.udroid.runtime.TerminalProfileStore import org.randomcoder.udroid.runtime.TerminalProfiles +import org.randomcoder.udroid.runtime.TerminalTypographyState +import org.randomcoder.udroid.runtime.TerminalTypographyStore import org.randomcoder.udroid.runtime.VirglServerMode import org.randomcoder.udroid.update.AppUpdatePhase import org.randomcoder.udroid.update.AppUpdateState @@ -250,9 +252,13 @@ fun UdroidApp( ) { val context = LocalContext.current val terminalProfileStore = remember(context) { TerminalProfileStore(context) } + val terminalTypographyStore = remember(context) { TerminalTypographyStore(context) } var terminalProfileState by remember(context) { mutableStateOf(terminalProfileStore.load()) } + var terminalTypographyState by remember(context) { + mutableStateOf(terminalTypographyStore.load()) + } var mountConfigurationSourceSystemId by rememberSaveable { mutableStateOf(null) } var selectedMountProfileSystemId by rememberSaveable { mutableStateOf(null) } val hasInstalledLinux = installedRootfsName != null @@ -291,6 +297,12 @@ fun UdroidApp( runtimeService?.updateTerminalAppearance(state.selectedProfile) onTerminalAppearanceChanged() } + fun applyTerminalTypography(state: TerminalTypographyState) { + val normalized = state.normalized() + terminalTypographyStore.save(normalized) + terminalTypographyState = normalized + runtimeService?.updateTerminalTypography(normalized) + } UdroidTerminalTheme(terminalProfileState.selectedProfile) { BackHandler { onDestinationSelected(UdroidDestination.SYSTEM) @@ -308,6 +320,7 @@ fun UdroidApp( service = runtimeService, installedRootfses = installedRootfses, profileState = terminalProfileState, + typographyState = terminalTypographyState, onSelectProfile = { profileId -> applyTerminalProfiles(terminalProfileState.copy(selectedProfileId = profileId)) }, @@ -330,6 +343,7 @@ fun UdroidApp( ), ) }, + onTypographyChanged = ::applyTerminalTypography, onStart = onStart, onStop = onStop, onExit = { onDestinationSelected(UdroidDestination.SYSTEM) }, diff --git a/app/src/main/java/org/randomcoder/udroid/ui/TerminalPage.kt b/app/src/main/java/org/randomcoder/udroid/ui/TerminalPage.kt index 05b4b66..3a2aa2d 100644 --- a/app/src/main/java/org/randomcoder/udroid/ui/TerminalPage.kt +++ b/app/src/main/java/org/randomcoder/udroid/ui/TerminalPage.kt @@ -1,7 +1,6 @@ package org.randomcoder.udroid.ui import android.content.Context -import android.graphics.Typeface import android.os.Build import android.util.Log import android.view.KeyEvent @@ -58,6 +57,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.runtime.withFrameNanos import androidx.compose.ui.Alignment @@ -80,9 +80,13 @@ import org.randomcoder.udroid.runtime.RuntimeSnapshot import org.randomcoder.udroid.runtime.RuntimeSupervisorService import org.randomcoder.udroid.runtime.TerminalColorProfile import org.randomcoder.udroid.runtime.TerminalProfileState +import org.randomcoder.udroid.runtime.TerminalTypographyState +import org.randomcoder.udroid.runtime.TerminalTypographyStore import org.randomcoder.udroid.runtime.TerminalTabCloseState import org.randomcoder.udroid.runtime.TerminalTabSnapshot import org.randomcoder.udroid.runtime.MAX_TERMINAL_TITLE_CHARS +import org.randomcoder.udroid.runtime.TERMINAL_TEXT_SIZE_MAX_SP +import org.randomcoder.udroid.runtime.TERMINAL_TEXT_SIZE_MIN_SP import org.randomcoder.udroid.runtime.terminalDistroTitle import kotlin.math.roundToInt @@ -93,9 +97,11 @@ fun InteractiveTerminalPage( service: RuntimeSupervisorService?, installedRootfses: List, profileState: TerminalProfileState, + typographyState: TerminalTypographyState, onSelectProfile: (String) -> Unit, onSaveProfile: (TerminalColorProfile) -> Unit, onDeleteProfile: (String) -> Unit, + onTypographyChanged: (TerminalTypographyState) -> Unit, onStart: () -> Unit, onStop: () -> Unit, onExit: () -> Unit, @@ -131,10 +137,12 @@ fun InteractiveTerminalPage( TerminalSettingsPage( modifier = modifier, state = profileState, + typographyState = typographyState, onBack = { showAppearance = false }, onSelectProfile = onSelectProfile, onSaveProfile = onSaveProfile, onDeleteProfile = onDeleteProfile, + onTypographyChanged = onTypographyChanged, ) return } @@ -206,6 +214,8 @@ fun InteractiveTerminalPage( modifier = Modifier.weight(1f), service = service, session = session, + typography = typographyState, + onTypographyChanged = onTypographyChanged, ) } } @@ -237,7 +247,6 @@ fun InteractiveTerminalPage( ) } } - @Composable private fun TerminalSessionBar( snapshot: RuntimeSnapshot, @@ -357,7 +366,6 @@ private fun TerminalSessionBar( } } } - @Composable @OptIn(ExperimentalFoundationApi::class) private fun TerminalTabsRow( @@ -830,41 +838,42 @@ private fun LiveTerminal( modifier: Modifier = Modifier, service: RuntimeSupervisorService, session: TerminalSession, + typography: TerminalTypographyState, + onTypographyChanged: (TerminalTypographyState) -> Unit, ) { val context = LocalContext.current val density = LocalDensity.current val terminalBackground = UdroidTerminal.toArgb() val modifiers = remember { TerminalModifierState() } - val preferences = - remember(context) { - context.applicationContext.getSharedPreferences( - TERMINAL_PREFERENCES, - Context.MODE_PRIVATE, - ) - } - val initialTextSize = remember(density) { - preferences.getInt( - KEY_TEXT_SIZE_PX, - with(density) { 15.sp.toPx().roundToInt() }, - ).coerceIn(MIN_TEXT_SIZE_PX.toInt(), MAX_TEXT_SIZE_PX.toInt()) - } + val typographyStore = remember(context) { TerminalTypographyStore(context) } + val typeface = remember(typography.selectedFont) { typographyStore.loadTypeface(typography.selectedFont) } + val textSizePx = with(density) { typography.textSizeSp.sp.toPx().roundToInt() } + val minimumTextSizePx = with(density) { TERMINAL_TEXT_SIZE_MIN_SP.sp.toPx() } + val maximumTextSizePx = with(density) { TERMINAL_TEXT_SIZE_MAX_SP.sp.toPx() } + val currentTypography by rememberUpdatedState(typography) + val currentOnTypographyChanged by rememberUpdatedState(onTypographyChanged) val client = - remember { + remember(context, density) { UdroidTerminalViewClient( context = context, modifiers = modifiers, - initialTextSize = initialTextSize, + initialTextSize = textSizePx, + minimumTextSize = minimumTextSizePx, + maximumTextSize = maximumTextSizePx, onTextSizeChanged = { - preferences.edit().putInt(KEY_TEXT_SIZE_PX, it).apply() + currentOnTypographyChanged( + currentTypography.copy( + textSizeSp = with(density) { it.toSp().value }, + ), + ) }, ) } val terminalView = - remember { + remember(context) { TerminalView(context, null).apply { setBackgroundColor(terminalBackground) - setTextSize(initialTextSize) - setTypeface(Typeface.MONOSPACE) + setTextAppearance(textSizePx, typeface, typography.lineSpacing) isFocusable = true isFocusableInTouchMode = true setTerminalViewClient(client) @@ -872,9 +881,16 @@ private fun LiveTerminal( } } + LaunchedEffect(terminalView, typeface, textSizePx, typography.lineSpacing, typography.cursorBlinkRateMs) { + client.updateTextSize(textSizePx) + terminalView.setTextAppearance(textSizePx, typeface, typography.lineSpacing) + client.updateCursorBlinkRate(typography.cursorBlinkRateMs) + } + DisposableEffect(service, terminalView) { service.attachTerminalView(terminalView) onDispose { + client.stopCursorBlinking() service.detachTerminalView(terminalView) } } @@ -980,17 +996,40 @@ private class UdroidTerminalViewClient( private val context: Context, private val modifiers: TerminalModifierState, initialTextSize: Int, + private val minimumTextSize: Float, + private val maximumTextSize: Float, private val onTextSizeChanged: (Int) -> Unit, ) : TerminalViewClient { private var terminalView: TerminalView? = null private var textSize = initialTextSize.toFloat() + private var cursorBlinkRate = 0 fun attach(view: TerminalView) { terminalView = view } + fun updateTextSize(textSize: Int) { + this.textSize = textSize.toFloat() + } + + fun updateCursorBlinkRate(rate: Int) { + cursorBlinkRate = rate + applyCursorBlinking() + } + + fun stopCursorBlinking() { + terminalView?.setTerminalCursorBlinkerState(false, true) + } + + private fun applyCursorBlinking() { + terminalView?.let { view -> + view.setTerminalCursorBlinkerRate(cursorBlinkRate) + view.setTerminalCursorBlinkerState(cursorBlinkRate > 0, true) + } + } + override fun onScale(scale: Float): Float { - textSize = (textSize * scale).coerceIn(MIN_TEXT_SIZE_PX, MAX_TEXT_SIZE_PX) + textSize = (textSize * scale).coerceIn(minimumTextSize, maximumTextSize) val roundedTextSize = textSize.roundToInt() terminalView?.setTextSize(roundedTextSize) onTextSizeChanged(roundedTextSize) @@ -1048,7 +1087,9 @@ private class UdroidTerminalViewClient( return false } - override fun onEmulatorSet() = Unit + override fun onEmulatorSet() { + applyCursorBlinking() + } override fun logError( tag: String, @@ -1100,8 +1141,3 @@ private class UdroidTerminalViewClient( Log.e(tag, error.message, error) } } - -private const val TERMINAL_PREFERENCES = "terminal-view" -private const val KEY_TEXT_SIZE_PX = "text-size-px" -private const val MIN_TEXT_SIZE_PX = 20f -private const val MAX_TEXT_SIZE_PX = 64f diff --git a/app/src/main/java/org/randomcoder/udroid/ui/TerminalSettingsPage.kt b/app/src/main/java/org/randomcoder/udroid/ui/TerminalSettingsPage.kt index 838fe6e..aed273e 100644 --- a/app/src/main/java/org/randomcoder/udroid/ui/TerminalSettingsPage.kt +++ b/app/src/main/java/org/randomcoder/udroid/ui/TerminalSettingsPage.kt @@ -1,8 +1,13 @@ package org.randomcoder.udroid.ui +import android.annotation.SuppressLint import android.content.Context +import android.graphics.Typeface import android.net.Uri import android.provider.OpenableColumns +import android.util.TypedValue +import android.view.Gravity +import android.widget.TextView import androidx.activity.compose.BackHandler import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts @@ -19,6 +24,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn @@ -26,22 +32,29 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.ArrowBack +import androidx.compose.material.icons.automirrored.outlined.KeyboardArrowRight import androidx.compose.material.icons.outlined.Add import androidx.compose.material.icons.outlined.Check +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.TextFields +import androidx.compose.material.icons.outlined.Palette import androidx.compose.material.icons.rounded.Close import androidx.compose.material.icons.rounded.MoreVert import androidx.compose.material.icons.rounded.Search import androidx.compose.material3.Button import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.FilterChip import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.RadioButton import androidx.compose.material3.Slider import androidx.compose.material3.Surface +import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -54,35 +67,61 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView import org.randomcoder.udroid.runtime.TerminalColorProfile +import org.randomcoder.udroid.runtime.TerminalCursorStyle +import org.randomcoder.udroid.runtime.TerminalFonts +import org.randomcoder.udroid.runtime.TerminalFontProfile import org.randomcoder.udroid.runtime.TerminalProfileState +import org.randomcoder.udroid.runtime.TerminalTypographyState +import org.randomcoder.udroid.runtime.TerminalTypographyStore +import org.randomcoder.udroid.runtime.TERMINAL_CURSOR_BLINK_RATE_DEFAULT_MS +import org.randomcoder.udroid.runtime.TERMINAL_CURSOR_BLINK_RATE_MAX_MS +import org.randomcoder.udroid.runtime.TERMINAL_CURSOR_BLINK_RATE_MIN_MS +import org.randomcoder.udroid.runtime.TERMINAL_LINE_SPACING_MAX +import org.randomcoder.udroid.runtime.TERMINAL_LINE_SPACING_MIN +import org.randomcoder.udroid.runtime.TERMINAL_TEXT_SIZE_MAX_SP +import org.randomcoder.udroid.runtime.TERMINAL_TEXT_SIZE_MIN_SP import org.randomcoder.udroid.runtime.exportTermuxProfile import org.randomcoder.udroid.runtime.formatTerminalColor import org.randomcoder.udroid.runtime.importTerminalProfile import org.randomcoder.udroid.runtime.parseTerminalColor +import java.util.Locale import java.util.UUID import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlin.math.roundToInt +private enum class TerminalAppearancePage { + HOME, + COLORS, + TYPOGRAPHY, + FONTS, +} + @Composable internal fun TerminalSettingsPage( modifier: Modifier = Modifier, state: TerminalProfileState, + typographyState: TerminalTypographyState, onBack: () -> Unit, onSelectProfile: (String) -> Unit, onSaveProfile: (TerminalColorProfile) -> Unit, onDeleteProfile: (String) -> Unit, + onTypographyChanged: (TerminalTypographyState) -> Unit, ) { val context = LocalContext.current val scope = rememberCoroutineScope() + val typographyStore = remember(context) { TerminalTypographyStore(context) } var editingProfile by remember { mutableStateOf(null) } + var page by remember { mutableStateOf(TerminalAppearancePage.HOME) } var pendingDeleteId by remember { mutableStateOf(null) } var fileMessage by remember { mutableStateOf(null) } var searchQuery by remember { mutableStateOf("") } @@ -129,7 +168,12 @@ internal fun TerminalSettingsPage( } val editing = editingProfile BackHandler { - if (editing != null) editingProfile = null else onBack() + when { + editing != null -> editingProfile = null + page == TerminalAppearancePage.FONTS -> page = TerminalAppearancePage.TYPOGRAPHY + page != TerminalAppearancePage.HOME -> page = TerminalAppearancePage.HOME + else -> onBack() + } } if (editing != null) { TerminalProfileEditor( @@ -143,6 +187,39 @@ internal fun TerminalSettingsPage( ) return } + if (page == TerminalAppearancePage.TYPOGRAPHY) { + TerminalTypographySettingsPage( + modifier = modifier, + state = typographyState, + colorProfile = state.selectedProfile, + store = typographyStore, + onBack = { page = TerminalAppearancePage.HOME }, + onChooseFont = { page = TerminalAppearancePage.FONTS }, + onChanged = onTypographyChanged, + ) + return + } + if (page == TerminalAppearancePage.FONTS) { + TerminalFontPickerPage( + modifier = modifier, + state = typographyState, + store = typographyStore, + onBack = { page = TerminalAppearancePage.TYPOGRAPHY }, + onChanged = onTypographyChanged, + ) + return + } + if (page == TerminalAppearancePage.HOME) { + TerminalAppearanceHome( + modifier = modifier, + colorProfile = state.selectedProfile, + typography = typographyState, + onBack = onBack, + onOpenColors = { page = TerminalAppearancePage.COLORS }, + onOpenTypography = { page = TerminalAppearancePage.TYPOGRAPHY }, + ) + return + } val profiles = filterTerminalProfiles(state.profiles, searchQuery) Column( @@ -151,7 +228,7 @@ internal fun TerminalSettingsPage( .fillMaxSize() .background(UdroidTerminal), ) { - TerminalSettingsHeader(title = "Terminal appearance", onBack = onBack) + TerminalSettingsHeader(title = "Colors", onBack = { page = TerminalAppearancePage.HOME }) LazyColumn( modifier = Modifier.fillMaxSize(), contentPadding = PaddingValues(16.dp), @@ -267,6 +344,90 @@ internal fun TerminalSettingsPage( } } +@Composable +private fun TerminalAppearanceHome( + modifier: Modifier, + colorProfile: TerminalColorProfile, + typography: TerminalTypographyState, + onBack: () -> Unit, + onOpenColors: () -> Unit, + onOpenTypography: () -> Unit, +) { + Column( + modifier = + modifier + .fillMaxSize() + .background(UdroidTerminal), + ) { + TerminalSettingsHeader(title = "Terminal appearance", onBack = onBack) + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Text( + "Choose what you want to customize.", + color = UdroidTerminalMuted, + style = MaterialTheme.typography.bodyMedium, + ) + TerminalAppearanceEntry( + icon = { Icon(Icons.Outlined.Palette, contentDescription = null, tint = UdroidTerminalGreen) }, + title = "Colors", + detail = colorProfile.name, + onClick = onOpenColors, + ) + TerminalAppearanceEntry( + icon = { Icon(Icons.Outlined.TextFields, contentDescription = null, tint = UdroidTerminalGreen) }, + title = "Font & text", + detail = "${typography.selectedFont.name} · ${typography.textSizeSp.formatValue()} sp", + onClick = onOpenTypography, + ) + } + } +} + +@Composable +private fun TerminalAppearanceEntry( + icon: @Composable () -> Unit, + title: String, + detail: String, + onClick: () -> Unit, +) { + Surface( + modifier = + Modifier + .fillMaxWidth() + .clickable(onClick = onClick), + color = UdroidTerminalSurface, + shape = RoundedCornerShape(14.dp), + border = BorderStroke(1.dp, UdroidTerminalLine), + ) { + Row( + modifier = Modifier.padding(horizontal = 14.dp, vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = + Modifier + .size(42.dp) + .background(UdroidTerminalRaised, RoundedCornerShape(12.dp)), + contentAlignment = Alignment.Center, + ) { + icon() + } + Spacer(Modifier.size(12.dp)) + Column(modifier = Modifier.weight(1f)) { + Text(title, color = UdroidTerminalText, fontWeight = FontWeight.SemiBold) + Text(detail, color = UdroidTerminalMuted, style = MaterialTheme.typography.bodySmall) + } + Icon( + Icons.AutoMirrored.Outlined.KeyboardArrowRight, + contentDescription = "Open $title settings", + tint = UdroidTerminalMuted, + ) + } + } +} + @Composable private fun TerminalSettingsHeader( title: String, @@ -293,6 +454,462 @@ private fun TerminalSettingsHeader( } } +@Composable +private fun TerminalTypographySettingsPage( + modifier: Modifier, + state: TerminalTypographyState, + colorProfile: TerminalColorProfile, + store: TerminalTypographyStore, + onBack: () -> Unit, + onChooseFont: () -> Unit, + onChanged: (TerminalTypographyState) -> Unit, +) { + val typeface = remember(state.selectedFont) { store.loadTypeface(state.selectedFont) } + + Column( + modifier = + modifier + .fillMaxSize() + .background(UdroidTerminal), + ) { + TerminalSettingsHeader(title = "Font & text", onBack = onBack) + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(14.dp), + ) { + item { + TerminalTypographyPreview( + state = state, + profile = colorProfile, + typeface = typeface, + ) + } + item { + Surface( + modifier = + Modifier + .fillMaxWidth() + .clickable(onClick = onChooseFont), + color = UdroidTerminalSurface, + shape = RoundedCornerShape(14.dp), + border = BorderStroke(1.dp, UdroidTerminalLine), + ) { + Row( + modifier = Modifier.padding(14.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Column(modifier = Modifier.weight(1f)) { + Text("Font", color = UdroidTerminalText, fontWeight = FontWeight.SemiBold) + Text( + state.selectedFont.name, + color = UdroidTerminalMuted, + style = MaterialTheme.typography.bodySmall, + ) + } + Icon( + Icons.AutoMirrored.Outlined.KeyboardArrowRight, + contentDescription = "Choose terminal font", + tint = UdroidTerminalMuted, + ) + } + } + } + item { + Surface( + color = UdroidTerminalSurface, + shape = RoundedCornerShape(14.dp), + border = BorderStroke(1.dp, UdroidTerminalLine), + ) { + Column( + modifier = Modifier.padding(14.dp), + verticalArrangement = Arrangement.spacedBy(14.dp), + ) { + TerminalSettingSlider( + label = "Text size", + valueLabel = "${state.textSizeSp.formatValue()} sp", + value = state.textSizeSp, + range = TERMINAL_TEXT_SIZE_MIN_SP..TERMINAL_TEXT_SIZE_MAX_SP, + steps = 19, + onValueChange = { onChanged(state.copy(textSizeSp = it)) }, + ) + HorizontalDivider(color = UdroidTerminalLine) + TerminalSettingSlider( + label = "Line height", + valueLabel = "${(state.lineSpacing * 100).roundToInt()}%", + value = state.lineSpacing, + range = TERMINAL_LINE_SPACING_MIN..TERMINAL_LINE_SPACING_MAX, + steps = 15, + onValueChange = { onChanged(state.copy(lineSpacing = it)) }, + ) + } + } + } + item { + Surface( + color = UdroidTerminalSurface, + shape = RoundedCornerShape(14.dp), + border = BorderStroke(1.dp, UdroidTerminalLine), + ) { + Column( + modifier = Modifier.padding(14.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Text("Cursor", color = UdroidTerminalText, fontWeight = FontWeight.SemiBold) + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + TerminalCursorStyle.entries.forEach { style -> + FilterChip( + selected = state.cursorStyle == style, + onClick = { onChanged(state.copy(cursorStyle = style)) }, + label = { Text(style.displayName()) }, + ) + } + } + Row(verticalAlignment = Alignment.CenterVertically) { + Column(modifier = Modifier.weight(1f)) { + Text("Blink cursor", color = UdroidTerminalText) + Text( + if (state.cursorBlinkRateMs == 0) "Off" else "Lower is faster", + color = UdroidTerminalMuted, + style = MaterialTheme.typography.bodySmall, + ) + } + Switch( + checked = state.cursorBlinkRateMs > 0, + onCheckedChange = { enabled -> + onChanged( + state.copy( + cursorBlinkRateMs = + if (enabled) TERMINAL_CURSOR_BLINK_RATE_DEFAULT_MS else 0, + ), + ) + }, + ) + } + if (state.cursorBlinkRateMs > 0) { + TerminalSettingSlider( + label = "Blink interval", + valueLabel = "${state.cursorBlinkRateMs} ms", + value = state.cursorBlinkRateMs.toFloat(), + range = + TERMINAL_CURSOR_BLINK_RATE_MIN_MS.toFloat().. + TERMINAL_CURSOR_BLINK_RATE_MAX_MS.toFloat(), + steps = 18, + onValueChange = { + onChanged(state.copy(cursorBlinkRateMs = (it / 100).roundToInt() * 100)) + }, + ) + } + } + } + } + item { + OutlinedButton( + onClick = { + onChanged( + state.copy( + selectedFontId = TerminalFonts.system.id, + textSizeSp = 15f, + lineSpacing = 1f, + cursorStyle = TerminalCursorStyle.BLOCK, + cursorBlinkRateMs = 0, + ), + ) + }, + modifier = Modifier.fillMaxWidth(), + ) { + Text("Reset font & text") + } + } + } + } +} + +@Composable +private fun TerminalFontPickerPage( + modifier: Modifier, + state: TerminalTypographyState, + store: TerminalTypographyStore, + onBack: () -> Unit, + onChanged: (TerminalTypographyState) -> Unit, +) { + val context = LocalContext.current + val scope = rememberCoroutineScope() + var searchQuery by remember { mutableStateOf("") } + var message by remember { mutableStateOf(null) } + val fontLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { uris -> + if (uris.isNotEmpty()) { + scope.launch { + val results = + withContext(Dispatchers.IO) { + uris.map { uri -> + val name = context.displayName(uri) ?: "Imported font.ttf" + store.importFont(uri, name) + } + } + val imported = results.mapNotNull(Result::getOrNull) + if (imported.isNotEmpty()) { + onChanged( + state.copy( + selectedFontId = imported.last().id, + importedFonts = state.importedFonts + imported, + ), + ) + } + val failed = results.size - imported.size + message = + when { + imported.size == 1 && failed == 0 -> "Imported ${imported.single().name}" + imported.isNotEmpty() && failed == 0 -> "Imported ${imported.size} fonts" + imported.isNotEmpty() -> "Imported ${imported.size} fonts; $failed failed" + else -> { + val reason = + results.firstNotNullOfOrNull { it.exceptionOrNull()?.message } + ?: "invalid files" + "Could not import fonts: $reason" + } + } + } + } + } + val fonts = state.fonts.filter { it.name.contains(searchQuery, ignoreCase = true) } + + Column( + modifier = + modifier + .fillMaxSize() + .background(UdroidTerminal), + ) { + TerminalSettingsHeader(title = "Fonts", onBack = onBack) + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + item { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + "Use a monospaced font to keep terminal columns aligned.", + modifier = Modifier.weight(1f), + color = UdroidTerminalMuted, + style = MaterialTheme.typography.bodySmall, + ) + Spacer(Modifier.size(12.dp)) + OutlinedButton( + onClick = { + fontLauncher.launch( + arrayOf( + "font/ttf", + "font/otf", + "application/x-font-ttf", + "application/x-font-opentype", + "application/octet-stream", + ), + ) + }, + ) { + Text("Import") + } + } + message?.let { + Spacer(Modifier.height(8.dp)) + Text(it, color = UdroidTerminalMuted, style = MaterialTheme.typography.bodySmall) + } + } + item { + OutlinedTextField( + value = searchQuery, + onValueChange = { searchQuery = it }, + modifier = Modifier.fillMaxWidth(), + placeholder = { Text("Search fonts") }, + leadingIcon = { Icon(Icons.Rounded.Search, contentDescription = null) }, + trailingIcon = + if (searchQuery.isBlank()) { + null + } else { + { + IconButton(onClick = { searchQuery = "" }) { + Icon(Icons.Rounded.Close, contentDescription = "Clear search") + } + } + }, + singleLine = true, + ) + } + if (fonts.isEmpty()) { + item { + Text( + "No fonts match “$searchQuery”.", + modifier = Modifier.padding(vertical = 12.dp), + color = UdroidTerminalMuted, + ) + } + } + items(fonts, key = TerminalFontProfile::id) { font -> + val typeface = remember(font) { store.loadTypeface(font) } + TerminalFontRow( + font = font, + typeface = typeface, + selected = font.id == state.selectedFontId, + onSelect = { onChanged(state.copy(selectedFontId = font.id)) }, + onDelete = + if (font.fileName == null) { + null + } else { + { + store.delete(font) + onChanged( + state.copy( + selectedFontId = + if (state.selectedFontId == font.id) { + TerminalFonts.system.id + } else { + state.selectedFontId + }, + importedFonts = state.importedFonts.filterNot { it.id == font.id }, + ), + ) + } + }, + ) + } + } + } +} + +@Composable +@SuppressLint("SetTextI18n") +private fun TerminalTypographyPreview( + state: TerminalTypographyState, + profile: TerminalColorProfile, + typeface: Typeface, +) { + Surface( + color = Color(profile.background), + shape = RoundedCornerShape(14.dp), + border = BorderStroke(1.dp, Color(profile.foreground).copy(alpha = 0.22f)), + ) { + val cursor = + when (state.cursorStyle) { + TerminalCursorStyle.BLOCK -> "█" + TerminalCursorStyle.BAR -> "│" + TerminalCursorStyle.UNDERLINE -> "_" + } + AndroidView( + factory = { context -> + TextView(context).apply { + gravity = Gravity.CENTER_VERTICAL + includeFontPadding = false + val padding = (16 * resources.displayMetrics.density).roundToInt() + setPadding(padding, padding, padding, padding) + } + }, + update = { view -> + view.text = "root@linux:~$ printf 'Hello'\nHello\n$cursor" + view.setTextColor(profile.foreground) + view.setTextSize(TypedValue.COMPLEX_UNIT_SP, state.textSizeSp) + view.typeface = typeface + view.setLineSpacing(0f, state.lineSpacing) + }, + modifier = + Modifier + .fillMaxWidth() + .heightIn(min = 116.dp), + ) + } +} + +@Composable +@SuppressLint("SetTextI18n") +private fun TerminalFontRow( + font: TerminalFontProfile, + typeface: Typeface, + selected: Boolean, + onSelect: () -> Unit, + onDelete: (() -> Unit)?, +) { + val sampleTextColor = UdroidTerminalText.toArgb() + Surface( + modifier = + Modifier + .fillMaxWidth() + .clickable(onClick = onSelect), + color = UdroidTerminalSurface, + shape = RoundedCornerShape(12.dp), + border = BorderStroke(1.dp, if (selected) UdroidTerminalGreen else UdroidTerminalLine), + ) { + Row( + modifier = Modifier.padding(start = 4.dp, end = 6.dp, top = 4.dp, bottom = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + RadioButton(selected = selected, onClick = onSelect) + Column(modifier = Modifier.weight(1f)) { + Text(font.name, color = UdroidTerminalText, fontWeight = FontWeight.Medium) + Text( + if (font.fileName == null) "Included" else "Imported", + color = UdroidTerminalMuted, + style = MaterialTheme.typography.labelSmall, + ) + AndroidView( + factory = { context -> + TextView(context).apply { + includeFontPadding = false + text = "Aa Bb Cc 0123 ~/ {}[]" + setTextSize(TypedValue.COMPLEX_UNIT_SP, 15f) + setTextColor(sampleTextColor) + } + }, + update = { it.typeface = typeface }, + modifier = Modifier.fillMaxWidth(), + ) + } + if (onDelete != null) { + IconButton(onClick = onDelete) { + Icon( + Icons.Outlined.Delete, + contentDescription = "Delete ${font.name}", + tint = UdroidTerminalMuted, + ) + } + } + } + } +} + +@Composable +private fun TerminalSettingSlider( + label: String, + valueLabel: String, + value: Float, + range: ClosedFloatingPointRange, + steps: Int, + onValueChange: (Float) -> Unit, +) { + Column { + Row { + Text(label, modifier = Modifier.weight(1f), color = UdroidTerminalText) + Text(valueLabel, color = UdroidTerminalMuted, style = MaterialTheme.typography.labelMedium) + } + Slider( + value = value, + onValueChange = onValueChange, + valueRange = range, + steps = steps, + ) + } +} + +private fun TerminalCursorStyle.displayName(): String = + when (this) { + TerminalCursorStyle.BLOCK -> "Block" + TerminalCursorStyle.BAR -> "Bar" + TerminalCursorStyle.UNDERLINE -> "Underline" + } + +private fun Float.formatValue(): String = + if (this == roundToInt().toFloat()) roundToInt().toString() else String.format(Locale.US, "%.1f", this) + @Composable private fun TerminalProfileCard( profile: TerminalColorProfile, diff --git a/app/src/test/java/org/randomcoder/udroid/runtime/TerminalTypographyTest.kt b/app/src/test/java/org/randomcoder/udroid/runtime/TerminalTypographyTest.kt new file mode 100644 index 0000000..3cc6489 --- /dev/null +++ b/app/src/test/java/org/randomcoder/udroid/runtime/TerminalTypographyTest.kt @@ -0,0 +1,91 @@ +package org.randomcoder.udroid.runtime + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.nio.charset.StandardCharsets +import java.nio.file.Files + +class TerminalTypographyTest { + @Test + fun exposesBundledFontsAsBuiltIns() { + assertEquals( + listOf("System monospace", "JetBrains Mono", "Fira Code", "Source Code Pro"), + TerminalFonts.builtIns.map(TerminalFontProfile::name), + ) + } + + @Test + fun normalizesBoundsAndMissingFontSelection() { + val state = + TerminalTypographyState( + selectedFontId = "missing", + textSizeSp = 100f, + lineSpacing = 0.1f, + cursorBlinkRateMs = 50, + ).normalized() + + assertEquals(TerminalFonts.system.id, state.selectedFontId) + assertEquals(TERMINAL_TEXT_SIZE_MAX_SP, state.textSizeSp) + assertEquals(TERMINAL_LINE_SPACING_MIN, state.lineSpacing) + assertEquals(TERMINAL_CURSOR_BLINK_RATE_MIN_MS, state.cursorBlinkRateMs) + } + + @Test + fun fontMetadataRoundTrips() { + val font = TerminalFontProfile("font-id", "JetBrains Mono", "123e4567-e89b-12d3-a456-426614174000.ttf") + + assertEquals(font, decodeTerminalFont(encodeTerminalFont(font))) + } + + @Test + fun malformedFontMetadataIsIgnored() { + assertNull(decodeTerminalFont("not-a-font")) + assertNull(decodeTerminalFont("font|Rm9udA|../font.ttf")) + } + + @Test + fun readsFullNameFromOpenTypeNameTable() { + val family = "Example Mono".toByteArray(StandardCharsets.UTF_16BE) + val fullName = "Example Mono Regular".toByteArray(StandardCharsets.UTF_16BE) + val nameTableLength = 6 + 24 + family.size + fullName.size + val buffer = ByteBuffer.allocate(28 + nameTableLength).order(ByteOrder.BIG_ENDIAN) + buffer.putInt(0x00010000) + buffer.putShort(1) + repeat(3) { buffer.putShort(0) } + buffer.put("name".toByteArray(StandardCharsets.US_ASCII)) + buffer.putInt(0) + buffer.putInt(28) + buffer.putInt(nameTableLength) + buffer.putShort(0) + buffer.putShort(2) + buffer.putShort(30) + buffer.putNameRecord(nameId = 1, length = family.size, offset = 0) + buffer.putNameRecord(nameId = 4, length = fullName.size, offset = family.size) + buffer.put(family) + buffer.put(fullName) + val file = Files.createTempFile("font-name", ".ttf").toFile() + + try { + file.writeBytes(buffer.array()) + assertEquals("Example Mono Regular", readOpenTypeFontName(file)) + } finally { + file.delete() + } + } + + private fun ByteBuffer.putNameRecord( + nameId: Int, + length: Int, + offset: Int, + ) { + putShort(3) + putShort(1) + putShort(0x0409.toShort()) + putShort(nameId.toShort()) + putShort(length.toShort()) + putShort(offset.toShort()) + } +} diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index a25e2c8..3e93cf3 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -47,10 +47,11 @@ stopped. ### Terminal colors -Select the palette button in **Terminal** to open **Terminal appearance**. You -can choose a built-in profile, create one, or customize a built-in profile as -an editable copy. Changes apply to the running terminal and are kept for new -terminal sessions. Built-in choices include uDroid, Breeze, Solarized, +Select the palette button in **Terminal** to open **Terminal appearance**, then +choose **Colors**. You can choose a built-in profile, create one, or customize +a built-in profile as an editable copy. Changes apply to the running terminal +and are kept for new terminal sessions. Built-in choices include uDroid, +Breeze, Solarized, Dracula, Catppuccin Mocha, Gruvbox Dark, Nord, Tokyo Night, Rosé Pine, and Ubuntu. @@ -86,6 +87,28 @@ Ghostty, or iTerm2 version of the scheme instead. Programs may temporarily change colors in their own terminal session; the saved profile remains the base appearance for the next session. +### Terminal font and text + +Open **Terminal appearance**, then **Font & text**, to change the terminal font, +text size, line height, cursor shape, and cursor blinking. The preview shows the +selected settings before you return to the terminal. Pinch inside the terminal +to change the same saved text-size setting. + +The app includes JetBrains Mono, Fira Code, and Source Code Pro. You can also +import a `.ttf` or `.otf` font from **Fonts**. + +**Import font** accepts one or more TTF and OTF files up to 16 MiB each. uDroid +uses the name stored inside each font, validates it, and copies it into the +app's private storage. Moving or deleting the original download does not remove +it from the terminal. Use a monospaced font to keep columns and terminal +interfaces aligned. Imported fonts can be removed from the font list; **Reset +font & text** selects Android's system monospace and restores the other defaults +without deleting imported fonts. + +[Google Fonts](https://fonts.google.com/?categoryFilters=Appearance:%2FMonospace%2FMonospace) +provides downloadable monospaced fonts. Check a font's license before sharing +or redistributing its file. + ### Desktop uDroid can start X11 desktop sessions installed inside Linux. diff --git a/third_party/termux/README.udroid.md b/third_party/termux/README.udroid.md index ed05bdc..7b4144a 100644 --- a/third_party/termux/README.udroid.md +++ b/third_party/termux/README.udroid.md @@ -16,10 +16,12 @@ The upstream license record is retained next to this file. required by current Android build tools. - `terminal-emulator/src/main/jni/Android.mk` links `libtermux.so` with `-Wl,-z,max-page-size=16384`. +- `TerminalRenderer` accepts a line-spacing multiplier, and + `TerminalView.setTextAppearance()` applies font, size, and line height with a + single PTY resize. -No Java terminal behavior was changed. uDroid owns the -`TerminalSessionClient`, `TerminalViewClient`, service lifecycle, PRoot launch -vector, and Compose integration outside this directory. +uDroid owns the `TerminalSessionClient`, `TerminalViewClient`, service +lifecycle, PRoot launch vector, and Compose integration outside this directory. ## Reproducibility check diff --git a/third_party/termux/terminal-view/src/main/java/com/termux/view/TerminalRenderer.java b/third_party/termux/terminal-view/src/main/java/com/termux/view/TerminalRenderer.java index a4bef7d..5012a18 100644 --- a/third_party/termux/terminal-view/src/main/java/com/termux/view/TerminalRenderer.java +++ b/third_party/termux/terminal-view/src/main/java/com/termux/view/TerminalRenderer.java @@ -20,6 +20,7 @@ public final class TerminalRenderer { final int mTextSize; final Typeface mTypeface; + final float mLineSpacingMultiplier; private final Paint mTextPaint = new Paint(); /** The width of a single mono spaced character obtained by {@link Paint#measureText(String)} on a single 'X'. */ @@ -34,14 +35,19 @@ public final class TerminalRenderer { private final float[] asciiMeasures = new float[127]; public TerminalRenderer(int textSize, Typeface typeface) { + this(textSize, typeface, 1.0f); + } + + public TerminalRenderer(int textSize, Typeface typeface, float lineSpacingMultiplier) { mTextSize = textSize; mTypeface = typeface; + mLineSpacingMultiplier = Math.max(0.5f, Math.min(2.0f, lineSpacingMultiplier)); mTextPaint.setTypeface(typeface); mTextPaint.setAntiAlias(true); mTextPaint.setTextSize(textSize); - mFontLineSpacing = (int) Math.ceil(mTextPaint.getFontSpacing()); + mFontLineSpacing = (int) Math.ceil(mTextPaint.getFontSpacing() * mLineSpacingMultiplier); mFontAscent = (int) Math.ceil(mTextPaint.ascent()); mFontLineSpacingAndAscent = mFontLineSpacing + mFontAscent; mFontWidth = mTextPaint.measureText("X"); diff --git a/third_party/termux/terminal-view/src/main/java/com/termux/view/TerminalView.java b/third_party/termux/terminal-view/src/main/java/com/termux/view/TerminalView.java index 351f47a..2e99119 100644 --- a/third_party/termux/terminal-view/src/main/java/com/termux/view/TerminalView.java +++ b/third_party/termux/terminal-view/src/main/java/com/termux/view/TerminalView.java @@ -496,12 +496,22 @@ public void onContextMenuClosed(Menu menu) { * @param textSize the new font size, in density-independent pixels. */ public void setTextSize(int textSize) { - mRenderer = new TerminalRenderer(textSize, mRenderer == null ? Typeface.MONOSPACE : mRenderer.mTypeface); + mRenderer = new TerminalRenderer( + textSize, + mRenderer == null ? Typeface.MONOSPACE : mRenderer.mTypeface, + mRenderer == null ? 1.0f : mRenderer.mLineSpacingMultiplier + ); updateSize(); } public void setTypeface(Typeface newTypeface) { - mRenderer = new TerminalRenderer(mRenderer.mTextSize, newTypeface); + mRenderer = new TerminalRenderer(mRenderer.mTextSize, newTypeface, mRenderer.mLineSpacingMultiplier); + updateSize(); + invalidate(); + } + + public void setTextAppearance(int textSize, Typeface typeface, float lineSpacingMultiplier) { + mRenderer = new TerminalRenderer(textSize, typeface, lineSpacingMultiplier); updateSize(); invalidate(); } From 37a6e667bbb035e2337246babfc5cfe58cbb595f Mon Sep 17 00:00:00 2001 From: saicharankandukuri Date: Fri, 6 Nov 2026 16:29:01 +0530 Subject: [PATCH 06/14] fix(terminal): expand text zoom range Allow terminal text to scale from 4sp to 96sp through pinch gestures and appearance settings. Keep the slider in one-sp increments and cover lower-bound normalization. --- .../java/org/randomcoder/udroid/runtime/TerminalTypography.kt | 4 ++-- .../java/org/randomcoder/udroid/ui/TerminalSettingsPage.kt | 2 +- .../org/randomcoder/udroid/runtime/TerminalTypographyTest.kt | 4 ++++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/randomcoder/udroid/runtime/TerminalTypography.kt b/app/src/main/java/org/randomcoder/udroid/runtime/TerminalTypography.kt index 1e8dd5a..76255c8 100644 --- a/app/src/main/java/org/randomcoder/udroid/runtime/TerminalTypography.kt +++ b/app/src/main/java/org/randomcoder/udroid/runtime/TerminalTypography.kt @@ -12,8 +12,8 @@ import java.nio.charset.Charset import java.util.Base64 import java.util.UUID -const val TERMINAL_TEXT_SIZE_MIN_SP = 10f -const val TERMINAL_TEXT_SIZE_MAX_SP = 30f +const val TERMINAL_TEXT_SIZE_MIN_SP = 4f +const val TERMINAL_TEXT_SIZE_MAX_SP = 96f const val TERMINAL_LINE_SPACING_MIN = 0.8f const val TERMINAL_LINE_SPACING_MAX = 1.6f const val TERMINAL_CURSOR_BLINK_RATE_DEFAULT_MS = 600 diff --git a/app/src/main/java/org/randomcoder/udroid/ui/TerminalSettingsPage.kt b/app/src/main/java/org/randomcoder/udroid/ui/TerminalSettingsPage.kt index aed273e..8bf46bd 100644 --- a/app/src/main/java/org/randomcoder/udroid/ui/TerminalSettingsPage.kt +++ b/app/src/main/java/org/randomcoder/udroid/ui/TerminalSettingsPage.kt @@ -530,7 +530,7 @@ private fun TerminalTypographySettingsPage( valueLabel = "${state.textSizeSp.formatValue()} sp", value = state.textSizeSp, range = TERMINAL_TEXT_SIZE_MIN_SP..TERMINAL_TEXT_SIZE_MAX_SP, - steps = 19, + steps = (TERMINAL_TEXT_SIZE_MAX_SP - TERMINAL_TEXT_SIZE_MIN_SP).roundToInt() - 1, onValueChange = { onChanged(state.copy(textSizeSp = it)) }, ) HorizontalDivider(color = UdroidTerminalLine) diff --git a/app/src/test/java/org/randomcoder/udroid/runtime/TerminalTypographyTest.kt b/app/src/test/java/org/randomcoder/udroid/runtime/TerminalTypographyTest.kt index 3cc6489..da373b4 100644 --- a/app/src/test/java/org/randomcoder/udroid/runtime/TerminalTypographyTest.kt +++ b/app/src/test/java/org/randomcoder/udroid/runtime/TerminalTypographyTest.kt @@ -29,6 +29,10 @@ class TerminalTypographyTest { assertEquals(TerminalFonts.system.id, state.selectedFontId) assertEquals(TERMINAL_TEXT_SIZE_MAX_SP, state.textSizeSp) + assertEquals( + TERMINAL_TEXT_SIZE_MIN_SP, + TerminalTypographyState(textSizeSp = 0f).normalized().textSizeSp, + ) assertEquals(TERMINAL_LINE_SPACING_MIN, state.lineSpacing) assertEquals(TERMINAL_CURSOR_BLINK_RATE_MIN_MS, state.cursorBlinkRateMs) } From e16a4196dbba639f4db5d172f09128fb8c97dee6 Mon Sep 17 00:00:00 2001 From: saicharankandukuri Date: Sun, 8 Nov 2026 13:51:34 +0530 Subject: [PATCH 07/14] feat(runtime): add PRoot IPC compatibility controls Persist per-system toggles for PRoot's System V IPC and ashmem-backed memfd compatibility, and apply them to terminal, desktop, and application launches. Expose both controls in Linux system details and retain them across filesystem resets. Related to #34 --- .../org/randomcoder/udroid/MainActivity.kt | 5 ++ .../udroid/runtime/ProotApplicationLaunch.kt | 4 + .../ProotCompatibilityConfiguration.kt | 65 +++++++++++++++++ .../udroid/runtime/ProotDesktopLaunch.kt | 4 + .../udroid/runtime/ProotTerminalLaunch.kt | 4 + .../randomcoder/udroid/ui/LinuxSystemPage.kt | 73 +++++++++++++++++++ .../ProotCompatibilityConfigurationTest.kt | 29 ++++++++ 7 files changed, 184 insertions(+) create mode 100644 app/src/main/java/org/randomcoder/udroid/runtime/ProotCompatibilityConfiguration.kt create mode 100644 app/src/test/java/org/randomcoder/udroid/runtime/ProotCompatibilityConfigurationTest.kt diff --git a/app/src/main/java/org/randomcoder/udroid/MainActivity.kt b/app/src/main/java/org/randomcoder/udroid/MainActivity.kt index a079046..85d6476 100644 --- a/app/src/main/java/org/randomcoder/udroid/MainActivity.kt +++ b/app/src/main/java/org/randomcoder/udroid/MainActivity.kt @@ -60,6 +60,7 @@ import org.randomcoder.udroid.runtime.DesktopEnvironment import org.randomcoder.udroid.runtime.DesktopEnvironmentScanner import org.randomcoder.udroid.runtime.DesktopSessionPhase import org.randomcoder.udroid.runtime.InstalledRootfs +import org.randomcoder.udroid.runtime.ProotCompatibilityConfigurationStore import org.randomcoder.udroid.runtime.ProotEnvironmentProfileStore import org.randomcoder.udroid.runtime.ProotMountProfile import org.randomcoder.udroid.runtime.ProotMountProfileValidator @@ -889,6 +890,10 @@ class MainActivity : ComponentActivity() { ?.let { cleanupWarnings += it.message ?: "launcher shortcuts" } if (resetWork == null) { + runCatching { + ProotCompatibilityConfigurationStore(this@MainActivity).remove(rootfsName) + }.exceptionOrNull() + ?.let { cleanupWarnings += it.message ?: "PRoot compatibility settings" } runCatching { ProotEnvironmentProfileStore(this@MainActivity).remove(rootfsName) } .exceptionOrNull() ?.let { cleanupWarnings += it.message ?: "environment profile" } diff --git a/app/src/main/java/org/randomcoder/udroid/runtime/ProotApplicationLaunch.kt b/app/src/main/java/org/randomcoder/udroid/runtime/ProotApplicationLaunch.kt index 174e533..642cb58 100644 --- a/app/src/main/java/org/randomcoder/udroid/runtime/ProotApplicationLaunch.kt +++ b/app/src/main/java/org/randomcoder/udroid/runtime/ProotApplicationLaunch.kt @@ -42,6 +42,7 @@ object ProotApplicationLaunchBuilder { ), ) val environmentProfile = ProotEnvironmentProfileStore(context).load(rootfs.name) + val compatibility = ProotCompatibilityConfigurationStore(context).load(rootfs.name) val guestEnvironment = ProotEnvironmentResolver.resolve(environmentProfile) val managedEnvironment = ProotEnvironmentResolver.resolveManagedOverrides(environmentProfile) @@ -56,6 +57,7 @@ object ProotApplicationLaunchBuilder { listOf(application.executable) + application.arguments, audioAuthDirectory = audioEndpoint?.hostAuthDirectory?.absolutePath, launchProfile = launchProfile, + compatibility = compatibility, mounts = mounts, guestEnvironment = guestEnvironment, managedEnvironment = managedEnvironment, @@ -96,6 +98,7 @@ object ProotApplicationLaunchBuilder { applicationArguments: List, audioAuthDirectory: String? = null, launchProfile: ProotLaunchProfile? = null, + compatibility: ProotCompatibilityConfiguration = ProotCompatibilityConfiguration(), mounts: List = ProotMountResolver.defaults(x11SocketDirectory, audioAuthDirectory), guestEnvironment: List = ProotEnvironmentResolver.resolve(), @@ -107,6 +110,7 @@ object ProotApplicationLaunchBuilder { add("--link2symlink") add("--kill-on-exit") add("--root-id") + addProotCompatibilityOptions(compatibility) add("--rootfs=$rootfsPath") addProotBindMounts(mounts) launchProfile?.addBindings(this) diff --git a/app/src/main/java/org/randomcoder/udroid/runtime/ProotCompatibilityConfiguration.kt b/app/src/main/java/org/randomcoder/udroid/runtime/ProotCompatibilityConfiguration.kt new file mode 100644 index 0000000..a8a46b5 --- /dev/null +++ b/app/src/main/java/org/randomcoder/udroid/runtime/ProotCompatibilityConfiguration.kt @@ -0,0 +1,65 @@ +package org.randomcoder.udroid.runtime + +import android.content.Context + +data class ProotCompatibilityConfiguration( + val sysvipcEnabled: Boolean = false, + val memfdFallbackEnabled: Boolean = false, +) + +class ProotCompatibilityConfigurationStore(context: Context) { + private val preferences = + context.applicationContext + .getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) + + fun load(systemId: String): ProotCompatibilityConfiguration = + ProotCompatibilityConfiguration( + sysvipcEnabled = preferences.getBoolean(key(systemId, KEY_SYSVIPC), false), + memfdFallbackEnabled = preferences.getBoolean(key(systemId, KEY_MEMFD_FALLBACK), false), + ) + + fun save( + systemId: String, + configuration: ProotCompatibilityConfiguration, + ): ProotCompatibilityConfiguration { + check( + preferences + .edit() + .putBoolean(key(systemId, KEY_SYSVIPC), configuration.sysvipcEnabled) + .putBoolean(key(systemId, KEY_MEMFD_FALLBACK), configuration.memfdFallbackEnabled) + .commit(), + ) { + "Could not save PRoot compatibility settings for $systemId" + } + return configuration + } + + fun remove(systemId: String) { + val prefix = "$systemId:" + val editor = preferences.edit() + preferences.all.keys + .filter { it.startsWith(prefix) } + .forEach(editor::remove) + check(editor.commit()) { + "Could not clear PRoot compatibility settings for $systemId" + } + } + + private fun key( + systemId: String, + setting: String, + ): String = "$systemId:$setting" + + private companion object { + const val PREFERENCES_NAME = "proot-compatibility-configuration" + const val KEY_SYSVIPC = "sysvipc" + const val KEY_MEMFD_FALLBACK = "memfd-fallback" + } +} + +internal fun MutableList.addProotCompatibilityOptions( + configuration: ProotCompatibilityConfiguration, +) { + if (configuration.sysvipcEnabled) add("--sysvipc") + if (configuration.memfdFallbackEnabled) add("--ashmem-memfd") +} diff --git a/app/src/main/java/org/randomcoder/udroid/runtime/ProotDesktopLaunch.kt b/app/src/main/java/org/randomcoder/udroid/runtime/ProotDesktopLaunch.kt index be5d0e7..faec865 100644 --- a/app/src/main/java/org/randomcoder/udroid/runtime/ProotDesktopLaunch.kt +++ b/app/src/main/java/org/randomcoder/udroid/runtime/ProotDesktopLaunch.kt @@ -34,6 +34,7 @@ object ProotDesktopLaunchBuilder { ), ) val environmentProfile = ProotEnvironmentProfileStore(context).load(rootfs.name) + val compatibility = ProotCompatibilityConfigurationStore(context).load(rootfs.name) val guestEnvironment = ProotEnvironmentResolver.resolve(environmentProfile) val managedEnvironment = ProotEnvironmentResolver.resolveManagedOverrides(environmentProfile) @@ -47,6 +48,7 @@ object ProotDesktopLaunchBuilder { configuration = configuration, audioAuthDirectory = audioEndpoint?.hostAuthDirectory?.absolutePath, launchProfile = launchProfile, + compatibility = compatibility, mounts = mounts, guestEnvironment = guestEnvironment, managedEnvironment = managedEnvironment, @@ -91,6 +93,7 @@ object ProotDesktopLaunchBuilder { hasDbusRunSession: Boolean, audioAuthDirectory: String? = null, launchProfile: ProotLaunchProfile? = null, + compatibility: ProotCompatibilityConfiguration = ProotCompatibilityConfiguration(), mounts: List = ProotMountResolver.defaults(x11SocketDirectory, audioAuthDirectory), guestEnvironment: List = ProotEnvironmentResolver.resolve(), @@ -101,6 +104,7 @@ object ProotDesktopLaunchBuilder { add("--link2symlink") add("--kill-on-exit") add("--root-id") + addProotCompatibilityOptions(compatibility) add("--rootfs=$rootfsPath") addProotBindMounts(mounts) launchProfile?.addBindings(this) diff --git a/app/src/main/java/org/randomcoder/udroid/runtime/ProotTerminalLaunch.kt b/app/src/main/java/org/randomcoder/udroid/runtime/ProotTerminalLaunch.kt index 4c33195..59f2ca2 100644 --- a/app/src/main/java/org/randomcoder/udroid/runtime/ProotTerminalLaunch.kt +++ b/app/src/main/java/org/randomcoder/udroid/runtime/ProotTerminalLaunch.kt @@ -63,6 +63,7 @@ object ProotTerminalLaunchBuilder { ), ) val environmentProfile = ProotEnvironmentProfileStore(context).load(rootfs.name) + val compatibility = ProotCompatibilityConfigurationStore(context).load(rootfs.name) val guestEnvironment = ProotEnvironmentResolver.resolve(environmentProfile) val managedEnvironment = ProotEnvironmentResolver.resolveManagedOverrides(environmentProfile) @@ -77,6 +78,7 @@ object ProotTerminalLaunchBuilder { x11SocketDirectory = x11SocketDirectory?.absolutePath, audioAuthDirectory = audioEndpoint?.hostAuthDirectory?.absolutePath, launchProfile = launchProfile, + compatibility = compatibility, mounts = mounts, guestEnvironment = guestEnvironment, managedEnvironment = managedEnvironment, @@ -107,6 +109,7 @@ object ProotTerminalLaunchBuilder { x11SocketDirectory: String? = null, audioAuthDirectory: String? = null, launchProfile: ProotLaunchProfile? = null, + compatibility: ProotCompatibilityConfiguration = ProotCompatibilityConfiguration(), mounts: List = ProotMountResolver.defaults(x11SocketDirectory, audioAuthDirectory), guestEnvironment: List = ProotEnvironmentResolver.resolve(), @@ -119,6 +122,7 @@ object ProotTerminalLaunchBuilder { add("--link2symlink") add("--kill-on-exit") add("--root-id") + addProotCompatibilityOptions(compatibility) add("--rootfs=$rootfsPath") addProotBindMounts(mounts) launchProfile?.addBindings(this) diff --git a/app/src/main/java/org/randomcoder/udroid/ui/LinuxSystemPage.kt b/app/src/main/java/org/randomcoder/udroid/ui/LinuxSystemPage.kt index ba0da2e..efa0179 100644 --- a/app/src/main/java/org/randomcoder/udroid/ui/LinuxSystemPage.kt +++ b/app/src/main/java/org/randomcoder/udroid/ui/LinuxSystemPage.kt @@ -71,6 +71,8 @@ import org.randomcoder.udroid.runtime.GraphicsProfileCompatibilityProbe import org.randomcoder.udroid.runtime.GraphicsProfileSupport import org.randomcoder.udroid.runtime.InstalledRootfs import org.randomcoder.udroid.runtime.PROOT_DEFAULT_MOUNTS +import org.randomcoder.udroid.runtime.ProotCompatibilityConfiguration +import org.randomcoder.udroid.runtime.ProotCompatibilityConfigurationStore import org.randomcoder.udroid.runtime.ProotEnvironmentProfile import org.randomcoder.udroid.runtime.ProotEnvironmentProfileStore import org.randomcoder.udroid.runtime.ProotMountProfile @@ -129,6 +131,7 @@ fun LinuxSystemPage( val openDeveloperOptions = rememberDeveloperOptionsAction(onRefreshCapabilities) val mountProfileStore = remember(context) { ProotMountProfileStore(context) } val environmentProfileStore = remember(context) { ProotEnvironmentProfileStore(context) } + val compatibilityStore = remember(context) { ProotCompatibilityConfigurationStore(context) } var confirmation by remember(rootfs.name) { mutableStateOf(null) } @@ -140,6 +143,13 @@ fun LinuxSystemPage( runCatching { environmentProfileStore.load(rootfs.name) } .getOrDefault(ProotEnvironmentProfile()) } + var compatibility by remember(rootfs.name) { + mutableStateOf( + runCatching { compatibilityStore.load(rootfs.name) } + .getOrDefault(ProotCompatibilityConfiguration()), + ) + } + var compatibilityMessage by remember(rootfs.name) { mutableStateOf(null) } val selectedEnvironment = environments.firstOrNull { it.id == configuration.environmentId } ?: environments.firstOrNull() @@ -521,6 +531,29 @@ fun LinuxSystemPage( ) } + item(key = "compatibility-label") { + UdroidSectionLabel( + text = "PRoot compatibility", + modifier = Modifier.padding(top = 4.dp), + ) + } + item(key = "compatibility-settings") { + ProotCompatibilityPanel( + configuration = compatibility, + message = compatibilityMessage, + onChange = { updated -> + runCatching { compatibilityStore.save(rootfs.name, updated) } + .onSuccess { + compatibility = it + compatibilityMessage = "Saved · New launches use changes" + }.onFailure { + compatibilityMessage = + it.message ?: "Could not save PRoot compatibility settings" + } + }, + ) + } + item(key = "filesystem-label") { UdroidSectionLabel( text = "Storage", @@ -711,6 +744,46 @@ fun LinuxSystemPage( } +@Composable +private fun ProotCompatibilityPanel( + configuration: ProotCompatibilityConfiguration, + message: String?, + onChange: (ProotCompatibilityConfiguration) -> Unit, +) { + Surface( + color = Color.Transparent, + border = BorderStroke(1.dp, UdroidLine), + shape = MaterialTheme.shapes.medium, + ) { + Column { + SettingRow( + title = "System V IPC", + detail = "Emulate shared memory, semaphores, and message queues inside each PRoot launch.", + checked = configuration.sysvipcEnabled, + enabled = true, + onCheckedChange = { onChange(configuration.copy(sysvipcEnabled = it)) }, + ) + HorizontalDivider(color = UdroidLine) + SettingRow( + title = "memfd compatibility", + detail = "Use Android shared memory when software cannot create memfd objects directly.", + checked = configuration.memfdFallbackEnabled, + enabled = true, + onCheckedChange = { onChange(configuration.copy(memfdFallbackEnabled = it)) }, + ) + message?.let { + HorizontalDivider(color = UdroidLine) + Text( + text = it, + modifier = Modifier.padding(horizontal = 14.dp, vertical = 10.dp), + color = UdroidMuted, + style = MaterialTheme.typography.bodySmall, + ) + } + } + } +} + @Composable private fun EnvironmentProfilePanel( profile: ProotEnvironmentProfile, diff --git a/app/src/test/java/org/randomcoder/udroid/runtime/ProotCompatibilityConfigurationTest.kt b/app/src/test/java/org/randomcoder/udroid/runtime/ProotCompatibilityConfigurationTest.kt new file mode 100644 index 0000000..fde21d4 --- /dev/null +++ b/app/src/test/java/org/randomcoder/udroid/runtime/ProotCompatibilityConfigurationTest.kt @@ -0,0 +1,29 @@ +package org.randomcoder.udroid.runtime + +import org.junit.Assert.assertEquals +import org.junit.Test + +class ProotCompatibilityConfigurationTest { + @Test + fun `disabled compatibility adds no PRoot options`() { + val arguments = mutableListOf("proot") + + arguments.addProotCompatibilityOptions(ProotCompatibilityConfiguration()) + + assertEquals(listOf("proot"), arguments) + } + + @Test + fun `enabled compatibility adds supported PRoot options`() { + val arguments = mutableListOf("proot") + + arguments.addProotCompatibilityOptions( + ProotCompatibilityConfiguration( + sysvipcEnabled = true, + memfdFallbackEnabled = true, + ), + ) + + assertEquals(listOf("proot", "--sysvipc", "--ashmem-memfd"), arguments) + } +} From 110d6d817099b6b9511947aad10058966cc6e2eb Mon Sep 17 00:00:00 2001 From: saicharankandukuri Date: Sun, 8 Nov 2026 14:14:27 +0530 Subject: [PATCH 08/14] feat(runtime): load shared PRoot launch flags Read udroid-launch-flags.txt from primary shared storage for each terminal, desktop, and Linux application launch. Append each non-empty, non-comment line after uDroid-managed PRoot options so users can override defaults without shell parsing. Document the file contract and cover parsing plus argument ordering across all launch paths. --- .../udroid/runtime/ProotApplicationLaunch.kt | 4 + .../udroid/runtime/ProotDesktopLaunch.kt | 4 + .../udroid/runtime/ProotLaunchFlags.kt | 27 +++++ .../udroid/runtime/ProotTerminalLaunch.kt | 4 + .../udroid/runtime/ProotLaunchFlagsTest.kt | 108 ++++++++++++++++++ docs/USER_GUIDE.md | 17 +++ 6 files changed, 164 insertions(+) create mode 100644 app/src/main/java/org/randomcoder/udroid/runtime/ProotLaunchFlags.kt create mode 100644 app/src/test/java/org/randomcoder/udroid/runtime/ProotLaunchFlagsTest.kt diff --git a/app/src/main/java/org/randomcoder/udroid/runtime/ProotApplicationLaunch.kt b/app/src/main/java/org/randomcoder/udroid/runtime/ProotApplicationLaunch.kt index 642cb58..39bd255 100644 --- a/app/src/main/java/org/randomcoder/udroid/runtime/ProotApplicationLaunch.kt +++ b/app/src/main/java/org/randomcoder/udroid/runtime/ProotApplicationLaunch.kt @@ -43,6 +43,7 @@ object ProotApplicationLaunchBuilder { ) val environmentProfile = ProotEnvironmentProfileStore(context).load(rootfs.name) val compatibility = ProotCompatibilityConfigurationStore(context).load(rootfs.name) + val additionalProotArguments = ProotLaunchFlags.load() val guestEnvironment = ProotEnvironmentResolver.resolve(environmentProfile) val managedEnvironment = ProotEnvironmentResolver.resolveManagedOverrides(environmentProfile) @@ -58,6 +59,7 @@ object ProotApplicationLaunchBuilder { audioAuthDirectory = audioEndpoint?.hostAuthDirectory?.absolutePath, launchProfile = launchProfile, compatibility = compatibility, + additionalProotArguments = additionalProotArguments, mounts = mounts, guestEnvironment = guestEnvironment, managedEnvironment = managedEnvironment, @@ -99,6 +101,7 @@ object ProotApplicationLaunchBuilder { audioAuthDirectory: String? = null, launchProfile: ProotLaunchProfile? = null, compatibility: ProotCompatibilityConfiguration = ProotCompatibilityConfiguration(), + additionalProotArguments: List = emptyList(), mounts: List = ProotMountResolver.defaults(x11SocketDirectory, audioAuthDirectory), guestEnvironment: List = ProotEnvironmentResolver.resolve(), @@ -115,6 +118,7 @@ object ProotApplicationLaunchBuilder { addProotBindMounts(mounts) launchProfile?.addBindings(this) add("--cwd=$guestWorkingDirectory") + addAll(additionalProotArguments) add("/usr/bin/env") add("-i") add("HOME=$guestHome") diff --git a/app/src/main/java/org/randomcoder/udroid/runtime/ProotDesktopLaunch.kt b/app/src/main/java/org/randomcoder/udroid/runtime/ProotDesktopLaunch.kt index faec865..265e888 100644 --- a/app/src/main/java/org/randomcoder/udroid/runtime/ProotDesktopLaunch.kt +++ b/app/src/main/java/org/randomcoder/udroid/runtime/ProotDesktopLaunch.kt @@ -35,6 +35,7 @@ object ProotDesktopLaunchBuilder { ) val environmentProfile = ProotEnvironmentProfileStore(context).load(rootfs.name) val compatibility = ProotCompatibilityConfigurationStore(context).load(rootfs.name) + val additionalProotArguments = ProotLaunchFlags.load() val guestEnvironment = ProotEnvironmentResolver.resolve(environmentProfile) val managedEnvironment = ProotEnvironmentResolver.resolveManagedOverrides(environmentProfile) @@ -49,6 +50,7 @@ object ProotDesktopLaunchBuilder { audioAuthDirectory = audioEndpoint?.hostAuthDirectory?.absolutePath, launchProfile = launchProfile, compatibility = compatibility, + additionalProotArguments = additionalProotArguments, mounts = mounts, guestEnvironment = guestEnvironment, managedEnvironment = managedEnvironment, @@ -94,6 +96,7 @@ object ProotDesktopLaunchBuilder { audioAuthDirectory: String? = null, launchProfile: ProotLaunchProfile? = null, compatibility: ProotCompatibilityConfiguration = ProotCompatibilityConfiguration(), + additionalProotArguments: List = emptyList(), mounts: List = ProotMountResolver.defaults(x11SocketDirectory, audioAuthDirectory), guestEnvironment: List = ProotEnvironmentResolver.resolve(), @@ -109,6 +112,7 @@ object ProotDesktopLaunchBuilder { addProotBindMounts(mounts) launchProfile?.addBindings(this) add("--cwd=$guestHome") + addAll(additionalProotArguments) add("/usr/bin/env") add("-i") add("HOME=$guestHome") diff --git a/app/src/main/java/org/randomcoder/udroid/runtime/ProotLaunchFlags.kt b/app/src/main/java/org/randomcoder/udroid/runtime/ProotLaunchFlags.kt new file mode 100644 index 0000000..0646890 --- /dev/null +++ b/app/src/main/java/org/randomcoder/udroid/runtime/ProotLaunchFlags.kt @@ -0,0 +1,27 @@ +package org.randomcoder.udroid.runtime + +import android.os.Environment +import java.io.File + +object ProotLaunchFlags { + const val FILE_NAME = "udroid-launch-flags.txt" + + @Suppress("DEPRECATION") + fun load(): List = load(File(Environment.getExternalStorageDirectory(), FILE_NAME)) + + internal fun load(file: File): List { + if (!file.exists()) return emptyList() + require(file.isFile) { "${file.absolutePath} is not a file" } + require(file.length() <= MAX_FILE_SIZE) { + "$FILE_NAME is larger than ${MAX_FILE_SIZE / 1024} KiB" + } + + return file.bufferedReader().useLines { lines -> + lines + .map(String::trim) + .filter { it.isNotEmpty() && !it.startsWith('#') } + .toList() + } + } + private const val MAX_FILE_SIZE = 64L * 1024 +} diff --git a/app/src/main/java/org/randomcoder/udroid/runtime/ProotTerminalLaunch.kt b/app/src/main/java/org/randomcoder/udroid/runtime/ProotTerminalLaunch.kt index 59f2ca2..2d9113f 100644 --- a/app/src/main/java/org/randomcoder/udroid/runtime/ProotTerminalLaunch.kt +++ b/app/src/main/java/org/randomcoder/udroid/runtime/ProotTerminalLaunch.kt @@ -64,6 +64,7 @@ object ProotTerminalLaunchBuilder { ) val environmentProfile = ProotEnvironmentProfileStore(context).load(rootfs.name) val compatibility = ProotCompatibilityConfigurationStore(context).load(rootfs.name) + val additionalProotArguments = ProotLaunchFlags.load() val guestEnvironment = ProotEnvironmentResolver.resolve(environmentProfile) val managedEnvironment = ProotEnvironmentResolver.resolveManagedOverrides(environmentProfile) @@ -79,6 +80,7 @@ object ProotTerminalLaunchBuilder { audioAuthDirectory = audioEndpoint?.hostAuthDirectory?.absolutePath, launchProfile = launchProfile, compatibility = compatibility, + additionalProotArguments = additionalProotArguments, mounts = mounts, guestEnvironment = guestEnvironment, managedEnvironment = managedEnvironment, @@ -110,6 +112,7 @@ object ProotTerminalLaunchBuilder { audioAuthDirectory: String? = null, launchProfile: ProotLaunchProfile? = null, compatibility: ProotCompatibilityConfiguration = ProotCompatibilityConfiguration(), + additionalProotArguments: List = emptyList(), mounts: List = ProotMountResolver.defaults(x11SocketDirectory, audioAuthDirectory), guestEnvironment: List = ProotEnvironmentResolver.resolve(), @@ -127,6 +130,7 @@ object ProotTerminalLaunchBuilder { addProotBindMounts(mounts) launchProfile?.addBindings(this) add("--cwd=$guestHome") + addAll(additionalProotArguments) add("/usr/bin/env") add("-i") add("HOME=$guestHome") diff --git a/app/src/test/java/org/randomcoder/udroid/runtime/ProotLaunchFlagsTest.kt b/app/src/test/java/org/randomcoder/udroid/runtime/ProotLaunchFlagsTest.kt new file mode 100644 index 0000000..dc5731b --- /dev/null +++ b/app/src/test/java/org/randomcoder/udroid/runtime/ProotLaunchFlagsTest.kt @@ -0,0 +1,108 @@ +package org.randomcoder.udroid.runtime + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import java.nio.file.Files + +class ProotLaunchFlagsTest { + @Test + fun `missing file adds no launch arguments`() { + val directory = Files.createTempDirectory("udroid-launch-flags").toFile() + try { + assertEquals(emptyList(), ProotLaunchFlags.load(directory.resolve("missing.txt"))) + } finally { + directory.deleteRecursively() + } + } + + @Test + fun `each non-empty non-comment line is one literal argument`() { + val file = Files.createTempFile("udroid-launch-flags", ".txt").toFile() + try { + file.writeText( + """ + # Extra bindings for every Linux system + + -b + /storage/emulated/0/My Files:/mnt/my files + -v + """.trimIndent(), + ) + + assertEquals( + listOf("-b", "/storage/emulated/0/My Files:/mnt/my files", "-v"), + ProotLaunchFlags.load(file), + ) + } finally { + file.delete() + } + } + + @Test + fun `all proot arguments are passed through literally`() { + val file = Files.createTempFile("udroid-launch-flags", ".txt").toFile() + try { + val arguments = listOf("--", "-r", "--rootfs=/tmp/root", "-R/tmp/root", "--cwd=/tmp") + file.writeText(arguments.joinToString("\n")) + + assertEquals(arguments, ProotLaunchFlags.load(file)) + } finally { + file.delete() + } + } + + @Test + fun `global arguments reach terminal desktop and application launches`() { + val extra = listOf("-v", "2") + val terminal = + ProotTerminalLaunchBuilder.buildArguments( + linker = "linker64", + prootPath = "proot", + rootfsPath = "rootfs", + guestHome = "/root", + guestShell = "/bin/sh", + additionalProotArguments = extra, + ).toList() + val desktop = + ProotDesktopLaunchBuilder.buildArguments( + prootPath = "proot", + rootfsPath = "rootfs", + x11SocketDirectory = "/x11/.X11-unix", + guestHome = "/root", + environment = + DesktopEnvironment( + id = "xfce", + name = "Xfce", + command = listOf("startxfce4"), + desktopFilePath = "/usr/share/xsessions/xfce.desktop", + kind = DesktopEnvironmentKind.XFCE, + ), + configuration = + DesktopConfiguration( + environmentId = "xfce", + compositingEnabled = false, + touchScaleEnabled = false, + ), + hasDbusRunSession = false, + additionalProotArguments = extra, + ) + val application = + ProotApplicationLaunchBuilder.buildArguments( + prootPath = "proot", + rootfsPath = "rootfs", + x11SocketDirectory = "/x11/.X11-unix", + guestHome = "/root", + guestWorkingDirectory = "/root", + applicationArguments = listOf("/usr/bin/demo"), + additionalProotArguments = extra, + ) + + listOf(terminal, desktop, application).forEach { arguments -> + assertTrue(arguments.indexOf("-v") > arguments.indexOf("--rootfs=rootfs")) + assertTrue(arguments.indexOf("-v") > arguments.indexOfFirst { it.startsWith("--cwd=") }) + assertTrue(arguments.indexOf("-v") < arguments.indexOf("/usr/bin/env")) + assertEquals("2", arguments[arguments.indexOf("-v") + 1]) + } + } +} diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 3e93cf3..e94e4d6 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -218,6 +218,23 @@ terminal, app, and desktop launches. Restart Linux when an existing terminal needs the new values. **Reset** restores uDroid-managed values without removing your custom variables. +Advanced users can add global PRoot arguments in +`/storage/emulated/0/udroid-launch-flags.txt`. Put one complete argument on each +line; blank lines and lines beginning with `#` are ignored. Options with a value +use two lines, for example: + +```text +-b +/storage/emulated/0/Projects:/mnt/projects +``` + +The file is read again for every terminal, desktop, and Linux app launch. It +requires Android storage access. Arguments are passed to PRoot without policy +restrictions after uDroid's own PRoot options, so duplicate options can override +uDroid defaults and invalid arguments can break the complete launch command. +Keep the file under your control because another app with all-files access +could edit it. + ## Reset or delete a system Stop its terminal and desktop first. From 05e7f69eae93b60f0c5bfb7af6cc91f8234b0428 Mon Sep 17 00:00:00 2001 From: saicharankandukuri Date: Sun, 8 Nov 2026 15:31:35 +0530 Subject: [PATCH 09/14] feat(terminal): track shell command lifecycle Parse OSC 133 semantic prompt markers and propagate command activity through terminal session state so closing a busy tab requires confirmation. Install shell-native lifecycle hooks for Bash, Zsh, and Fish in configured rootfses, refresh them on terminal launch, and document OSC compatibility. --- .../udroid/install/AndroidRootfsRuntime.kt | 121 +++++++++++++++++- .../udroid/runtime/ProotTerminalLaunch.kt | 2 + .../runtime/RuntimeSupervisorService.kt | 13 ++ .../udroid/runtime/TerminalTabs.kt | 12 ++ .../org/randomcoder/udroid/ui/TerminalPage.kt | 45 ++++++- .../udroid/install/RootfsPipelineTest.kt | 36 +++++- .../udroid/runtime/TerminalTabRegistryTest.kt | 13 ++ docs/USER_GUIDE.md | 34 +++++ .../com/termux/terminal/TerminalEmulator.java | 27 ++++ .../com/termux/terminal/TerminalOutput.java | 3 + .../com/termux/terminal/TerminalSession.java | 5 + .../terminal/TerminalSessionClient.java | 2 + .../terminal/OperatingSystemControlTest.java | 18 +++ .../com/termux/terminal/TerminalTestCase.java | 6 + 14 files changed, 332 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/randomcoder/udroid/install/AndroidRootfsRuntime.kt b/app/src/main/java/org/randomcoder/udroid/install/AndroidRootfsRuntime.kt index 326b5a2..fee29ed 100644 --- a/app/src/main/java/org/randomcoder/udroid/install/AndroidRootfsRuntime.kt +++ b/app/src/main/java/org/randomcoder/udroid/install/AndroidRootfsRuntime.kt @@ -325,6 +325,13 @@ class AndroidRootfsConfigurator : RootfsConfigurator { File(rootfs, "etc/resolv.conf"), "nameserver 1.1.1.1\nnameserver 8.8.8.8\n", ) + configureShellProfile(rootfs) + appendAndroidGroups(File(rootfs, "etc/group")) + File(rootfs, "usr/bin/sudo").takeIf(File::exists)?.setExecutable(true, false) + } + + fun configureShellProfile(rootfs: File) { + val integrationDirectory = File(rootfs, "etc/udroid/shell-integration") replaceFile( File(rootfs, "etc/profile.d/udroid.sh"), """ @@ -339,11 +346,109 @@ class AndroidRootfsConfigurator : RootfsConfigurator { export PULSE_SERVER=127.0.0.1 export TERM=${'$'}{TERM-xterm-256color} export TMPDIR=/tmp + + if [ -n "${'$'}{BASH_VERSION-}" ] && [ -r /etc/udroid/shell-integration/udroid.bash ]; then + . /etc/udroid/shell-integration/udroid.bash + fi """.trimIndent() + "\n", executable = true, ) - appendAndroidGroups(File(rootfs, "etc/group")) - File(rootfs, "usr/bin/sudo").takeIf(File::exists)?.setExecutable(true, false) + replaceFile( + File(integrationDirectory, "udroid.bash"), + """ + case ${'$'}- in + *i*) ;; + *) return ;; + esac + [ -z "${'$'}{UDROID_BASH_INTEGRATION-}" ] || return + UDROID_BASH_INTEGRATION=1 + + __udroid_prompt_end='\[\e]133;B\a\]' + __udroid_prompt_command() { + __udroid_status=${'$'}? + printf '\033]133;D;%s\033\\\033]133;A\033\\' "${'$'}__udroid_status" + case ${'$'}PS1 in + *"${'$'}__udroid_prompt_end"*) ;; + *) PS1="${'$'}PS1${'$'}__udroid_prompt_end" ;; + esac + } + PS0="${'$'}{PS0-}"'\[\e]133;C\a\]' + case "${'$'}(declare -p PROMPT_COMMAND 2>/dev/null)" in + "declare -a"*) PROMPT_COMMAND+=(__udroid_prompt_command) ;; + *) + if [ -n "${'$'}{PROMPT_COMMAND-}" ]; then + PROMPT_COMMAND="${'$'}PROMPT_COMMAND;__udroid_prompt_command" + else + PROMPT_COMMAND=__udroid_prompt_command + fi + ;; + esac + """.trimIndent() + "\n", + ) + + if (listOf("bin/zsh", "usr/bin/zsh").any { File(rootfs, it).exists() }) { + replaceFile( + File(integrationDirectory, "udroid.zsh"), + """ + [[ -o interactive ]] || return + [[ -z "${'$'}{UDROID_ZSH_INTEGRATION-}" ]] || return + typeset -g UDROID_ZSH_INTEGRATION=1 + typeset -gi __udroid_command_running=0 + autoload -Uz add-zsh-hook + + __udroid_prompt_command() { + local command_status=${'$'}? + if (( __udroid_command_running )); then + builtin print -n -- "\e]133;D;${'$'}command_status\a" + fi + builtin print -n -- "\e]133;A\a" + __udroid_command_running=0 + } + + __udroid_preexec() { + builtin print -n -- "\e]133;B\a\e]133;C\a" + __udroid_command_running=1 + } + + add-zsh-hook precmd __udroid_prompt_command + add-zsh-hook preexec __udroid_preexec + """.trimIndent() + "\n", + ) + ensureLine( + File(rootfs, "etc/zsh/zshrc"), + "[[ -r /etc/udroid/shell-integration/udroid.zsh ]] && builtin source /etc/udroid/shell-integration/udroid.zsh", + ) + } + + if (listOf("bin/fish", "usr/bin/fish").any { File(rootfs, it).exists() }) { + replaceFile( + File(rootfs, "etc/fish/conf.d/udroid.fish"), + """ + status is-interactive; or return + set -q UDROID_FISH_INTEGRATION; and return + set -g UDROID_FISH_INTEGRATION 1 + set -g __udroid_command_running 0 + + function __udroid_prompt_command --on-event fish_prompt + if test "${'$'}__udroid_command_running" = 1 + printf '\e]133;D\a' + end + printf '\e]133;A\a' + set -g __udroid_command_running 0 + end + + function __udroid_preexec --on-event fish_preexec + printf '\e]133;B\a\e]133;C\a' + set -g __udroid_command_running 1 + end + + function __udroid_postexec --on-event fish_postexec + printf '\e]133;D;%s\a' ${'$'}status + set -g __udroid_command_running 0 + end + """.trimIndent() + "\n", + ) + } } private fun appendAndroidGroups(groupFile: File) { @@ -387,6 +492,18 @@ class AndroidRootfsConfigurator : RootfsConfigurator { if (executable) target.setExecutable(true, false) } + private fun ensureLine( + target: File, + line: String, + ) { + target.parentFile?.mkdirs() + val existing = target.takeIf(File::isFile)?.readText().orEmpty() + if (existing.lineSequence().any { it == line }) return + target.appendText((if (existing.isEmpty() || existing.endsWith('\n')) "" else "\n") + line + "\n") + target.setReadable(true, false) + target.setWritable(true, true) + } + private fun File.isSymbolicLink(): Boolean = Files.isSymbolicLink(toPath()) } diff --git a/app/src/main/java/org/randomcoder/udroid/runtime/ProotTerminalLaunch.kt b/app/src/main/java/org/randomcoder/udroid/runtime/ProotTerminalLaunch.kt index 2d9113f..813f298 100644 --- a/app/src/main/java/org/randomcoder/udroid/runtime/ProotTerminalLaunch.kt +++ b/app/src/main/java/org/randomcoder/udroid/runtime/ProotTerminalLaunch.kt @@ -2,6 +2,7 @@ package org.randomcoder.udroid.runtime import android.content.Context import org.randomcoder.udroid.audio.AudioEndpoint +import org.randomcoder.udroid.install.AndroidRootfsConfigurator import org.randomcoder.udroid.install.ProotRuntime import org.randomcoder.udroid.install.RootfsInstallationPipeline import java.io.File @@ -35,6 +36,7 @@ object ProotTerminalLaunchBuilder { require(File(rootfs, RootfsInstallationPipeline.READY_MARKER).isFile) { "The selected Linux image is not ready" } + AndroidRootfsConfigurator().configureShellProfile(rootfs) val linker = AndroidExecutableCommand.systemLinkerPath() val temporaryDirectory = File(context.cacheDir, "proot").apply { diff --git a/app/src/main/java/org/randomcoder/udroid/runtime/RuntimeSupervisorService.kt b/app/src/main/java/org/randomcoder/udroid/runtime/RuntimeSupervisorService.kt index 234d6b3..575c741 100644 --- a/app/src/main/java/org/randomcoder/udroid/runtime/RuntimeSupervisorService.kt +++ b/app/src/main/java/org/randomcoder/udroid/runtime/RuntimeSupervisorService.kt @@ -105,6 +105,18 @@ class RuntimeSupervisorService : Service() { } } + override fun onCommandStateChanged( + changedSession: TerminalSession, + running: Boolean, + ) { + mainHandler.post { + val tab = terminalTabs.findByValue(changedSession) ?: return@post + if (terminalTabs.updateCommandRunning(tab.id, running) != null) { + scheduleTerminalTabsChanged() + } + } + } + override fun onSessionFinished(finishedSession: TerminalSession) { handleSessionFinished(finishedSession) } @@ -299,6 +311,7 @@ class RuntimeSupervisorService : Service() { rootfsName = tab.rootfsName, pid = tab.value.pid.takeIf { it > 0 }?.toLong(), running = tab.value.isRunning, + commandRunning = tab.commandRunning, active = tab.id == terminalTabs.activeId, closeState = tab.closeState, closeError = tab.closeError, diff --git a/app/src/main/java/org/randomcoder/udroid/runtime/TerminalTabs.kt b/app/src/main/java/org/randomcoder/udroid/runtime/TerminalTabs.kt index 809290e..9458a8f 100644 --- a/app/src/main/java/org/randomcoder/udroid/runtime/TerminalTabs.kt +++ b/app/src/main/java/org/randomcoder/udroid/runtime/TerminalTabs.kt @@ -7,6 +7,7 @@ data class TerminalTabSnapshot( val rootfsName: String, val pid: Long?, val running: Boolean, + val commandRunning: Boolean, val active: Boolean, val closeState: TerminalTabCloseState = TerminalTabCloseState.OPEN, val closeError: String? = null, @@ -41,6 +42,7 @@ internal data class TerminalTab( val value: T, var shellTitle: String? = null, var customTitle: String? = null, + var commandRunning: Boolean = false, var closeState: TerminalTabCloseState = TerminalTabCloseState.OPEN, var closeError: String? = null, ) { @@ -101,6 +103,16 @@ internal class TerminalTabRegistry { return tab } + fun updateCommandRunning( + id: String, + running: Boolean, + ): TerminalTab? { + val tab = tabs[id] ?: return null + if (tab.commandRunning == running) return null + tab.commandRunning = running + return tab + } + fun beginClose(id: String): TerminalTab? { val tab = tabs[id] ?: return null if (tab.closeState == TerminalTabCloseState.CLOSING || diff --git a/app/src/main/java/org/randomcoder/udroid/ui/TerminalPage.kt b/app/src/main/java/org/randomcoder/udroid/ui/TerminalPage.kt index 3a2aa2d..2bde184 100644 --- a/app/src/main/java/org/randomcoder/udroid/ui/TerminalPage.kt +++ b/app/src/main/java/org/randomcoder/udroid/ui/TerminalPage.kt @@ -112,6 +112,7 @@ fun InteractiveTerminalPage( var stopRequested by remember(session) { mutableStateOf(false) } var tabError by remember { mutableStateOf(null) } var renamingTab by remember { mutableStateOf(null) } + var pendingCloseTabId by remember { mutableStateOf(null) } var creatingTab by remember { mutableStateOf(false) } var showDistroPicker by remember { mutableStateOf(false) } var showAppearance by remember { mutableStateOf(false) } @@ -133,6 +134,12 @@ fun InteractiveTerminalPage( } } + fun closeTab(id: String) { + if (service?.closeTerminalTab(id) != true) { + tabError = "Could not close terminal" + } + } + if (showAppearance) { TerminalSettingsPage( modifier = modifier, @@ -174,8 +181,10 @@ fun InteractiveTerminalPage( onRename = { tab -> renamingTab = tab }, onFollowTitle = { id -> service?.followTerminalShellTitle(id) }, onClose = { id -> - if (service?.closeTerminalTab(id) != true) { - tabError = "Could not close terminal" + if (tabs.firstOrNull { it.id == id }?.commandRunning == true) { + pendingCloseTabId = id + } else { + closeTab(id) } }, creatingTab = creatingTab, @@ -235,6 +244,38 @@ fun InteractiveTerminalPage( ) } + tabs.firstOrNull { it.id == pendingCloseTabId }?.let { tab -> + val stopsLinux = tabs.size == 1 + AlertDialog( + onDismissRequest = { pendingCloseTabId = null }, + title = { Text(if (stopsLinux) "Stop Linux?" else "Close tab?") }, + text = { + Text( + if (stopsLinux) { + "A command is still running. Stopping Linux will end it." + } else { + "A command is still running in ${tab.title}. Closing the tab will end it." + }, + ) + }, + confirmButton = { + Button( + onClick = { + pendingCloseTabId = null + closeTab(tab.id) + }, + ) { + Text(if (stopsLinux) "Stop Linux" else "Close tab") + } + }, + dismissButton = { + TextButton(onClick = { pendingCloseTabId = null }) { + Text("Cancel") + } + }, + ) + } + if (showDistroPicker) { TerminalDistroPicker( installedRootfses = installedRootfses, diff --git a/app/src/test/java/org/randomcoder/udroid/install/RootfsPipelineTest.kt b/app/src/test/java/org/randomcoder/udroid/install/RootfsPipelineTest.kt index 13167e6..6049d53 100644 --- a/app/src/test/java/org/randomcoder/udroid/install/RootfsPipelineTest.kt +++ b/app/src/test/java/org/randomcoder/udroid/install/RootfsPipelineTest.kt @@ -151,7 +151,41 @@ class RootfsPipelineTest { AndroidRootfsConfigurator().configure(rootfs) assertTrue(File(rootfs, "etc/hosts").readText().contains("localhost")) - assertTrue(File(rootfs, "etc/profile.d/udroid.sh").canExecute()) + val profile = File(rootfs, "etc/profile.d/udroid.sh") + val profileContents = profile.readText() + assertTrue(profile.canExecute()) + assertTrue(profileContents.contains("udroid.bash")) + val bashIntegration = File(rootfs, "etc/udroid/shell-integration/udroid.bash").readText() + assertTrue(bashIntegration.contains("133;A")) + assertTrue(bashIntegration.contains("133;B")) + assertTrue(bashIntegration.contains("133;C")) + assertTrue(bashIntegration.contains("133;D")) + } + + @Test + fun `available shells receive native command lifecycle hooks`() { + val rootfs = Files.createTempDirectory("udroid-shell-configurator-test").toFile() + File(rootfs, "usr/bin").mkdirs() + File(rootfs, "usr/bin/zsh").createNewFile() + File(rootfs, "usr/bin/fish").createNewFile() + + val configurator = AndroidRootfsConfigurator() + configurator.configure(rootfs) + configurator.configureShellProfile(rootfs) + + val zshIntegration = File(rootfs, "etc/udroid/shell-integration/udroid.zsh").readText() + assertTrue(zshIntegration.contains("add-zsh-hook precmd")) + assertTrue(zshIntegration.contains("add-zsh-hook preexec")) + assertEquals( + 1, + File(rootfs, "etc/zsh/zshrc") + .readLines() + .count { it.contains("udroid.zsh") }, + ) + val fishIntegration = File(rootfs, "etc/fish/conf.d/udroid.fish").readText() + assertTrue(fishIntegration.contains("fish_prompt")) + assertTrue(fishIntegration.contains("fish_preexec")) + assertTrue(fishIntegration.contains("fish_postexec")) } private fun fixture(): Fixture { diff --git a/app/src/test/java/org/randomcoder/udroid/runtime/TerminalTabRegistryTest.kt b/app/src/test/java/org/randomcoder/udroid/runtime/TerminalTabRegistryTest.kt index 8f52aae..42e4240 100644 --- a/app/src/test/java/org/randomcoder/udroid/runtime/TerminalTabRegistryTest.kt +++ b/app/src/test/java/org/randomcoder/udroid/runtime/TerminalTabRegistryTest.kt @@ -129,6 +129,19 @@ class TerminalTabRegistryTest { assertNull(registry.get("one")?.closeError) } + @Test + fun `command state is isolated per tab and ignores duplicate markers`() { + val registry = TerminalTabRegistry() + registry.add(tab("one")) + registry.add(tab("two")) + + assertEquals("one", registry.updateCommandRunning("one", true)?.id) + assertNull(registry.updateCommandRunning("one", true)) + assertEquals(true, registry.get("one")?.commandRunning) + assertEquals(false, registry.get("two")?.commandRunning) + assertEquals(false, registry.updateCommandRunning("one", false)?.commandRunning) + } + @Test fun `rootfs names become readable distro titles`() { assertEquals("Ubuntu Jammy", terminalDistroTitle("udroid-jammy-raw")) diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index e94e4d6..84bd6f8 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -109,6 +109,40 @@ without deleting imported fonts. provides downloadable monospaced fonts. Check a font's license before sharing or redistributing its file. +### Terminal OSC compatibility + +Terminal programs use Operating System Command (OSC) sequences to communicate +with the terminal outside the normal text grid. uDroid currently supports: + +| Sequence | Support | uDroid behavior | +| --- | --- | --- | +| OSC 0, 1, 2 | Supported | Sets the shell title. A manually renamed tab keeps its name until **Follow shell title** is selected. | +| OSC 4 | Supported | Changes indexed terminal colors for the current session. | +| OSC 10, 11, 12 | Supported | Changes the current foreground, background, and cursor colors. | +| OSC 52 | Write only | Copies Base64-encoded text from a terminal program to the Android clipboard. | +| OSC 104, 110, 111, 112 | Supported | Resets indexed colors, foreground, background, or cursor color. | +| OSC 133 A/B/C/D | Bash, Zsh, and Fish | Tracks whether the foreground command is running so uDroid can warn before closing its tab. | +| OSC 7 | Not supported | The terminal does not track the shell's current directory. | +| OSC 8 | Not supported | Hyperlinks are displayed as ordinary text. | +| OSC 9, 777 | Not supported | Terminal programs cannot create Android notifications. | +| OSC 9;4 | Not supported | Terminal progress is not shown in the Android UI. | +| OSC 1337 images | Not supported | Inline image rendering is not implemented by the embedded Termux terminal. | + +Supported OSC sequences may end with either BEL or ST. OSC 133 command +tracking is automatically configured for interactive Bash, Zsh, and Fish +shells. A newly installed Zsh or Fish is detected when the next uDroid terminal +starts. Tracking cannot detect background jobs after the prompt returns, and a +prompt framework that replaces the shell's registered hooks may disable it. +The exit status carried by OSC 133 D is accepted but is not shown or saved. + +OSC 52 lets command output replace the Android clipboard, so run untrusted +terminal programs with the same care as programs that can print arbitrary +terminal control sequences. See the +[xterm control-sequence reference](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html) +for title, color, and clipboard sequences, and +[VS Code shell integration](https://code.visualstudio.com/docs/terminal/shell-integration) +for the OSC 133 A/B/C/D protocol used by the Bash integration. + ### Desktop uDroid can start X11 desktop sessions installed inside Linux. diff --git a/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java b/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java index 8d7eaa8..5c46a1c 100644 --- a/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java +++ b/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java @@ -188,6 +188,9 @@ public final class TerminalEmulator { /** Holds OSC and device control arguments, which can be strings. */ private final StringBuilder mOSCOrDeviceControlArgs = new StringBuilder(); + /** Whether OSC 133 has marked a foreground command as running. */ + private boolean mCommandRunning; + /** * True if the current escape sequence should continue, false if the current escape sequence should be terminated. * Used when parsing a single character. @@ -2110,6 +2113,9 @@ private void doOscSetTextParameters(String bellOrStringTerminator) { Logger.logError(mClient, LOG_TAG, "OSC Manipulate selection, invalid string '" + textParameter + ""); } break; + case 133: // FinalTerm semantic prompt markers. + handleSemanticPrompt(textParameter); + break; case 104: // "104;$c" → Reset Color Number $c. It is reset to the color specified by the corresponding X // resource. Any number of c parameters may be given. These parameters correspond to the ANSI colors 0-7, @@ -2288,6 +2294,26 @@ private void collectOSCArgs(int b) { } } + private void handleSemanticPrompt(String parameter) { + if (parameter.isEmpty() || (parameter.length() > 1 && parameter.charAt(1) != ';')) return; + switch (parameter.charAt(0)) { + case 'C': + setCommandRunning(true); + break; + case 'A': + case 'B': + case 'D': + setCommandRunning(false); + break; + } + } + + private void setCommandRunning(boolean running) { + if (mCommandRunning == running) return; + mCommandRunning = running; + mSession.onCommandStateChanged(running); + } + private void unimplementedSequence(int b) { logError("Unimplemented sequence char '" + (char) b + "' (U+" + String.format("%04x", b) + ")"); finishSequence(); @@ -2524,6 +2550,7 @@ public void clearScrollCounter() { /** Reset terminal state so user can interact with it regardless of present state. */ public void reset() { + setCommandRunning(false); setCursorStyle(); mArgIndex = 0; mContinueSequence = false; diff --git a/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalOutput.java b/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalOutput.java index 305082a..3d444f4 100644 --- a/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalOutput.java +++ b/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalOutput.java @@ -18,6 +18,9 @@ public final void write(String data) { /** Notify the terminal client that the terminal title has changed. */ public abstract void titleChanged(String oldTitle, String newTitle); + /** Notify the terminal client that OSC 133 marked a command as running or finished. */ + public abstract void onCommandStateChanged(boolean running); + /** Notify the terminal client that text should be copied to clipboard. */ public abstract void onCopyTextToClipboard(String text); diff --git a/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalSession.java b/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalSession.java index 324e0bc..60d1240 100644 --- a/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalSession.java +++ b/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalSession.java @@ -259,6 +259,11 @@ public void titleChanged(String oldTitle, String newTitle) { mClient.onTitleChanged(this); } + @Override + public void onCommandStateChanged(boolean running) { + mClient.onCommandStateChanged(this, running); + } + public synchronized boolean isRunning() { return mShellPid != -1; } diff --git a/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalSessionClient.java b/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalSessionClient.java index 0ffe8e4..8ee4df7 100644 --- a/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalSessionClient.java +++ b/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalSessionClient.java @@ -11,6 +11,8 @@ public interface TerminalSessionClient { void onTitleChanged(TerminalSession changedSession); + void onCommandStateChanged(TerminalSession changedSession, boolean running); + void onSessionFinished(TerminalSession finishedSession); void onCopyTextToClipboard(TerminalSession session, String text); diff --git a/third_party/termux/terminal-emulator/src/test/java/com/termux/terminal/OperatingSystemControlTest.java b/third_party/termux/terminal-emulator/src/test/java/com/termux/terminal/OperatingSystemControlTest.java index 13377c0..20564da 100644 --- a/third_party/termux/terminal-emulator/src/test/java/com/termux/terminal/OperatingSystemControlTest.java +++ b/third_party/termux/terminal-emulator/src/test/java/com/termux/terminal/OperatingSystemControlTest.java @@ -9,6 +9,24 @@ /** "ESC ]" is the Operating System Command. */ public class OperatingSystemControlTest extends TerminalTestCase { + public void testSemanticPromptCommandState() { + withTerminalSized(10, 10); + + enterString("\033]133;A\007\033]133;B\033\\"); + assertTrue(mOutput.commandStateChanges.isEmpty()); + + enterString("\033]133;C\007\033]133;C;ignored\033\\"); + assertEquals(1, mOutput.commandStateChanges.size()); + assertTrue(mOutput.commandStateChanges.get(0)); + + enterString("\033]133;D;7\033\\"); + assertEquals(2, mOutput.commandStateChanges.size()); + assertFalse(mOutput.commandStateChanges.get(1)); + + enterString("\033]133;invalid\007"); + assertEquals(2, mOutput.commandStateChanges.size()); + } + public void testSetTitle() throws Exception { List expectedTitleChanges = new ArrayList<>(); diff --git a/third_party/termux/terminal-emulator/src/test/java/com/termux/terminal/TerminalTestCase.java b/third_party/termux/terminal-emulator/src/test/java/com/termux/terminal/TerminalTestCase.java index 4490207..ac0f7d2 100644 --- a/third_party/termux/terminal-emulator/src/test/java/com/termux/terminal/TerminalTestCase.java +++ b/third_party/termux/terminal-emulator/src/test/java/com/termux/terminal/TerminalTestCase.java @@ -20,6 +20,7 @@ public static class MockTerminalOutput extends TerminalOutput { final ByteArrayOutputStream baos = new ByteArrayOutputStream(); public final List titleChanges = new ArrayList<>(); public final List clipboardPuts = new ArrayList<>(); + public final List commandStateChanges = new ArrayList<>(); public int bellsRung = 0; public int colorsChanged = 0; @@ -39,6 +40,11 @@ public void titleChanged(String oldTitle, String newTitle) { titleChanges.add(new ChangedTitle(oldTitle, newTitle)); } + @Override + public void onCommandStateChanged(boolean running) { + commandStateChanges.add(running); + } + @Override public void onCopyTextToClipboard(String text) { clipboardPuts.add(text); From 52dc6a2e44ef717ec82ae1ce7f4d8fab3a78d420 Mon Sep 17 00:00:00 2001 From: saicharankandukuri Date: Sun, 8 Nov 2026 15:53:27 +0530 Subject: [PATCH 10/14] feat(terminal): inherit guest working directory Parse validated local OSC 7 reports and track the current directory per terminal tab. New tabs for the same Linux system start in that directory when it still resolves inside the rootfs or a configured bind mount, otherwise they fall back to the guest home. Install Bash, Zsh, and Fish working-directory hooks and document the supported URI forms and fallback behavior. --- .../udroid/install/AndroidRootfsRuntime.kt | 23 ++++++ .../udroid/runtime/ProotTerminalLaunch.kt | 42 ++++++++++- .../runtime/RuntimeSupervisorService.kt | 16 ++++ .../udroid/runtime/TerminalTabs.kt | 11 +++ .../udroid/install/RootfsPipelineTest.kt | 5 ++ .../udroid/runtime/ProotTerminalLaunchTest.kt | 75 +++++++++++++++++++ .../udroid/runtime/TerminalTabRegistryTest.kt | 12 +++ docs/USER_GUIDE.md | 12 ++- .../com/termux/terminal/TerminalEmulator.java | 39 ++++++++++ .../com/termux/terminal/TerminalOutput.java | 3 + .../com/termux/terminal/TerminalSession.java | 5 ++ .../terminal/TerminalSessionClient.java | 2 + .../terminal/OperatingSystemControlTest.java | 15 ++++ .../com/termux/terminal/TerminalTestCase.java | 6 ++ 14 files changed, 261 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/randomcoder/udroid/install/AndroidRootfsRuntime.kt b/app/src/main/java/org/randomcoder/udroid/install/AndroidRootfsRuntime.kt index fee29ed..0b6e73c 100644 --- a/app/src/main/java/org/randomcoder/udroid/install/AndroidRootfsRuntime.kt +++ b/app/src/main/java/org/randomcoder/udroid/install/AndroidRootfsRuntime.kt @@ -364,9 +364,15 @@ class AndroidRootfsConfigurator : RootfsConfigurator { UDROID_BASH_INTEGRATION=1 __udroid_prompt_end='\[\e]133;B\a\]' + __udroid_last_cwd= __udroid_prompt_command() { __udroid_status=${'$'}? printf '\033]133;D;%s\033\\\033]133;A\033\\' "${'$'}__udroid_status" + __udroid_cwd=${'$'}{PWD//[[:cntrl:]]/} + if [ "${'$'}__udroid_cwd" != "${'$'}__udroid_last_cwd" ]; then + printf '\033]7;kitty-shell-cwd://localhost%s\033\\' "${'$'}__udroid_cwd" + __udroid_last_cwd=${'$'}__udroid_cwd + fi case ${'$'}PS1 in *"${'$'}__udroid_prompt_end"*) ;; *) PS1="${'$'}PS1${'$'}__udroid_prompt_end" ;; @@ -394,8 +400,16 @@ class AndroidRootfsConfigurator : RootfsConfigurator { [[ -z "${'$'}{UDROID_ZSH_INTEGRATION-}" ]] || return typeset -g UDROID_ZSH_INTEGRATION=1 typeset -gi __udroid_command_running=0 + typeset -g __udroid_last_cwd= autoload -Uz add-zsh-hook + __udroid_report_cwd() { + local cwd=${'$'}{PWD//[[:cntrl:]]/} + [[ "${'$'}cwd" == "${'$'}__udroid_last_cwd" ]] && return + builtin print -n -- "\e]7;kitty-shell-cwd://localhost${'$'}cwd\e\\" + __udroid_last_cwd=${'$'}cwd + } + __udroid_prompt_command() { local command_status=${'$'}? if (( __udroid_command_running )); then @@ -403,6 +417,7 @@ class AndroidRootfsConfigurator : RootfsConfigurator { fi builtin print -n -- "\e]133;A\a" __udroid_command_running=0 + __udroid_report_cwd } __udroid_preexec() { @@ -412,6 +427,7 @@ class AndroidRootfsConfigurator : RootfsConfigurator { add-zsh-hook precmd __udroid_prompt_command add-zsh-hook preexec __udroid_preexec + add-zsh-hook chpwd __udroid_report_cwd """.trimIndent() + "\n", ) ensureLine( @@ -429,6 +445,11 @@ class AndroidRootfsConfigurator : RootfsConfigurator { set -g UDROID_FISH_INTEGRATION 1 set -g __udroid_command_running 0 + function __udroid_report_cwd --on-variable PWD + status is-command-substitution; and return + printf '\e]7;file://localhost%s\a' (string escape --style=url ${'$'}PWD) + end + function __udroid_prompt_command --on-event fish_prompt if test "${'$'}__udroid_command_running" = 1 printf '\e]133;D\a' @@ -446,6 +467,8 @@ class AndroidRootfsConfigurator : RootfsConfigurator { printf '\e]133;D;%s\a' ${'$'}status set -g __udroid_command_running 0 end + + __udroid_report_cwd """.trimIndent() + "\n", ) } diff --git a/app/src/main/java/org/randomcoder/udroid/runtime/ProotTerminalLaunch.kt b/app/src/main/java/org/randomcoder/udroid/runtime/ProotTerminalLaunch.kt index 813f298..8f32f39 100644 --- a/app/src/main/java/org/randomcoder/udroid/runtime/ProotTerminalLaunch.kt +++ b/app/src/main/java/org/randomcoder/udroid/runtime/ProotTerminalLaunch.kt @@ -15,6 +15,7 @@ data class ProotTerminalLaunch( val environment: Array, val rootfs: File, val mounts: List, + val guestWorkingDirectory: String, ) object InstalledRootfsResolver { @@ -32,6 +33,7 @@ object ProotTerminalLaunchBuilder { x11SocketDirectory: File? = null, audioEndpoint: AudioEndpoint? = null, launchProfile: ProotLaunchProfile? = null, + requestedWorkingDirectory: String? = null, ): ProotTerminalLaunch { require(File(rootfs, RootfsInstallationPipeline.READY_MARKER).isFile) { "The selected Linux image is not ready" @@ -70,6 +72,8 @@ object ProotTerminalLaunchBuilder { val guestEnvironment = ProotEnvironmentResolver.resolve(environmentProfile) val managedEnvironment = ProotEnvironmentResolver.resolveManagedOverrides(environmentProfile) + val guestWorkingDirectory = + resolveGuestWorkingDirectory(rootfs, mounts, requestedWorkingDirectory, guestHome) val arguments = buildArguments( @@ -77,6 +81,7 @@ object ProotTerminalLaunchBuilder { prootPath = runtime.executable.absolutePath, rootfsPath = ProotPathContract.rootfsPath(context, rootfs), guestHome = guestHome, + guestWorkingDirectory = guestWorkingDirectory, guestShell = guestShell, x11SocketDirectory = x11SocketDirectory?.absolutePath, audioAuthDirectory = audioEndpoint?.hostAuthDirectory?.absolutePath, @@ -101,6 +106,7 @@ object ProotTerminalLaunchBuilder { environment = environment, rootfs = rootfs, mounts = mounts, + guestWorkingDirectory = guestWorkingDirectory, ) } @@ -109,6 +115,7 @@ object ProotTerminalLaunchBuilder { prootPath: String, rootfsPath: String, guestHome: String, + guestWorkingDirectory: String = guestHome, guestShell: String, x11SocketDirectory: String? = null, audioAuthDirectory: String? = null, @@ -131,7 +138,7 @@ object ProotTerminalLaunchBuilder { add("--rootfs=$rootfsPath") addProotBindMounts(mounts) launchProfile?.addBindings(this) - add("--cwd=$guestHome") + add("--cwd=$guestWorkingDirectory") addAll(additionalProotArguments) add("/usr/bin/env") add("-i") @@ -179,4 +186,37 @@ object ProotTerminalLaunchBuilder { candidate.isFile || Files.isSymbolicLink(candidate.toPath()) } } + + internal fun resolveGuestWorkingDirectory( + rootfs: File, + mounts: List, + requestedPath: String?, + fallback: String, + ): String { + if (requestedPath == null || + requestedPath.length > 4096 || + !requestedPath.startsWith('/') || + requestedPath.any(Char::isISOControl) || + requestedPath.split('/').any { it == "." || it == ".." } + ) { + return fallback + } + val mount = + mounts + .filter { + requestedPath == it.guestTarget || + requestedPath.startsWith("${it.guestTarget.trimEnd('/')}/") + } + .maxByOrNull { it.guestTarget.length } + val hostPath = + if (mount == null) { + File(rootfs, requestedPath.removePrefix("/")) + } else { + File( + mount.hostSource, + requestedPath.removePrefix(mount.guestTarget).removePrefix("/"), + ) + } + return requestedPath.takeIf { hostPath.isDirectory } ?: fallback + } } diff --git a/app/src/main/java/org/randomcoder/udroid/runtime/RuntimeSupervisorService.kt b/app/src/main/java/org/randomcoder/udroid/runtime/RuntimeSupervisorService.kt index 575c741..0b56582 100644 --- a/app/src/main/java/org/randomcoder/udroid/runtime/RuntimeSupervisorService.kt +++ b/app/src/main/java/org/randomcoder/udroid/runtime/RuntimeSupervisorService.kt @@ -117,6 +117,16 @@ class RuntimeSupervisorService : Service() { } } + override fun onWorkingDirectoryChanged( + changedSession: TerminalSession, + path: String, + ) { + mainHandler.post { + val tab = terminalTabs.findByValue(changedSession) ?: return@post + terminalTabs.updateWorkingDirectory(tab.id, path) + } + } + override fun onSessionFinished(finishedSession: TerminalSession) { handleSessionFinished(finishedSession) } @@ -335,6 +345,10 @@ class RuntimeSupervisorService : Service() { return } terminalCreationInFlight = true + val inheritedWorkingDirectory = + terminalTabs.active() + ?.takeIf { it.rootfsName == rootfsName } + ?.workingDirectory val x11SocketDirectory = x11Controller.activeSocketDirectory() val audioEndpoint = audioController.endpoint() applicationExecutor.execute { @@ -358,6 +372,7 @@ class RuntimeSupervisorService : Service() { audioEndpoint = audioEndpoint, launchProfile = graphicsLaunchProfile(graphicsProfile, virglServerMode, rootfs), + requestedWorkingDirectory = inheritedWorkingDirectory, ) } mainHandler.post { @@ -1645,6 +1660,7 @@ class RuntimeSupervisorService : Service() { defaultTitle = title, rootfsName = launch.rootfs.name, value = session, + workingDirectory = launch.guestWorkingDirectory, ) terminalTabs.add(tab) try { diff --git a/app/src/main/java/org/randomcoder/udroid/runtime/TerminalTabs.kt b/app/src/main/java/org/randomcoder/udroid/runtime/TerminalTabs.kt index 9458a8f..b669d4a 100644 --- a/app/src/main/java/org/randomcoder/udroid/runtime/TerminalTabs.kt +++ b/app/src/main/java/org/randomcoder/udroid/runtime/TerminalTabs.kt @@ -43,6 +43,7 @@ internal data class TerminalTab( var shellTitle: String? = null, var customTitle: String? = null, var commandRunning: Boolean = false, + var workingDirectory: String? = null, var closeState: TerminalTabCloseState = TerminalTabCloseState.OPEN, var closeError: String? = null, ) { @@ -113,6 +114,16 @@ internal class TerminalTabRegistry { return tab } + fun updateWorkingDirectory( + id: String, + path: String, + ): TerminalTab? { + val tab = tabs[id] ?: return null + if (tab.workingDirectory == path) return null + tab.workingDirectory = path + return tab + } + fun beginClose(id: String): TerminalTab? { val tab = tabs[id] ?: return null if (tab.closeState == TerminalTabCloseState.CLOSING || diff --git a/app/src/test/java/org/randomcoder/udroid/install/RootfsPipelineTest.kt b/app/src/test/java/org/randomcoder/udroid/install/RootfsPipelineTest.kt index 6049d53..ba0cf14 100644 --- a/app/src/test/java/org/randomcoder/udroid/install/RootfsPipelineTest.kt +++ b/app/src/test/java/org/randomcoder/udroid/install/RootfsPipelineTest.kt @@ -160,6 +160,7 @@ class RootfsPipelineTest { assertTrue(bashIntegration.contains("133;B")) assertTrue(bashIntegration.contains("133;C")) assertTrue(bashIntegration.contains("133;D")) + assertTrue(bashIntegration.contains("kitty-shell-cwd://localhost")) } @Test @@ -176,6 +177,8 @@ class RootfsPipelineTest { val zshIntegration = File(rootfs, "etc/udroid/shell-integration/udroid.zsh").readText() assertTrue(zshIntegration.contains("add-zsh-hook precmd")) assertTrue(zshIntegration.contains("add-zsh-hook preexec")) + assertTrue(zshIntegration.contains("add-zsh-hook chpwd")) + assertTrue(zshIntegration.contains("kitty-shell-cwd://localhost")) assertEquals( 1, File(rootfs, "etc/zsh/zshrc") @@ -186,6 +189,8 @@ class RootfsPipelineTest { assertTrue(fishIntegration.contains("fish_prompt")) assertTrue(fishIntegration.contains("fish_preexec")) assertTrue(fishIntegration.contains("fish_postexec")) + assertTrue(fishIntegration.contains("--on-variable PWD")) + assertTrue(fishIntegration.contains("file://localhost")) } private fun fixture(): Fixture { diff --git a/app/src/test/java/org/randomcoder/udroid/runtime/ProotTerminalLaunchTest.kt b/app/src/test/java/org/randomcoder/udroid/runtime/ProotTerminalLaunchTest.kt index 3af917c..36716a8 100644 --- a/app/src/test/java/org/randomcoder/udroid/runtime/ProotTerminalLaunchTest.kt +++ b/app/src/test/java/org/randomcoder/udroid/runtime/ProotTerminalLaunchTest.kt @@ -43,6 +43,81 @@ class ProotTerminalLaunchTest { ) } + @Test + fun `resolved guest working directory is passed to proot`() { + val arguments = + ProotTerminalLaunchBuilder.buildArguments( + linker = "linker64", + prootPath = "proot", + rootfsPath = "rootfs", + guestHome = "/root", + guestWorkingDirectory = "/root/project", + guestShell = "/bin/bash", + ) + + assertTrue("--cwd=/root/project" in arguments) + } + + @Test + fun `working directory resolves inside rootfs and bind mounts`() { + val rootfs = Files.createTempDirectory("udroid-cwd-rootfs").toFile() + val mounted = Files.createTempDirectory("udroid-cwd-mount").toFile() + try { + rootfs.resolve("root/project").mkdirs() + mounted.resolve("sources").mkdirs() + val mounts = listOf(ResolvedProotMount(mounted.path, "/mnt/shared", "test")) + + assertEquals( + "/root/project", + ProotTerminalLaunchBuilder.resolveGuestWorkingDirectory( + rootfs, + mounts, + "/root/project", + "/root", + ), + ) + assertEquals( + "/mnt/shared/sources", + ProotTerminalLaunchBuilder.resolveGuestWorkingDirectory( + rootfs, + mounts, + "/mnt/shared/sources", + "/root", + ), + ) + } finally { + rootfs.deleteRecursively() + mounted.deleteRecursively() + } + } + + @Test + fun `missing or unsafe working directory falls back home`() { + val rootfs = Files.createTempDirectory("udroid-cwd-fallback").toFile() + try { + assertEquals( + "/root", + ProotTerminalLaunchBuilder.resolveGuestWorkingDirectory( + rootfs, + emptyList(), + "/root/../etc", + "/root", + ), + ) + assertEquals( + "/root", + ProotTerminalLaunchBuilder.resolveGuestWorkingDirectory( + rootfs, + emptyList(), + "/missing", + "/root", + ), + ) + } finally { + rootfs.deleteRecursively() + } + } + @Test fun `all Android and guest bridge mounts are explicit`() { val arguments = diff --git a/app/src/test/java/org/randomcoder/udroid/runtime/TerminalTabRegistryTest.kt b/app/src/test/java/org/randomcoder/udroid/runtime/TerminalTabRegistryTest.kt index 42e4240..2a1efcc 100644 --- a/app/src/test/java/org/randomcoder/udroid/runtime/TerminalTabRegistryTest.kt +++ b/app/src/test/java/org/randomcoder/udroid/runtime/TerminalTabRegistryTest.kt @@ -142,6 +142,18 @@ class TerminalTabRegistryTest { assertEquals(false, registry.updateCommandRunning("one", false)?.commandRunning) } + @Test + fun `working directory is isolated per tab and ignores duplicate reports`() { + val registry = TerminalTabRegistry() + registry.add(tab("one")) + registry.add(tab("two")) + + assertEquals("one", registry.updateWorkingDirectory("one", "/root/project")?.id) + assertNull(registry.updateWorkingDirectory("one", "/root/project")) + assertEquals("/root/project", registry.get("one")?.workingDirectory) + assertNull(registry.get("two")?.workingDirectory) + } + @Test fun `rootfs names become readable distro titles`() { assertEquals("Ubuntu Jammy", terminalDistroTitle("udroid-jammy-raw")) diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 84bd6f8..fdde0e4 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -122,7 +122,7 @@ with the terminal outside the normal text grid. uDroid currently supports: | OSC 52 | Write only | Copies Base64-encoded text from a terminal program to the Android clipboard. | | OSC 104, 110, 111, 112 | Supported | Resets indexed colors, foreground, background, or cursor color. | | OSC 133 A/B/C/D | Bash, Zsh, and Fish | Tracks whether the foreground command is running so uDroid can warn before closing its tab. | -| OSC 7 | Not supported | The terminal does not track the shell's current directory. | +| OSC 7 | Bash, Zsh, and Fish | Tracks the guest working directory so a new tab for the same Linux system opens in the current directory. | | OSC 8 | Not supported | Hyperlinks are displayed as ordinary text. | | OSC 9, 777 | Not supported | Terminal programs cannot create Android notifications. | | OSC 9;4 | Not supported | Terminal progress is not shown in the Android UI. | @@ -130,9 +130,13 @@ with the terminal outside the normal text grid. uDroid currently supports: Supported OSC sequences may end with either BEL or ST. OSC 133 command tracking is automatically configured for interactive Bash, Zsh, and Fish -shells. A newly installed Zsh or Fish is detected when the next uDroid terminal -starts. Tracking cannot detect background jobs after the prompt returns, and a -prompt framework that replaces the shell's registered hooks may disable it. +shells. OSC 7 working-directory tracking is configured for the same shells and +accepts local `file://` and `kitty-shell-cwd://` URIs. Remote hosts, unsafe +paths, and directories that no longer exist fall back to the Linux home +directory. A newly installed Zsh or Fish is detected when the next uDroid +terminal starts. Tracking cannot detect background jobs after the prompt +returns, and a prompt framework that replaces the shell's registered hooks may +disable it. The exit status carried by OSC 133 D is accepted but is not shown or saved. OSC 52 lets command output replace the Android clipboard, so run untrusted diff --git a/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java b/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java index 5c46a1c..cb376b3 100644 --- a/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java +++ b/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java @@ -2,6 +2,8 @@ import android.util.Base64; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Locale; @@ -2072,6 +2074,9 @@ private void doOscSetTextParameters(String bellOrStringTerminator) { if (endOfInput) break; } break; + case 7: // Report the shell's current working directory. + handleWorkingDirectory(textParameter); + break; case 10: // Set foreground color. case 11: // Set background color. case 12: // Set cursor color. @@ -2308,6 +2313,40 @@ private void handleSemanticPrompt(String parameter) { } } + private void handleWorkingDirectory(String uri) { + final String filePrefix = "file://"; + final String kittyPrefix = "kitty-shell-cwd://"; + boolean fileUri = uri.startsWith(filePrefix); + String remainder; + if (fileUri) { + remainder = uri.substring(filePrefix.length()); + } else if (uri.startsWith(kittyPrefix)) { + remainder = uri.substring(kittyPrefix.length()); + } else { + return; + } + + int pathStart = remainder.indexOf('/'); + if (pathStart < 0) return; + String authority = remainder.substring(0, pathStart); + if (!authority.isEmpty() && !"localhost".equalsIgnoreCase(authority)) return; + + String encodedPath = remainder.substring(pathStart); + if (fileUri && (encodedPath.indexOf('?') >= 0 || encodedPath.indexOf('#') >= 0)) return; + try { + String path = URLDecoder.decode(encodedPath.replace("+", "%2B"), StandardCharsets.UTF_8.name()); + if (path.length() > 4096 || path.charAt(0) != '/') return; + for (int i = 0; i < path.length(); i++) { + if (Character.isISOControl(path.charAt(i))) return; + } + for (String segment : path.split("/", -1)) { + if (".".equals(segment) || "..".equals(segment)) return; + } + mSession.onWorkingDirectoryChanged(path); + } catch (IllegalArgumentException | UnsupportedEncodingException ignored) { + } + } + private void setCommandRunning(boolean running) { if (mCommandRunning == running) return; mCommandRunning = running; diff --git a/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalOutput.java b/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalOutput.java index 3d444f4..4a4b5aa 100644 --- a/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalOutput.java +++ b/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalOutput.java @@ -21,6 +21,9 @@ public final void write(String data) { /** Notify the terminal client that OSC 133 marked a command as running or finished. */ public abstract void onCommandStateChanged(boolean running); + /** Notify the terminal client that OSC 7 reported the shell's current directory. */ + public abstract void onWorkingDirectoryChanged(String path); + /** Notify the terminal client that text should be copied to clipboard. */ public abstract void onCopyTextToClipboard(String text); diff --git a/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalSession.java b/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalSession.java index 60d1240..5884a29 100644 --- a/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalSession.java +++ b/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalSession.java @@ -264,6 +264,11 @@ public void onCommandStateChanged(boolean running) { mClient.onCommandStateChanged(this, running); } + @Override + public void onWorkingDirectoryChanged(String path) { + mClient.onWorkingDirectoryChanged(this, path); + } + public synchronized boolean isRunning() { return mShellPid != -1; } diff --git a/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalSessionClient.java b/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalSessionClient.java index 8ee4df7..d155135 100644 --- a/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalSessionClient.java +++ b/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalSessionClient.java @@ -13,6 +13,8 @@ public interface TerminalSessionClient { void onCommandStateChanged(TerminalSession changedSession, boolean running); + void onWorkingDirectoryChanged(TerminalSession changedSession, String path); + void onSessionFinished(TerminalSession finishedSession); void onCopyTextToClipboard(TerminalSession session, String text); diff --git a/third_party/termux/terminal-emulator/src/test/java/com/termux/terminal/OperatingSystemControlTest.java b/third_party/termux/terminal-emulator/src/test/java/com/termux/terminal/OperatingSystemControlTest.java index 20564da..de6535d 100644 --- a/third_party/termux/terminal-emulator/src/test/java/com/termux/terminal/OperatingSystemControlTest.java +++ b/third_party/termux/terminal-emulator/src/test/java/com/termux/terminal/OperatingSystemControlTest.java @@ -27,6 +27,21 @@ public void testSemanticPromptCommandState() { assertEquals(2, mOutput.commandStateChanges.size()); } + public void testWorkingDirectory() { + withTerminalSized(10, 10); + + enterString("\033]7;file://localhost/root/hello%20world\007"); + enterString("\033]7;kitty-shell-cwd://localhost/home/用户 + notes\033\\"); + assertEquals(List.of("/root/hello world", "/home/用户 + notes"), mOutput.workingDirectoryChanges); + + enterString("\033]7;file://example.com/root\007"); + enterString("\033]7;file://localhost/root/../etc\007"); + enterString("\033]7;file://localhost/root/%1Bbad\007"); + enterString("\033]7;file://localhost/root/%invalid\007"); + enterString("\033]7;/root\007"); + assertEquals(2, mOutput.workingDirectoryChanges.size()); + } + public void testSetTitle() throws Exception { List expectedTitleChanges = new ArrayList<>(); diff --git a/third_party/termux/terminal-emulator/src/test/java/com/termux/terminal/TerminalTestCase.java b/third_party/termux/terminal-emulator/src/test/java/com/termux/terminal/TerminalTestCase.java index ac0f7d2..114aad3 100644 --- a/third_party/termux/terminal-emulator/src/test/java/com/termux/terminal/TerminalTestCase.java +++ b/third_party/termux/terminal-emulator/src/test/java/com/termux/terminal/TerminalTestCase.java @@ -21,6 +21,7 @@ public static class MockTerminalOutput extends TerminalOutput { public final List titleChanges = new ArrayList<>(); public final List clipboardPuts = new ArrayList<>(); public final List commandStateChanges = new ArrayList<>(); + public final List workingDirectoryChanges = new ArrayList<>(); public int bellsRung = 0; public int colorsChanged = 0; @@ -45,6 +46,11 @@ public void onCommandStateChanged(boolean running) { commandStateChanges.add(running); } + @Override + public void onWorkingDirectoryChanged(String path) { + workingDirectoryChanges.add(path); + } + @Override public void onCopyTextToClipboard(String text) { clipboardPuts.add(text); From 0821b1248d24fb27af9db4bd350b14e2652bcdd3 Mon Sep 17 00:00:00 2001 From: saicharankandukuri Date: Sun, 8 Nov 2026 16:37:41 +0530 Subject: [PATCH 11/14] feat(terminal): support OSC 8 hyperlinks Preserve bounded hyperlink metadata through terminal writes, copies, Unicode cells, scrollback, and reflow. Underline linked cells and require confirmation before opening supported web or mail links, while keeping a copy action for every URI. Adapted from termux/termux-app#5390 by Matt Schwartz. --- .../org/randomcoder/udroid/ui/TerminalPage.kt | 54 +++++++ .../randomcoder/udroid/ui/TerminalLinkTest.kt | 17 ++ .../com/termux/terminal/TerminalBuffer.java | 30 +++- .../com/termux/terminal/TerminalEmulator.java | 85 +++++++++- .../java/com/termux/terminal/TerminalRow.java | 39 ++++- .../com/termux/terminal/HyperlinkTest.java | 150 ++++++++++++++++++ .../com/termux/view/TerminalRenderer.java | 13 +- 7 files changed, 372 insertions(+), 16 deletions(-) create mode 100644 app/src/test/java/org/randomcoder/udroid/ui/TerminalLinkTest.kt create mode 100644 third_party/termux/terminal-emulator/src/test/java/com/termux/terminal/HyperlinkTest.java diff --git a/app/src/main/java/org/randomcoder/udroid/ui/TerminalPage.kt b/app/src/main/java/org/randomcoder/udroid/ui/TerminalPage.kt index 2bde184..6329b8d 100644 --- a/app/src/main/java/org/randomcoder/udroid/ui/TerminalPage.kt +++ b/app/src/main/java/org/randomcoder/udroid/ui/TerminalPage.kt @@ -1,11 +1,16 @@ package org.randomcoder.udroid.ui +import android.content.ClipData +import android.content.ClipboardManager import android.content.Context +import android.content.Intent +import android.net.Uri import android.os.Build import android.util.Log import android.view.KeyEvent import android.view.MotionEvent import android.view.inputmethod.InputMethodManager +import android.widget.Toast import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background @@ -893,6 +898,7 @@ private fun LiveTerminal( val maximumTextSizePx = with(density) { TERMINAL_TEXT_SIZE_MAX_SP.sp.toPx() } val currentTypography by rememberUpdatedState(typography) val currentOnTypographyChanged by rememberUpdatedState(onTypographyChanged) + var pendingLink by remember { mutableStateOf(null) } val client = remember(context, density) { UdroidTerminalViewClient( @@ -901,6 +907,7 @@ private fun LiveTerminal( initialTextSize = textSizePx, minimumTextSize = minimumTextSizePx, maximumTextSize = maximumTextSizePx, + onLinkTapped = { pendingLink = it }, onTextSizeChanged = { currentOnTypographyChanged( currentTypography.copy( @@ -956,8 +963,49 @@ private fun LiveTerminal( write = service::writeToTerminal, ) } + pendingLink?.let { uri -> + AlertDialog( + onDismissRequest = { pendingLink = null }, + title = { Text("Open link?") }, + text = { Text(uri) }, + confirmButton = { + TextButton( + onClick = { + pendingLink = null + if (!openTerminalLink(context, uri)) { + Toast.makeText(context, "This link type cannot be opened", Toast.LENGTH_SHORT).show() + } + }, + ) { Text("Open") } + }, + dismissButton = { + TextButton( + onClick = { + context.getSystemService(ClipboardManager::class.java) + .setPrimaryClip(ClipData.newPlainText("Terminal link", uri)) + pendingLink = null + Toast.makeText(context, "Link copied", Toast.LENGTH_SHORT).show() + }, + ) { Text("Copy") } + }, + ) + } } +internal fun openTerminalLink( + context: Context, + value: String, +): Boolean { + val uri = Uri.parse(value) + if (!terminalLinkCanOpen(uri.scheme)) return false + return runCatching { + context.startActivity(Intent(Intent.ACTION_VIEW, uri)) + }.isSuccess +} + +internal fun terminalLinkCanOpen(scheme: String?): Boolean = + scheme.equals("http", true) || scheme.equals("https", true) || scheme.equals("mailto", true) + @Composable private fun ExtraKeysRow( modifiers: TerminalModifierState, @@ -1039,6 +1087,7 @@ private class UdroidTerminalViewClient( initialTextSize: Int, private val minimumTextSize: Float, private val maximumTextSize: Float, + private val onLinkTapped: (String) -> Unit, private val onTextSizeChanged: (Int) -> Unit, ) : TerminalViewClient { private var terminalView: TerminalView? = null @@ -1079,6 +1128,11 @@ private class UdroidTerminalViewClient( override fun onSingleTapUp(event: MotionEvent) { terminalView?.let { view -> + val (column, row) = view.getColumnAndRow(event, true) + view.currentSession?.emulator?.getHyperlinkUriAt(row, column)?.let { + onLinkTapped(it) + return + } view.requestFocus() context.getSystemService(InputMethodManager::class.java) .showSoftInput(view, InputMethodManager.SHOW_IMPLICIT) diff --git a/app/src/test/java/org/randomcoder/udroid/ui/TerminalLinkTest.kt b/app/src/test/java/org/randomcoder/udroid/ui/TerminalLinkTest.kt new file mode 100644 index 0000000..a8462af --- /dev/null +++ b/app/src/test/java/org/randomcoder/udroid/ui/TerminalLinkTest.kt @@ -0,0 +1,17 @@ +package org.randomcoder.udroid.ui + +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class TerminalLinkTest { + @Test + fun onlyWebAndMailLinksCanLeaveTheTerminal() { + assertTrue(terminalLinkCanOpen("https")) + assertTrue(terminalLinkCanOpen("HTTP")) + assertTrue(terminalLinkCanOpen("mailto")) + assertFalse(terminalLinkCanOpen("intent")) + assertFalse(terminalLinkCanOpen("javascript")) + assertFalse(terminalLinkCanOpen(null)) + } +} diff --git a/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalBuffer.java b/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalBuffer.java index 21d6518..54fc8c9 100644 --- a/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalBuffer.java +++ b/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalBuffer.java @@ -290,21 +290,32 @@ public void resize(int newColumns, int newRows, int newTotalRows, int[] cursor, lastNonSpaceIndex = oldLine.getSpaceUsed(); if (cursorAtThisRow) justToCursor = true; } else { - for (int i = 0; i < oldLine.getSpaceUsed(); i++) - // NEWLY INTRODUCED BUG! Should not index oldLine.mStyle with char indices - if (oldLine.mText[i] != ' '/* || oldLine.mStyle[i] != currentStyle */) - lastNonSpaceIndex = i + 1; + int column = 0; + for (int i = 0; i < oldLine.getSpaceUsed();) { + int codePoint = Character.codePointAt(oldLine.mText, i, oldLine.getSpaceUsed()); + i += Character.charCount(codePoint); + // Hyperlinks occupy cells even when their labels are spaces. Track + // terminal columns separately from UTF-16 indices and combining marks. + if (codePoint != ' ' || oldLine.getHyperlink(column) != 0) + lastNonSpaceIndex = i; + column += Math.max(0, WcWidth.width(codePoint)); + } } int currentOldCol = 0; long styleAtCol = 0; + int linkAtCol = 0; for (int i = 0; i < lastNonSpaceIndex; i++) { // Note that looping over java character, not cells. char c = oldLine.mText[i]; int codePoint = (Character.isHighSurrogate(c)) ? Character.toCodePoint(c, oldLine.mText[++i]) : c; int displayWidth = WcWidth.width(codePoint); // Use the last style if this is a zero-width character: - if (displayWidth > 0) styleAtCol = oldLine.getStyle(currentOldCol); + if (displayWidth > 0) { + styleAtCol = oldLine.getStyle(currentOldCol); + // An OSC 8 hyperlink reflows with its cell, as the style does. + linkAtCol = oldLine.getHyperlink(currentOldCol); + } // Line wrap as necessary: if (currentOutputExternalColumn + displayWidth > mColumns) { @@ -320,7 +331,7 @@ public void resize(int newColumns, int newRows, int newTotalRows, int[] cursor, int offsetDueToCombiningChar = ((displayWidth <= 0 && currentOutputExternalColumn > 0) ? 1 : 0); int outputColumn = currentOutputExternalColumn - offsetDueToCombiningChar; - setChar(outputColumn, currentOutputExternalRow, codePoint, styleAtCol); + setChar(outputColumn, currentOutputExternalRow, codePoint, styleAtCol, linkAtCol); if (displayWidth > 0) { if (oldCursorRow == externalOldRow && oldCursorColumn == currentOldCol) { @@ -449,10 +460,15 @@ public TerminalRow allocateFullLineIfNecessary(int row) { } public void setChar(int column, int row, int codePoint, long style) { + setChar(column, row, codePoint, style, 0); + } + + /** as {@link #setChar(int, int, int, long)}, printed under the OSC 8 hyperlink {@code link} (0 for none). */ + public void setChar(int column, int row, int codePoint, long style, int link) { if (row < 0 || row >= mScreenRows || column < 0 || column >= mColumns) throw new IllegalArgumentException("TerminalBuffer.setChar(): row=" + row + ", column=" + column + ", mScreenRows=" + mScreenRows + ", mColumns=" + mColumns); row = externalToInternalRow(row); - allocateFullLineIfNecessary(row).setChar(column, codePoint, style); + allocateFullLineIfNecessary(row).setChar(column, codePoint, style, link); } public long getStyleAt(int externalRow, int column) { diff --git a/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java b/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java index cb376b3..4650976 100644 --- a/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java +++ b/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java @@ -6,6 +6,7 @@ import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.HashMap; import java.util.Locale; import java.util.Objects; import java.util.Stack; @@ -193,6 +194,26 @@ public final class TerminalEmulator { /** Whether OSC 133 has marked a foreground command as running. */ private boolean mCommandRunning; + // Adapted from termux/termux-app#5390 by Matt Schwartz. + static final int MAX_HYPERLINKS = 1024; + private final String[] mHyperlinkUris = new String[MAX_HYPERLINKS + 1]; + private final String[] mHyperlinkKeys = new String[MAX_HYPERLINKS + 1]; + private final HashMap mHyperlinkIds = new HashMap<>(); + private int mHyperlink; + private int mNextHyperlink = 1; + private int mHyperlinkOpensSinceSweep; + + /** Resolve the OSC 8 URI stored on a terminal cell, or null. */ + public String getHyperlinkUriAt(int row, int column) { + if (row < -mScreen.getActiveTranscriptRows() || row >= mRows || column < 0 || column >= mColumns) return null; + TerminalRow line = mScreen.mLines[mScreen.externalToInternalRow(row)]; + return line == null ? null : getHyperlinkUri(line.getHyperlink(column)); + } + + public String getHyperlinkUri(int id) { + return id > 0 && id <= MAX_HYPERLINKS ? mHyperlinkUris[id] : null; + } + /** * True if the current escape sequence should continue, false if the current escape sequence should be terminated. * Used when parsing a single character. @@ -2077,6 +2098,9 @@ private void doOscSetTextParameters(String bellOrStringTerminator) { case 7: // Report the shell's current working directory. handleWorkingDirectory(textParameter); break; + case 8: // Hyperlink: 8;params;URI opens, 8;; closes. + setHyperlink(textParameter); + break; case 10: // Set foreground color. case 11: // Set background color. case 12: // Set cursor color. @@ -2163,6 +2187,64 @@ private void doOscSetTextParameters(String bellOrStringTerminator) { finishSequence(); } + private void setHyperlink(String parameters) { + int split = parameters.indexOf(';'); + String uri = split < 0 ? "" : parameters.substring(split + 1); + if (uri.isEmpty()) { + mHyperlink = 0; + return; + } + String explicitId = null; + for (String parameter : parameters.substring(0, split).split(":", -1)) { + if (parameter.startsWith("id=") && parameter.length() > 3) explicitId = parameter.substring(3); + } + String key = explicitId == null ? null : explicitId + '\u0000' + uri; + Integer existing = key == null ? null : mHyperlinkIds.get(key); + mHyperlink = existing != null ? existing : allocateHyperlink(key, uri); + } + + private int allocateHyperlink(String key, String uri) { + mHyperlinkOpensSinceSweep++; + int id = freeHyperlink(); + if (id == 0 && mHyperlinkOpensSinceSweep >= MAX_HYPERLINKS / 4) { + mHyperlinkOpensSinceSweep = 0; + sweepHyperlinks(); + id = freeHyperlink(); + } + if (id == 0) return 0; + mHyperlinkUris[id] = uri; + mHyperlinkKeys[id] = key; + if (key != null) mHyperlinkIds.put(key, id); + mNextHyperlink = id % MAX_HYPERLINKS + 1; + return id; + } + + private int freeHyperlink() { + for (int step = 0; step < MAX_HYPERLINKS; step++) { + int id = (mNextHyperlink - 1 + step) % MAX_HYPERLINKS + 1; + if (mHyperlinkUris[id] == null) return id; + } + return 0; + } + + private void sweepHyperlinks() { + boolean[] live = new boolean[MAX_HYPERLINKS + 1]; + live[mHyperlink] = true; + for (TerminalBuffer buffer : new TerminalBuffer[]{mMainBuffer, mAltBuffer}) { + for (TerminalRow row : buffer.mLines) { + if (row != null && row.mLinks != null) { + for (int link : row.mLinks) live[link] = true; + } + } + } + for (int id = 1; id <= MAX_HYPERLINKS; id++) { + if (live[id] || mHyperlinkUris[id] == null) continue; + if (mHyperlinkKeys[id] != null) mHyperlinkIds.remove(mHyperlinkKeys[id]); + mHyperlinkUris[id] = null; + mHyperlinkKeys[id] = null; + } + } + private void blockClear(int sx, int sy, int w) { blockClear(sx, sy, w, 1); } @@ -2549,7 +2631,7 @@ private void emitCodePoint(int codePoint) { // so was mCursorCol changed after the offsetDueToCombiningChar conditional by another thread? // TODO: Check if there are thread synchronization issues with mCursorCol and mCursorRow, possibly causing others bugs too. if (column < 0) column = 0; - mScreen.setChar(column, mCursorRow, codePoint, getStyle()); + mScreen.setChar(column, mCursorRow, codePoint, getStyle(), mHyperlink); if (autoWrap && displayWidth > 0) mAboutToAutoWrap = (mCursorCol == mRightMargin - displayWidth); @@ -2589,6 +2671,7 @@ public void clearScrollCounter() { /** Reset terminal state so user can interact with it regardless of present state. */ public void reset() { + mHyperlink = 0; setCommandRunning(false); setCursorStyle(); mArgIndex = 0; diff --git a/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalRow.java b/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalRow.java index d68dc32..46d9fe0 100644 --- a/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalRow.java +++ b/third_party/termux/terminal-emulator/src/main/java/com/termux/terminal/TerminalRow.java @@ -47,6 +47,8 @@ public final class TerminalRow { boolean mLineWrap; /** The style bits of each cell in the row. See {@link TextStyle}. */ final long[] mStyle; + /** OSC 8 hyperlink id for each cell, or null while this row has no links. */ + int[] mLinks; /** If this row might contain chars with width != 1, used for deactivating fast path */ boolean mHasNonOneWidthOrSurrogateChars; @@ -65,6 +67,7 @@ public void copyInterval(TerminalRow line, int sourceX1, int sourceX2, int desti final int x2 = line.findStartOfColumn(sourceX2); boolean startingFromSecondHalfOfWideChar = (sourceX1 > 0 && line.wideDisplayCharacterStartingAt(sourceX1 - 1)); final char[] sourceChars = (this == line) ? Arrays.copyOf(line.mText, line.mText.length) : line.mText; + final int[] sourceLinks = (this == line && line.mLinks != null) ? line.mLinks.clone() : line.mLinks; int latestNonCombiningWidth = 0; for (int i = x1; i < x2; i++) { char sourceChar = sourceChars[i]; @@ -80,7 +83,7 @@ public void copyInterval(TerminalRow line, int sourceX1, int sourceX2, int desti sourceX1 += latestNonCombiningWidth; latestNonCombiningWidth = w; } - setChar(destinationX, codePoint, line.getStyle(sourceX1)); + setChar(destinationX, codePoint, line.getStyle(sourceX1), sourceLinks == null ? 0 : sourceLinks[sourceX1]); } } @@ -146,16 +149,22 @@ public void clear(long style) { Arrays.fill(mStyle, style); mSpaceUsed = (short) mColumns; mHasNonOneWidthOrSurrogateChars = false; + mLinks = null; } // https://github.com/steven676/Android-Terminal-Emulator/commit/9a47042620bec87617f0b4f5d50568535668fe26 public void setChar(int columnToSet, int codePoint, long style) { + setChar(columnToSet, codePoint, style, 0); + } + + /** Write a cell carrying an OSC 8 hyperlink id (0 for none). */ + public void setChar(int columnToSet, int codePoint, long style, int link) { if (columnToSet < 0 || columnToSet >= mStyle.length) throw new IllegalArgumentException("TerminalRow.setChar(): columnToSet=" + columnToSet + ", codePoint=" + codePoint + ", style=" + style); - mStyle[columnToSet] = style; - final int newCodePointDisplayWidth = WcWidth.width(codePoint); + if (newCodePointDisplayWidth > 0) setHyperlink(columnToSet, link); + mStyle[columnToSet] = style; // Fast path when we don't have any chars with width != 1 if (!mHasNonOneWidthOrSurrogateChars) { @@ -174,6 +183,7 @@ public void setChar(int columnToSet, int codePoint, long style) { if (newIsCombining) { // When standing at second half of wide character and inserting combining: if (wasExtraColForWideChar) columnToSet--; + link = getHyperlink(columnToSet); } else { // Check if we are overwriting the second half of a wide character starting at the previous column: if (wasExtraColForWideChar) setChar(columnToSet - 1, ' ', style); @@ -268,11 +278,34 @@ public void setChar(int columnToSet, int codePoint, long style) { mSpaceUsed -= nextLen; } } + + setHyperlink(columnToSet, link); + if (columnToSet + 1 < mColumns) { + if (newCodePointDisplayWidth == 2 || (newIsCombining && oldCodePointDisplayWidth == 2)) + setHyperlink(columnToSet + 1, link); + else if (oldCodePointDisplayWidth == 2 && newCodePointDisplayWidth == 1) + setHyperlink(columnToSet + 1, 0); + } + } + + private void setHyperlink(int column, int link) { + if (mLinks == null) { + if (link == 0) return; + mLinks = new int[mColumns]; + } + mLinks[column] = link; + } + + public int getHyperlink(int column) { + return mLinks == null ? 0 : mLinks[column]; } boolean isBlank() { for (int charIndex = 0, charLen = getSpaceUsed(); charIndex < charLen; charIndex++) if (mText[charIndex] != ' ') return false; + if (mLinks != null) + for (int link : mLinks) + if (link != 0) return false; return true; } diff --git a/third_party/termux/terminal-emulator/src/test/java/com/termux/terminal/HyperlinkTest.java b/third_party/termux/terminal-emulator/src/test/java/com/termux/terminal/HyperlinkTest.java new file mode 100644 index 0000000..2f7ec81 --- /dev/null +++ b/third_party/termux/terminal-emulator/src/test/java/com/termux/terminal/HyperlinkTest.java @@ -0,0 +1,150 @@ +package com.termux.terminal; + +/** OSC 8 metadata follows terminal cells without opening or validating URLs. */ +public class HyperlinkTest extends TerminalTestCase { + private static final String CLOSE = "\033]8;;\033\\"; + + private String open(String params, String uri) { + return "\033]8;" + params + ";" + uri + "\033\\"; + } + + private int link(int row, int column) { + TerminalBuffer screen = mTerminal.getScreen(); + return screen.mLines[screen.externalToInternalRow(row)].getHyperlink(column); + } + + private String uri(int row, int column) { + return mTerminal.getHyperlinkUri(link(row, column)); + } + + public void testOpenCloseAndTerminators() { + withTerminalSized(20, 3); + enterString(open("", "https://example.com/a;b") + "label" + CLOSE + "plain"); + assertEquals("https://example.com/a;b", uri(0, 0)); + assertEquals(link(0, 0), link(0, 4)); + assertNull(uri(0, 5)); + enterString("\033]8;;https://example.org\007X\033]8;;\007Y"); + assertEquals("https://example.org", uri(0, 10)); + assertNull(uri(0, 11)); + assertNull(mTerminal.getHyperlinkUri(-1)); + assertNull(mTerminal.getHyperlinkUri(TerminalEmulator.MAX_HYPERLINKS + 1)); + } + + public void testExplicitIdsAndSeparateImplicitLinks() { + withTerminalSized(10, 3); + enterString(open("id=part", "https://example.com") + "A" + CLOSE); + enterString(open("ignored=value:id=part", "https://example.com") + "B" + CLOSE); + assertEquals(link(0, 0), link(0, 1)); + enterString(open("id=part", "https://example.org") + "C" + CLOSE); + assertTrue(link(0, 0) != link(0, 2)); + enterString(open("", "https://example.com") + "D" + CLOSE); + enterString(open("", "https://example.com") + "E" + CLOSE); + assertTrue(link(0, 3) != link(0, 4)); + } + + public void testWideCharactersOverwriteAndCombining() { + withTerminalSized(10, 3); + enterString(open("", "https://example.com") + "\u754c\u0301" + CLOSE); + assertEquals(uri(0, 0), uri(0, 1)); + assertEquals("https://example.com", uri(0, 0)); + enterString("\033[1;2HX"); + assertNull(uri(0, 0)); + assertNull(uri(0, 1)); + enterString("\033[1;1H" + open("", "https://example.org") + "\u754c" + CLOSE); + enterString("\033[1;1HY"); + assertNull(uri(0, 0)); + assertNull(uri(0, 1)); + } + + public void testCombiningMarksKeepTheBaseCharactersLink() { + withTerminalSized(6, 3); + enterString(open("", "https://base.example") + "\u754c" + CLOSE + "\u0301"); + assertEquals("https://base.example", uri(0, 0)); + assertEquals(uri(0, 0), uri(0, 1)); + enterString(open("", "https://other.example") + "\u0302" + CLOSE + "x"); + assertEquals("https://base.example", uri(0, 0)); + assertEquals(uri(0, 0), uri(0, 1)); + assertNull(uri(0, 2)); + resize(3, 4); + assertEquals("https://base.example", uri(0, 0)); + assertEquals(uri(0, 0), uri(0, 1)); + } + + public void testLinkedSpacesSurviveReflowAfterUnicodeText() { + String[] prefixes = {"", "x", "\ud83d\ude00\u0301"}; + int[] columns = {0, 1, 2}; + for (int i = 0; i < prefixes.length; i++) { + withTerminalSized(8, 3); + enterString(prefixes[i] + open("", "https://spaces.example") + " " + CLOSE + "\r\nX"); + resize(6, 3); + for (int col = columns[i]; col < columns[i] + 3; col++) { + assertEquals("https://spaces.example", uri(0, col)); + } + assertNull(uri(0, columns[i] + 3)); + } + } + + public void testCopyWithinRowKeepsOriginalLinks() { + TerminalRow row = new TerminalRow(6, TextStyle.NORMAL); + for (int col = 0; col < 6; col++) row.setChar(col, 'a' + col, TextStyle.NORMAL, col + 1); + row.copyInterval(row, 0, 4, 2); + assertEquals(1, row.getHyperlink(2)); + assertEquals(2, row.getHyperlink(3)); + assertEquals(3, row.getHyperlink(4)); + assertEquals(4, row.getHyperlink(5)); + row.copyInterval(row, 2, 6, 0); + for (int col = 0; col < 4; col++) assertEquals(col + 1, row.getHyperlink(col)); + row.clear(TextStyle.NORMAL); + for (int col = 0; col < 6; col++) assertEquals(0, row.getHyperlink(col)); + } + + public void testLinksSurviveReflowAndHistory() { + withTerminalSized(6, 3); + enterString(open("", "https://example.com") + "abcdef" + CLOSE); + resize(3, 4); + assertEquals("https://example.com", uri(0, 0)); + assertEquals("https://example.com", uri(1, 2)); + enterString("\r\n1\r\n2\r\n3\r\n4"); + assertEquals("https://example.com", uri(-2, 0)); + assertEquals("https://example.com", uri(-1, 2)); + } + + public void testEraseAndResetCloseLinks() { + withTerminalSized(10, 3); + enterString(open("", "https://example.com") + "abc" + CLOSE); + enterString("\033[2K"); + for (int col = 0; col < 10; col++) assertNull(uri(0, col)); + enterString(open("", "https://example.org")); + mTerminal.reset(); + enterString("X"); + assertNull(uri(mTerminal.getCursorRow(), mTerminal.getCursorCol() - 1)); + } + + public void testFullTableDoesNotRetargetLiveCells() { + withTerminalSized(TerminalEmulator.MAX_HYPERLINKS + 2, 2); + for (int i = 0; i < TerminalEmulator.MAX_HYPERLINKS; i++) { + enterString(open("", "https://example.com/" + i) + "x" + CLOSE); + } + enterString(open("", "https://overflow.example") + "z" + CLOSE); + assertNull(uri(0, TerminalEmulator.MAX_HYPERLINKS)); + assertEquals("https://example.com/0", uri(0, 0)); + assertEquals("https://example.com/1023", uri(0, 1023)); + enterString("\033[1;1H "); + for (int i = 0; i < TerminalEmulator.MAX_HYPERLINKS / 4; i++) { + enterString(open("id=replacement", "https://replacement.example") + CLOSE); + } + enterString("\033[1;1H" + open("id=replacement", "https://replacement.example") + "r" + CLOSE); + assertEquals("https://replacement.example", uri(0, 0)); + assertEquals("https://example.com/1", uri(0, 1)); + assertEquals("https://example.com/1023", uri(0, 1023)); + } + + public void testAlternateBufferDoesNotReplaceMainBufferLinks() { + withTerminalSized(10, 3); + enterString(open("id=main", "https://main.example") + "M" + CLOSE); + enterString("\033[?1049h\033[H" + open("id=alt", "https://alt.example") + "A" + CLOSE); + assertEquals("https://alt.example", uri(0, 0)); + enterString("\033[?1049l"); + assertEquals("https://main.example", uri(0, 0)); + } +} diff --git a/third_party/termux/terminal-view/src/main/java/com/termux/view/TerminalRenderer.java b/third_party/termux/terminal-view/src/main/java/com/termux/view/TerminalRenderer.java index 5012a18..35578f8 100644 --- a/third_party/termux/terminal-view/src/main/java/com/termux/view/TerminalRenderer.java +++ b/third_party/termux/terminal-view/src/main/java/com/termux/view/TerminalRenderer.java @@ -93,6 +93,7 @@ public final void render(TerminalEmulator mEmulator, Canvas canvas, int topRow, long lastRunStyle = 0; boolean lastRunInsideCursor = false; boolean lastRunInsideSelection = false; + boolean lastRunHyperlink = false; int lastRunStartColumn = -1; int lastRunStartIndex = 0; boolean lastRunFontWidthMismatch = false; @@ -108,6 +109,7 @@ public final void render(TerminalEmulator mEmulator, Canvas canvas, int topRow, final boolean insideCursor = (cursorX == column || (codePointWcWidth == 2 && cursorX == column + 1)); final boolean insideSelection = column >= selx1 && column <= selx2; final long style = lineObject.getStyle(column); + final boolean hyperlink = lineObject.getHyperlink(column) != 0; // 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 @@ -117,7 +119,7 @@ public final void render(TerminalEmulator mEmulator, Canvas canvas, int topRow, currentCharIndex, charsForCodePoint); final boolean fontWidthMismatch = Math.abs(measuredCodePointWidth / mFontWidth - codePointWcWidth) > 0.01; - if (style != lastRunStyle || insideCursor != lastRunInsideCursor || insideSelection != lastRunInsideSelection || fontWidthMismatch || lastRunFontWidthMismatch) { + if (style != lastRunStyle || insideCursor != lastRunInsideCursor || insideSelection != lastRunInsideSelection || hyperlink != lastRunHyperlink || fontWidthMismatch || lastRunFontWidthMismatch) { if (column == 0) { // Skip first column as there is nothing to draw, just record the current style. } else { @@ -130,12 +132,13 @@ public final void render(TerminalEmulator mEmulator, Canvas canvas, int topRow, } drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun, lastRunStartIndex, charsSinceLastRun, measuredWidthForRun, - cursorColor, cursorShape, lastRunStyle, reverseVideo || invertCursorTextColor || lastRunInsideSelection); + cursorColor, cursorShape, lastRunStyle, reverseVideo || invertCursorTextColor || lastRunInsideSelection, lastRunHyperlink); } measuredWidthForRun = 0.f; lastRunStyle = style; lastRunInsideCursor = insideCursor; lastRunInsideSelection = insideSelection; + lastRunHyperlink = hyperlink; lastRunStartColumn = column; lastRunStartIndex = currentCharIndex; lastRunFontWidthMismatch = fontWidthMismatch; @@ -158,18 +161,18 @@ public final void render(TerminalEmulator mEmulator, Canvas canvas, int topRow, invertCursorTextColor = true; } drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun, lastRunStartIndex, charsSinceLastRun, - measuredWidthForRun, cursorColor, cursorShape, lastRunStyle, reverseVideo || invertCursorTextColor || lastRunInsideSelection); + measuredWidthForRun, cursorColor, cursorShape, lastRunStyle, reverseVideo || invertCursorTextColor || lastRunInsideSelection, lastRunHyperlink); } } private void drawTextRun(Canvas canvas, char[] text, int[] palette, float y, int startColumn, int runWidthColumns, int startCharIndex, int runWidthChars, float mes, int cursor, int cursorStyle, - long textStyle, boolean reverseVideo) { + long textStyle, boolean reverseVideo, boolean hyperlink) { int foreColor = TextStyle.decodeForeColor(textStyle); final int effect = TextStyle.decodeEffect(textStyle); int backColor = TextStyle.decodeBackColor(textStyle); final boolean bold = (effect & (TextStyle.CHARACTER_ATTRIBUTE_BOLD | TextStyle.CHARACTER_ATTRIBUTE_BLINK)) != 0; - final boolean underline = (effect & TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE) != 0; + final boolean underline = hyperlink || (effect & TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE) != 0; final boolean italic = (effect & TextStyle.CHARACTER_ATTRIBUTE_ITALIC) != 0; final boolean strikeThrough = (effect & TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH) != 0; final boolean dim = (effect & TextStyle.CHARACTER_ATTRIBUTE_DIM) != 0; From 6f6868eae5f1f9cce4dd91cf67e8f9f4da263301 Mon Sep 17 00:00:00 2001 From: saicharankandukuri Date: Sun, 8 Nov 2026 16:37:41 +0530 Subject: [PATCH 12/14] feat(files): expose Linux systems to Android picker Register a read-only DocumentsProvider for installed root filesystems. Resolve Linux absolute and relative symlinks within the guest root, reject traversal, and label each source with its distro icon and a uDroid-specific description. --- app/src/main/AndroidManifest.xml | 11 + .../filesystem/RootfsDocumentsProvider.kt | 216 ++++++++++++++++++ .../filesystem/RootfsDocumentsProviderTest.kt | 39 ++++ 3 files changed, 266 insertions(+) create mode 100644 app/src/main/java/org/randomcoder/udroid/filesystem/RootfsDocumentsProvider.kt create mode 100644 app/src/test/java/org/randomcoder/udroid/filesystem/RootfsDocumentsProviderTest.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index af95702..83b2244 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -92,6 +92,17 @@ android:resource="@xml/update_file_paths" /> + + + + + + ?): Cursor { + val cursor = MatrixCursor(projection ?: ROOT_COLUMNS) + registry.all().forEach { rootfs -> + cursor.newRow() + .add(DocumentsContract.Root.COLUMN_ROOT_ID, rootfs.name) + .add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, RootfsDocumentId.encode(rootfs.name, "")) + .add(DocumentsContract.Root.COLUMN_TITLE, terminalDistroTitle(rootfs.name)) + .add(DocumentsContract.Root.COLUMN_SUMMARY, "uDroid Linux file system") + .add(DocumentsContract.Root.COLUMN_ICON, rootfsIconResource(rootfs.name)) + .add(DocumentsContract.Root.COLUMN_FLAGS, DocumentsContract.Root.FLAG_LOCAL_ONLY or DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD) + .add(DocumentsContract.Root.COLUMN_MIME_TYPES, "*/*") + } + return cursor + } + + override fun queryDocument( + documentId: String, + projection: Array?, + ): Cursor = MatrixCursor(projection ?: DOCUMENT_COLUMNS).also { includeDocument(it, resolve(documentId)) } + + override fun queryChildDocuments( + parentDocumentId: String, + projection: Array?, + sortOrder: String?, + ): Cursor { + val parent = resolve(parentDocumentId) + if (!Files.isDirectory(parent.path)) throw FileNotFoundException("Not a directory") + val cursor = MatrixCursor(projection ?: DOCUMENT_COLUMNS) + Files.newDirectoryStream(parent.path).use { children -> + children + .sortedBy { it.fileName.toString().lowercase() } + .forEach { child -> + val relative = listOf(parent.relative, child.fileName.toString()).filter(String::isNotEmpty).joinToString("/") + runCatching { resolve(RootfsDocumentId.encode(parent.rootfs.name, relative)) } + .getOrNull() + ?.takeIf { Files.isDirectory(it.path) || Files.isRegularFile(it.path) } + ?.let { includeDocument(cursor, it) } + } + } + return cursor + } + + override fun openDocument( + documentId: String, + mode: String, + signal: CancellationSignal?, + ): ParcelFileDescriptor { + if (mode != "r") throw FileNotFoundException("Linux filesystems are read-only in Android Files") + val document = resolve(documentId) + if (!Files.isRegularFile(document.path)) throw FileNotFoundException("Not a regular file") + return ParcelFileDescriptor.open(document.path.toFile(), ParcelFileDescriptor.MODE_READ_ONLY) + } + + override fun isChildDocument( + parentDocumentId: String, + documentId: String, + ): Boolean { + val parent = runCatching { resolve(parentDocumentId) }.getOrNull() ?: return false + val child = runCatching { resolve(documentId) }.getOrNull() ?: return false + return parent.rootfs.name == child.rootfs.name && + (parent.relative.isEmpty() || child.relative.startsWith(parent.relative + "/")) && + child.relative != parent.relative + } + + private fun includeDocument( + cursor: MatrixCursor, + document: ResolvedDocument, + ) { + val attributes = Files.readAttributes(document.path, BasicFileAttributes::class.java) + val directory = attributes.isDirectory + cursor.newRow() + .add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, RootfsDocumentId.encode(document.rootfs.name, document.relative)) + .add( + DocumentsContract.Document.COLUMN_DISPLAY_NAME, + if (document.relative.isEmpty()) terminalDistroTitle(document.rootfs.name) else document.relative.substringAfterLast('/'), + ) + .add(DocumentsContract.Document.COLUMN_MIME_TYPE, if (directory) DocumentsContract.Document.MIME_TYPE_DIR else mimeType(document.path)) + .add(DocumentsContract.Document.COLUMN_FLAGS, 0) + .add(DocumentsContract.Document.COLUMN_SIZE, if (directory) null else attributes.size()) + .add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, attributes.lastModifiedTime().toMillis()) + } + + private fun resolve(documentId: String): ResolvedDocument { + val id = runCatching { RootfsDocumentId.decode(documentId) } + .getOrElse { throw FileNotFoundException("Invalid document id") } + val rootfs = registry.all().firstOrNull { it.name == id.rootfs } + ?: throw FileNotFoundException("Linux system is not installed") + val path = runCatching { RootfsPathResolver.resolve(rootfs.directory.toPath(), id.relative) } + .getOrElse { throw FileNotFoundException("File is outside the Linux filesystem") } + if (!Files.exists(path)) throw FileNotFoundException("File does not exist") + return ResolvedDocument(rootfs, id.relative, path) + } + + private fun mimeType(path: Path): String = + URLConnection.guessContentTypeFromName(path.fileName.toString()) ?: "application/octet-stream" + + private data class ResolvedDocument( + val rootfs: InstalledRootfs, + val relative: String, + val path: Path, + ) + + private companion object { + val ROOT_COLUMNS = arrayOf( + DocumentsContract.Root.COLUMN_ROOT_ID, + DocumentsContract.Root.COLUMN_DOCUMENT_ID, + DocumentsContract.Root.COLUMN_TITLE, + DocumentsContract.Root.COLUMN_SUMMARY, + DocumentsContract.Root.COLUMN_ICON, + DocumentsContract.Root.COLUMN_FLAGS, + DocumentsContract.Root.COLUMN_MIME_TYPES, + ) + val DOCUMENT_COLUMNS = arrayOf( + DocumentsContract.Document.COLUMN_DOCUMENT_ID, + DocumentsContract.Document.COLUMN_DISPLAY_NAME, + DocumentsContract.Document.COLUMN_MIME_TYPE, + DocumentsContract.Document.COLUMN_FLAGS, + DocumentsContract.Document.COLUMN_SIZE, + DocumentsContract.Document.COLUMN_LAST_MODIFIED, + ) + } +} + +internal fun rootfsIconResource(name: String): Int { + val normalized = name.lowercase() + return when { + listOf("focal", "jammy", "noble", "oracular", "plucky", "questing", "resolute", "ubuntu") + .any(normalized::contains) -> R.drawable.distro_ubuntu + "debian" in normalized || "trixie" in normalized || "sid" in normalized -> R.drawable.distro_debian + "arch" in normalized -> R.drawable.distro_arch + "alpine" in normalized -> R.drawable.distro_alpine + "void" in normalized -> R.drawable.distro_void + else -> R.drawable.distro_unknown + } +} + +internal data class RootfsDocumentId( + val rootfs: String, + val relative: String, +) { + companion object { + private val encoder = Base64.getUrlEncoder().withoutPadding() + private val decoder = Base64.getUrlDecoder() + + fun encode(rootfs: String, relative: String): String = + "${encoder.encodeToString(rootfs.toByteArray(StandardCharsets.UTF_8))}:${encoder.encodeToString(relative.toByteArray(StandardCharsets.UTF_8))}" + + fun decode(value: String): RootfsDocumentId { + val separator = value.indexOf(':') + require(separator > 0) + val rootfs = String(decoder.decode(value.substring(0, separator)), StandardCharsets.UTF_8) + val relative = String(decoder.decode(value.substring(separator + 1)), StandardCharsets.UTF_8) + require(rootfs.isNotBlank() && '\u0000' !in rootfs) + require(relative.isEmpty() || relative.split('/').none { it.isEmpty() || it == "." || it == ".." }) + return RootfsDocumentId(rootfs, relative) + } + } +} + +internal object RootfsPathResolver { + fun resolve(root: Path, relative: String): Path { + val absoluteRoot = root.toAbsolutePath().normalize() + val pending = ArrayDeque(relative.split('/').filter(String::isNotEmpty)) + val resolved = ArrayDeque() + var symlinks = 0 + while (pending.isNotEmpty()) { + when (val part = pending.removeFirst()) { + "." -> Unit + ".." -> { + require(resolved.isNotEmpty()) + resolved.removeLast() + } + else -> { + val candidate = resolved.fold(absoluteRoot) { path, component -> path.resolve(component) }.resolve(part) + if (Files.isSymbolicLink(candidate)) { + require(++symlinks <= 40) + val target = Files.readSymbolicLink(candidate) + if (target.isAbsolute) resolved.clear() + target.map(Path::toString).asReversed().forEach(pending::addFirst) + } else { + resolved.addLast(part) + } + } + } + } + return resolved.fold(absoluteRoot) { path, component -> path.resolve(component) } + .normalize() + .also { require(it.startsWith(absoluteRoot)) } + } +} diff --git a/app/src/test/java/org/randomcoder/udroid/filesystem/RootfsDocumentsProviderTest.kt b/app/src/test/java/org/randomcoder/udroid/filesystem/RootfsDocumentsProviderTest.kt new file mode 100644 index 0000000..e768047 --- /dev/null +++ b/app/src/test/java/org/randomcoder/udroid/filesystem/RootfsDocumentsProviderTest.kt @@ -0,0 +1,39 @@ +package org.randomcoder.udroid.filesystem + +import java.nio.file.Files +import org.junit.Assert.assertEquals +import org.junit.Assert.assertThrows +import org.junit.Test +import org.randomcoder.udroid.R + +class RootfsDocumentsProviderTest { + @Test + fun rootIconsFollowTheInstalledDistribution() { + assertEquals(R.drawable.distro_ubuntu, rootfsIconResource("udroid-jammy-raw")) + assertEquals(R.drawable.distro_debian, rootfsIconResource("debian-trixie")) + assertEquals(R.drawable.distro_unknown, rootfsIconResource("custom-linux")) + } + + @Test + fun documentIdsRoundTripAndRejectTraversal() { + val id = RootfsDocumentId.decode(RootfsDocumentId.encode("ubuntu:noble", "usr/share/a b")) + assertEquals("ubuntu:noble", id.rootfs) + assertEquals("usr/share/a b", id.relative) + assertThrows(IllegalArgumentException::class.java) { + RootfsDocumentId.decode(RootfsDocumentId.encode("ubuntu", "../outside")) + } + } + + @Test + fun resolverKeepsRelativeAndAbsoluteSymlinksInsideGuestRoot() { + val root = Files.createTempDirectory("rootfs-provider") + Files.createDirectories(root.resolve("usr/bin")) + Files.write(root.resolve("usr/bin/tool"), "ok".toByteArray()) + Files.createSymbolicLink(root.resolve("bin"), root.fileSystem.getPath("usr/bin")) + Files.createSymbolicLink(root.resolve("sbin"), root.fileSystem.getPath("/usr/bin")) + + assertEquals(root.resolve("usr/bin/tool"), RootfsPathResolver.resolve(root, "bin/tool")) + assertEquals(root.resolve("usr/bin/tool"), RootfsPathResolver.resolve(root, "sbin/tool")) + assertThrows(IllegalArgumentException::class.java) { RootfsPathResolver.resolve(root, "../outside") } + } +} From fa2d0aef11071915632d186d6b81553e90119e0c Mon Sep 17 00:00:00 2001 From: saicharankandukuri Date: Sun, 8 Nov 2026 16:37:41 +0530 Subject: [PATCH 13/14] docs: document terminal links and Linux file access Record OSC 8 behavior and URI restrictions, and explain how installed Linux filesystems appear as read-only locations in Android's system picker. --- docs/USER_GUIDE.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index fdde0e4..de499dc 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -123,7 +123,7 @@ with the terminal outside the normal text grid. uDroid currently supports: | OSC 104, 110, 111, 112 | Supported | Resets indexed colors, foreground, background, or cursor color. | | OSC 133 A/B/C/D | Bash, Zsh, and Fish | Tracks whether the foreground command is running so uDroid can warn before closing its tab. | | OSC 7 | Bash, Zsh, and Fish | Tracks the guest working directory so a new tab for the same Linux system opens in the current directory. | -| OSC 8 | Not supported | Hyperlinks are displayed as ordinary text. | +| OSC 8 | Supported | Underlines hyperlinks and offers **Open** or **Copy** when selected. | | OSC 9, 777 | Not supported | Terminal programs cannot create Android notifications. | | OSC 9;4 | Not supported | Terminal progress is not shown in the Android UI. | | OSC 1337 images | Not supported | Inline image rendering is not implemented by the embedded Termux terminal. | @@ -141,7 +141,8 @@ The exit status carried by OSC 133 D is accepted but is not shown or saved. OSC 52 lets command output replace the Android clipboard, so run untrusted terminal programs with the same care as programs that can print arbitrary -terminal control sequences. See the +terminal control sequences. OSC 8 opens only `http`, `https`, and `mailto` +links after confirmation; other link types can still be copied. See the [xterm control-sequence reference](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html) for title, color, and clipboard sequences, and [VS Code shell integration](https://code.visualstudio.com/docs/terminal/shell-integration) @@ -247,6 +248,10 @@ Open **File access** to choose which Android and system paths Linux can see. Internal shared storage can be exposed at `/mnt/shared`. Custom mounts are for other Android paths the app can read. +Android's Files app also lists each installed Linux system as a read-only +location. Use it to browse or open Linux files without sharing the whole +filesystem with Android apps; file changes still belong in Linux. + Stop Linux before changing mounts. Session mounts used by the display, audio, or graphics features are added automatically only when those features need them. From ffa3f0d6ff66cf734982d63ae52943ab9a25ac05 Mon Sep 17 00:00:00 2001 From: saicharankandukuri Date: Sun, 8 Nov 2026 17:47:25 +0530 Subject: [PATCH 14/14] fix: address terminal integration review findings Write guest configuration atomically without following symlinks, while allowing terminal launch to continue if integration refresh fails. Keep Android Files roots synchronized with installed distro names, escape percent signs in OSC 7 paths, and preserve profile-name edits while changing colors. --- .../filesystem/RootfsDocumentsProvider.kt | 30 ++- .../udroid/install/AndroidRootfsRuntime.kt | 213 ++++++++++++++---- .../udroid/install/InstallerService.kt | 3 + .../udroid/runtime/InstalledRootfsRegistry.kt | 2 + .../udroid/runtime/ProotTerminalLaunch.kt | 6 +- .../udroid/ui/TerminalSettingsPage.kt | 9 +- .../filesystem/RootfsDocumentsProviderTest.kt | 6 + .../udroid/install/RootfsPipelineTest.kt | 30 +++ .../terminal/OperatingSystemControlTest.java | 5 +- 9 files changed, 247 insertions(+), 57 deletions(-) diff --git a/app/src/main/java/org/randomcoder/udroid/filesystem/RootfsDocumentsProvider.kt b/app/src/main/java/org/randomcoder/udroid/filesystem/RootfsDocumentsProvider.kt index eb67f46..8e683d3 100644 --- a/app/src/main/java/org/randomcoder/udroid/filesystem/RootfsDocumentsProvider.kt +++ b/app/src/main/java/org/randomcoder/udroid/filesystem/RootfsDocumentsProvider.kt @@ -1,12 +1,15 @@ package org.randomcoder.udroid.filesystem +import android.content.Context import android.database.Cursor import android.database.MatrixCursor import android.os.CancellationSignal import android.os.ParcelFileDescriptor import android.provider.DocumentsContract import android.provider.DocumentsProvider +import android.util.Log import org.randomcoder.udroid.R +import org.randomcoder.udroid.install.InstalledRootfsSourceStore import org.randomcoder.udroid.runtime.InstalledRootfs import org.randomcoder.udroid.runtime.InstalledRootfsRegistry import org.randomcoder.udroid.runtime.terminalDistroTitle @@ -21,6 +24,7 @@ import java.util.Base64 class RootfsDocumentsProvider : DocumentsProvider() { private val registry by lazy { InstalledRootfsRegistry(requireNotNull(context)) } + private val installSources by lazy { InstalledRootfsSourceStore(requireNotNull(context)) } override fun onCreate(): Boolean = true @@ -30,7 +34,7 @@ class RootfsDocumentsProvider : DocumentsProvider() { cursor.newRow() .add(DocumentsContract.Root.COLUMN_ROOT_ID, rootfs.name) .add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, RootfsDocumentId.encode(rootfs.name, "")) - .add(DocumentsContract.Root.COLUMN_TITLE, terminalDistroTitle(rootfs.name)) + .add(DocumentsContract.Root.COLUMN_TITLE, displayName(rootfs)) .add(DocumentsContract.Root.COLUMN_SUMMARY, "uDroid Linux file system") .add(DocumentsContract.Root.COLUMN_ICON, rootfsIconResource(rootfs.name)) .add(DocumentsContract.Root.COLUMN_FLAGS, DocumentsContract.Root.FLAG_LOCAL_ONLY or DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD) @@ -98,7 +102,7 @@ class RootfsDocumentsProvider : DocumentsProvider() { .add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, RootfsDocumentId.encode(document.rootfs.name, document.relative)) .add( DocumentsContract.Document.COLUMN_DISPLAY_NAME, - if (document.relative.isEmpty()) terminalDistroTitle(document.rootfs.name) else document.relative.substringAfterLast('/'), + if (document.relative.isEmpty()) displayName(document.rootfs) else document.relative.substringAfterLast('/'), ) .add(DocumentsContract.Document.COLUMN_MIME_TYPE, if (directory) DocumentsContract.Document.MIME_TYPE_DIR else mimeType(document.path)) .add(DocumentsContract.Document.COLUMN_FLAGS, 0) @@ -120,6 +124,12 @@ class RootfsDocumentsProvider : DocumentsProvider() { private fun mimeType(path: Path): String = URLConnection.guessContentTypeFromName(path.fileName.toString()) ?: "application/octet-stream" + private fun displayName(rootfs: InstalledRootfs): String = + rootfsDisplayName( + rootfs.name, + runCatching { installSources.load(rootfs.name)?.displayName }.getOrNull(), + ) + private data class ResolvedDocument( val rootfs: InstalledRootfs, val relative: String, @@ -147,6 +157,22 @@ class RootfsDocumentsProvider : DocumentsProvider() { } } +internal fun rootfsDisplayName( + rootfsName: String, + installedDisplayName: String?, +): String = installedDisplayName?.takeIf(String::isNotBlank) ?: terminalDistroTitle(rootfsName) + +internal fun notifyRootfsDocumentsChanged(context: Context) { + runCatching { + context.contentResolver.notifyChange( + DocumentsContract.buildRootsUri("${context.packageName}.linux-files"), + null, + ) + }.onFailure { error -> + Log.w("uDroidFiles", "Could not notify Android Files about Linux systems", error) + } +} + internal fun rootfsIconResource(name: String): Int { val normalized = name.lowercase() return when { diff --git a/app/src/main/java/org/randomcoder/udroid/install/AndroidRootfsRuntime.kt b/app/src/main/java/org/randomcoder/udroid/install/AndroidRootfsRuntime.kt index 0b6e73c..1a2aac0 100644 --- a/app/src/main/java/org/randomcoder/udroid/install/AndroidRootfsRuntime.kt +++ b/app/src/main/java/org/randomcoder/udroid/install/AndroidRootfsRuntime.kt @@ -21,7 +21,16 @@ import java.io.InterruptedIOException import java.io.InputStreamReader import java.io.IOException import java.io.RandomAccessFile +import java.nio.ByteBuffer +import java.nio.charset.StandardCharsets +import java.nio.channels.FileChannel import java.nio.file.Files +import java.nio.file.LinkOption +import java.nio.file.Path +import java.nio.file.Paths +import java.nio.file.StandardCopyOption +import java.nio.file.StandardOpenOption +import java.nio.file.attribute.PosixFilePermission import java.util.concurrent.TimeUnit import java.util.zip.GZIPInputStream import kotlin.math.max @@ -308,32 +317,33 @@ class ProotTarExtractor( class AndroidRootfsConfigurator : RootfsConfigurator { override fun configure(rootfs: File) { + val writer = RootfsWriter(rootfs.toPath()) listOf("dev", "dev/shm", "proc", "sys", "tmp", "etc", "etc/profile.d").forEach { - check(File(rootfs, it).mkdirs() || File(rootfs, it).isDirectory) { - "Could not prepare /$it" - } + writer.ensureDirectory(it) } - replaceFile( - File(rootfs, "etc/hosts"), + writer.replaceFile( + "etc/hosts", """ 127.0.0.1 localhost 127.0.0.1 localhost.localdomain ::1 localhost ip6-localhost ip6-loopback """.trimIndent() + "\n", ) - replaceFile( - File(rootfs, "etc/resolv.conf"), + writer.replaceFile( + "etc/resolv.conf", "nameserver 1.1.1.1\nnameserver 8.8.8.8\n", ) configureShellProfile(rootfs) - appendAndroidGroups(File(rootfs, "etc/group")) - File(rootfs, "usr/bin/sudo").takeIf(File::exists)?.setExecutable(true, false) + appendAndroidGroups(writer) + writer.makeExecutableIfRegular("usr/bin/sudo") } fun configureShellProfile(rootfs: File) { - val integrationDirectory = File(rootfs, "etc/udroid/shell-integration") - replaceFile( - File(rootfs, "etc/profile.d/udroid.sh"), + val writer = RootfsWriter(rootfs.toPath()) + writer.ensureDirectory("etc/profile.d") + writer.ensureDirectory("etc/udroid/shell-integration") + writer.replaceFile( + "etc/profile.d/udroid.sh", """ export ANDROID_ART_ROOT=${'$'}{ANDROID_ART_ROOT-} export ANDROID_DATA=${'$'}{ANDROID_DATA-} @@ -353,8 +363,8 @@ class AndroidRootfsConfigurator : RootfsConfigurator { """.trimIndent() + "\n", executable = true, ) - replaceFile( - File(integrationDirectory, "udroid.bash"), + writer.replaceFile( + "etc/udroid/shell-integration/udroid.bash", """ case ${'$'}- in *i*) ;; @@ -369,6 +379,7 @@ class AndroidRootfsConfigurator : RootfsConfigurator { __udroid_status=${'$'}? printf '\033]133;D;%s\033\\\033]133;A\033\\' "${'$'}__udroid_status" __udroid_cwd=${'$'}{PWD//[[:cntrl:]]/} + __udroid_cwd=${'$'}{__udroid_cwd//%/%25} if [ "${'$'}__udroid_cwd" != "${'$'}__udroid_last_cwd" ]; then printf '\033]7;kitty-shell-cwd://localhost%s\033\\' "${'$'}__udroid_cwd" __udroid_last_cwd=${'$'}__udroid_cwd @@ -393,8 +404,9 @@ class AndroidRootfsConfigurator : RootfsConfigurator { ) if (listOf("bin/zsh", "usr/bin/zsh").any { File(rootfs, it).exists() }) { - replaceFile( - File(integrationDirectory, "udroid.zsh"), + writer.ensureDirectory("etc/zsh") + writer.replaceFile( + "etc/udroid/shell-integration/udroid.zsh", """ [[ -o interactive ]] || return [[ -z "${'$'}{UDROID_ZSH_INTEGRATION-}" ]] || return @@ -405,6 +417,7 @@ class AndroidRootfsConfigurator : RootfsConfigurator { __udroid_report_cwd() { local cwd=${'$'}{PWD//[[:cntrl:]]/} + cwd=${'$'}{cwd//%/%25} [[ "${'$'}cwd" == "${'$'}__udroid_last_cwd" ]] && return builtin print -n -- "\e]7;kitty-shell-cwd://localhost${'$'}cwd\e\\" __udroid_last_cwd=${'$'}cwd @@ -430,15 +443,16 @@ class AndroidRootfsConfigurator : RootfsConfigurator { add-zsh-hook chpwd __udroid_report_cwd """.trimIndent() + "\n", ) - ensureLine( - File(rootfs, "etc/zsh/zshrc"), + writer.ensureLine( + "etc/zsh/zshrc", "[[ -r /etc/udroid/shell-integration/udroid.zsh ]] && builtin source /etc/udroid/shell-integration/udroid.zsh", ) } if (listOf("bin/fish", "usr/bin/fish").any { File(rootfs, it).exists() }) { - replaceFile( - File(rootfs, "etc/fish/conf.d/udroid.fish"), + writer.ensureDirectory("etc/fish/conf.d") + writer.replaceFile( + "etc/fish/conf.d/udroid.fish", """ status is-interactive; or return set -q UDROID_FISH_INTEGRATION; and return @@ -474,8 +488,8 @@ class AndroidRootfsConfigurator : RootfsConfigurator { } } - private fun appendAndroidGroups(groupFile: File) { - val existing = if (groupFile.isFile) groupFile.readLines() else emptyList() + private fun appendAndroidGroups(writer: RootfsWriter) { + val existing = writer.readText("etc/group")?.lines().orEmpty() val existingGids = existing.mapNotNull { line -> line.split(':').getOrNull(2)?.toIntOrNull() }.toSet() val gids = @@ -491,44 +505,147 @@ class AndroidRootfsConfigurator : RootfsConfigurator { ?.distinct() .orEmpty() if (gids.isEmpty()) return - groupFile.parentFile?.mkdirs() - groupFile.appendText( - gids.joinToString(separator = "", transform = { "aid_$it:x:$it:root\n" }), + writer.replaceFile( + "etc/group", + buildString { + append(existing.joinToString("\n")) + if (isNotEmpty() && !endsWith('\n')) append('\n') + append(gids.joinToString(separator = "", transform = { "aid_$it:x:$it:root\n" })) + }, ) } +} - private fun replaceFile( - target: File, - contents: String, - executable: Boolean = false, - ) { - target.parentFile?.mkdirs() - if (target.exists() || target.isSymbolicLink()) { - check(target.delete()) { "Could not replace ${target.path}" } +private class RootfsWriter(rootfs: Path) { + private val root = rootfs.toAbsolutePath().normalize() + + init { + require(Files.isDirectory(root, LinkOption.NOFOLLOW_LINKS) && !Files.isSymbolicLink(root)) { + "Rootfs is not a real directory: $root" } - FileOutputStream(target).use { output -> - output.write(contents.toByteArray()) - output.fd.sync() + } + + fun ensureDirectory(relative: String) { + var current = root + relativePath(relative).forEach { component -> + current = current.resolve(component) + if (Files.exists(current, LinkOption.NOFOLLOW_LINKS)) { + check(Files.isDirectory(current, LinkOption.NOFOLLOW_LINKS) && !Files.isSymbolicLink(current)) { + "Rootfs path is not a real directory: $current" + } + } else { + Files.createDirectory(current) + } } - target.setReadable(true, false) - target.setWritable(true, true) - if (executable) target.setExecutable(true, false) } - private fun ensureLine( - target: File, + fun readText(relative: String): String? { + val target = resolveExistingFile(relative) ?: return null + check(Files.isRegularFile(target, LinkOption.NOFOLLOW_LINKS) && !Files.isSymbolicLink(target)) { + "Rootfs path is not a regular file: $target" + } + return String(Files.readAllBytes(target), StandardCharsets.UTF_8) + } + + fun ensureLine( + relative: String, line: String, ) { - target.parentFile?.mkdirs() - val existing = target.takeIf(File::isFile)?.readText().orEmpty() + val existing = readText(relative).orEmpty() if (existing.lineSequence().any { it == line }) return - target.appendText((if (existing.isEmpty() || existing.endsWith('\n')) "" else "\n") + line + "\n") - target.setReadable(true, false) - target.setWritable(true, true) + replaceFile(relative, existing + (if (existing.isEmpty() || existing.endsWith('\n')) "" else "\n") + line + "\n") } - private fun File.isSymbolicLink(): Boolean = - Files.isSymbolicLink(toPath()) + fun replaceFile( + relative: String, + contents: String, + executable: Boolean = false, + ) { + val target = resolveFile(relative) + val parent = checkNotNull(target.parent) + val existingPermissions = + target + .takeIf { Files.isRegularFile(it, LinkOption.NOFOLLOW_LINKS) && !Files.isSymbolicLink(it) } + ?.let { Files.getPosixFilePermissions(it, LinkOption.NOFOLLOW_LINKS) } + ?: DEFAULT_FILE_PERMISSIONS + val targetPermissions = + if (executable) existingPermissions + EXECUTE_PERMISSIONS else existingPermissions + val staging = Files.createTempFile(parent, ".udroid-", ".tmp") + try { + FileChannel.open(staging, StandardOpenOption.WRITE).use { channel -> + val bytes = contents.toByteArray() + var buffer = ByteBuffer.wrap(bytes) + while (buffer.hasRemaining()) channel.write(buffer) + channel.force(true) + } + Files.setPosixFilePermissions(staging, targetPermissions) + Files.move( + staging, + target, + StandardCopyOption.ATOMIC_MOVE, + StandardCopyOption.REPLACE_EXISTING, + ) + } finally { + Files.deleteIfExists(staging) + } + } + + fun makeExecutableIfRegular(relative: String) { + val target = resolveExistingFile(relative) ?: return + check(Files.isRegularFile(target, LinkOption.NOFOLLOW_LINKS) && !Files.isSymbolicLink(target)) { + "Rootfs path is not a regular file: $target" + } + Files.setPosixFilePermissions( + target, + Files.getPosixFilePermissions(target, LinkOption.NOFOLLOW_LINKS) + EXECUTE_PERMISSIONS, + ) + } + + private fun resolveFile(relative: String): Path { + val relativePath = relativePath(relative) + val parent = relativePath.parent?.toString().orEmpty() + if (parent.isNotEmpty()) ensureDirectory(parent) + return root.resolve(relativePath) + } + + private fun resolveExistingFile(relative: String): Path? { + val relativePath = relativePath(relative) + var current = root + relativePath.forEachIndexed { index, component -> + current = current.resolve(component) + if (!Files.exists(current, LinkOption.NOFOLLOW_LINKS)) return null + if (index < relativePath.nameCount - 1) { + check(Files.isDirectory(current, LinkOption.NOFOLLOW_LINKS) && !Files.isSymbolicLink(current)) { + "Rootfs path is not a real directory: $current" + } + } + } + return current + } + + private fun relativePath(relative: String): Path { + val path = Paths.get(relative) + require(!path.isAbsolute && path.none { it.toString() == ".." }) { + "Rootfs path must stay relative: $relative" + } + return path.normalize() + } + + private companion object { + val DEFAULT_FILE_PERMISSIONS = + setOf( + PosixFilePermission.OWNER_READ, + PosixFilePermission.OWNER_WRITE, + PosixFilePermission.GROUP_READ, + PosixFilePermission.OTHERS_READ, + ) + val EXECUTE_PERMISSIONS = + setOf( + PosixFilePermission.OWNER_EXECUTE, + PosixFilePermission.GROUP_EXECUTE, + PosixFilePermission.OTHERS_EXECUTE, + ) + } } class ProotRootfsHealthCheck( diff --git a/app/src/main/java/org/randomcoder/udroid/install/InstallerService.kt b/app/src/main/java/org/randomcoder/udroid/install/InstallerService.kt index 905af48..903436d 100644 --- a/app/src/main/java/org/randomcoder/udroid/install/InstallerService.kt +++ b/app/src/main/java/org/randomcoder/udroid/install/InstallerService.kt @@ -15,6 +15,7 @@ import androidx.core.content.ContextCompat import org.randomcoder.udroid.MainActivity import org.randomcoder.udroid.UdroidApplication import org.randomcoder.udroid.catalog.DistroVariant +import org.randomcoder.udroid.filesystem.notifyRootfsDocumentsChanged import org.randomcoder.udroid.oci.OciImageReference import org.randomcoder.udroid.oci.OciInstallEvent import org.randomcoder.udroid.oci.OciInstallStage @@ -387,6 +388,7 @@ class InstallerService : Service() { ) app.rootfsRegistry.setActiveIfNone(result.rootfs.name) rememberInstallSource(work) + notifyRootfsDocumentsChanged(this) progress.complete(result.reusedInstallation) app.journal.append( component = "installer", @@ -518,6 +520,7 @@ class InstallerService : Service() { val operationId = work.operationId app.rootfsRegistry.setActiveIfNone(rootfs.name) rememberInstallSource(work) + notifyRootfsDocumentsChanged(this) val previous = app.installState.current() val completed = InstallProgress( diff --git a/app/src/main/java/org/randomcoder/udroid/runtime/InstalledRootfsRegistry.kt b/app/src/main/java/org/randomcoder/udroid/runtime/InstalledRootfsRegistry.kt index c4420bc..fecb374 100644 --- a/app/src/main/java/org/randomcoder/udroid/runtime/InstalledRootfsRegistry.kt +++ b/app/src/main/java/org/randomcoder/udroid/runtime/InstalledRootfsRegistry.kt @@ -2,6 +2,7 @@ package org.randomcoder.udroid.runtime import android.content.Context import org.randomcoder.udroid.UdroidApplication +import org.randomcoder.udroid.filesystem.notifyRootfsDocumentsChanged import org.randomcoder.udroid.install.RootfsInstallationPipeline import java.io.File import java.io.IOException @@ -84,6 +85,7 @@ class InstalledRootfsRegistry(context: Context) { "Failed to clear the active Linux system" } } + notifyRootfsDocumentsChanged(appContext) } private fun persistActive(name: String) { diff --git a/app/src/main/java/org/randomcoder/udroid/runtime/ProotTerminalLaunch.kt b/app/src/main/java/org/randomcoder/udroid/runtime/ProotTerminalLaunch.kt index 8f32f39..2efa52e 100644 --- a/app/src/main/java/org/randomcoder/udroid/runtime/ProotTerminalLaunch.kt +++ b/app/src/main/java/org/randomcoder/udroid/runtime/ProotTerminalLaunch.kt @@ -1,6 +1,7 @@ package org.randomcoder.udroid.runtime import android.content.Context +import android.util.Log import org.randomcoder.udroid.audio.AudioEndpoint import org.randomcoder.udroid.install.AndroidRootfsConfigurator import org.randomcoder.udroid.install.ProotRuntime @@ -38,7 +39,10 @@ object ProotTerminalLaunchBuilder { require(File(rootfs, RootfsInstallationPipeline.READY_MARKER).isFile) { "The selected Linux image is not ready" } - AndroidRootfsConfigurator().configureShellProfile(rootfs) + runCatching { AndroidRootfsConfigurator().configureShellProfile(rootfs) } + .onFailure { error -> + Log.w("uDroidTerminal", "Could not refresh guest shell integration", error) + } val linker = AndroidExecutableCommand.systemLinkerPath() val temporaryDirectory = File(context.cacheDir, "proot").apply { diff --git a/app/src/main/java/org/randomcoder/udroid/ui/TerminalSettingsPage.kt b/app/src/main/java/org/randomcoder/udroid/ui/TerminalSettingsPage.kt index 8bf46bd..746b126 100644 --- a/app/src/main/java/org/randomcoder/udroid/ui/TerminalSettingsPage.kt +++ b/app/src/main/java/org/randomcoder/udroid/ui/TerminalSettingsPage.kt @@ -1084,6 +1084,7 @@ private fun TerminalProfileEditor( onSave: (TerminalColorProfile) -> Unit, ) { var profile by remember(initial.id) { mutableStateOf(initial) } + var profileName by remember(initial.id) { mutableStateOf(initial.name) } var selectedColor by remember { mutableIntStateOf(BACKGROUND_COLOR) } val color = profile.colorAt(selectedColor) var hexText by remember(selectedColor, color) { mutableStateOf(formatTerminalColor(color)) } @@ -1101,8 +1102,8 @@ private fun TerminalProfileEditor( ) { item { OutlinedTextField( - value = profile.name, - onValueChange = { profile = profile.copy(name = it.take(40)) }, + value = profileName, + onValueChange = { profileName = it.take(40) }, modifier = Modifier.fillMaxWidth(), label = { Text("Profile name") }, singleLine = true, @@ -1164,8 +1165,8 @@ private fun TerminalProfileEditor( } item { Button( - onClick = { onSave(profile) }, - enabled = profile.name.isNotBlank(), + onClick = { onSave(profile.copy(name = profileName.trim())) }, + enabled = profileName.isNotBlank(), modifier = Modifier.fillMaxWidth(), ) { Text("Save profile") diff --git a/app/src/test/java/org/randomcoder/udroid/filesystem/RootfsDocumentsProviderTest.kt b/app/src/test/java/org/randomcoder/udroid/filesystem/RootfsDocumentsProviderTest.kt index e768047..84c2729 100644 --- a/app/src/test/java/org/randomcoder/udroid/filesystem/RootfsDocumentsProviderTest.kt +++ b/app/src/test/java/org/randomcoder/udroid/filesystem/RootfsDocumentsProviderTest.kt @@ -14,6 +14,12 @@ class RootfsDocumentsProviderTest { assertEquals(R.drawable.distro_unknown, rootfsIconResource("custom-linux")) } + @Test + fun savedInstallNameTakesPriorityOverTheInternalDirectoryName() { + assertEquals("Ubuntu 22.04 LTS", rootfsDisplayName("udroid-jammy-raw", "Ubuntu 22.04 LTS")) + assertEquals("Ubuntu Jammy", rootfsDisplayName("udroid-jammy-raw", null)) + } + @Test fun documentIdsRoundTripAndRejectTraversal() { val id = RootfsDocumentId.decode(RootfsDocumentId.encode("ubuntu:noble", "usr/share/a b")) diff --git a/app/src/test/java/org/randomcoder/udroid/install/RootfsPipelineTest.kt b/app/src/test/java/org/randomcoder/udroid/install/RootfsPipelineTest.kt index ba0cf14..df9044f 100644 --- a/app/src/test/java/org/randomcoder/udroid/install/RootfsPipelineTest.kt +++ b/app/src/test/java/org/randomcoder/udroid/install/RootfsPipelineTest.kt @@ -161,6 +161,7 @@ class RootfsPipelineTest { assertTrue(bashIntegration.contains("133;C")) assertTrue(bashIntegration.contains("133;D")) assertTrue(bashIntegration.contains("kitty-shell-cwd://localhost")) + assertTrue(bashIntegration.contains("//%/%25")) } @Test @@ -179,6 +180,7 @@ class RootfsPipelineTest { assertTrue(zshIntegration.contains("add-zsh-hook preexec")) assertTrue(zshIntegration.contains("add-zsh-hook chpwd")) assertTrue(zshIntegration.contains("kitty-shell-cwd://localhost")) + assertTrue(zshIntegration.contains("//%/%25")) assertEquals( 1, File(rootfs, "etc/zsh/zshrc") @@ -193,6 +195,34 @@ class RootfsPipelineTest { assertTrue(fishIntegration.contains("file://localhost")) } + @Test + fun `android compatibility configuration never follows guest symlinks`() { + val rootfs = Files.createTempDirectory("udroid-configurator-symlink").toFile() + val outside = Files.createTempDirectory("udroid-configurator-outside").toFile() + val profileDirectory = File(rootfs, "etc/profile.d") + try { + File(rootfs, "etc").mkdirs() + val outsideHosts = File(outside, "hosts").apply { writeText("keep me") } + Files.createSymbolicLink(File(rootfs, "etc/hosts").toPath(), outsideHosts.toPath()) + + AndroidRootfsConfigurator().configure(rootfs) + + assertEquals("keep me", outsideHosts.readText()) + assertFalse(Files.isSymbolicLink(File(rootfs, "etc/hosts").toPath())) + + profileDirectory.deleteRecursively() + Files.createSymbolicLink(profileDirectory.toPath(), outside.toPath()) + val failure = runCatching { AndroidRootfsConfigurator().configure(rootfs) }.exceptionOrNull() + + assertTrue(failure is IllegalStateException) + assertFalse(File(outside, "udroid.sh").exists()) + } finally { + if (Files.isSymbolicLink(profileDirectory.toPath())) Files.delete(profileDirectory.toPath()) + rootfs.deleteRecursively() + outside.deleteRecursively() + } + } + private fun fixture(): Fixture { val directory = Files.createTempDirectory("udroid-rootfs-test").toFile() val archive = File(directory, "jammy.tar.gz").apply { writeText("archive") } diff --git a/third_party/termux/terminal-emulator/src/test/java/com/termux/terminal/OperatingSystemControlTest.java b/third_party/termux/terminal-emulator/src/test/java/com/termux/terminal/OperatingSystemControlTest.java index de6535d..f7c9b33 100644 --- a/third_party/termux/terminal-emulator/src/test/java/com/termux/terminal/OperatingSystemControlTest.java +++ b/third_party/termux/terminal-emulator/src/test/java/com/termux/terminal/OperatingSystemControlTest.java @@ -32,14 +32,15 @@ public void testWorkingDirectory() { enterString("\033]7;file://localhost/root/hello%20world\007"); enterString("\033]7;kitty-shell-cwd://localhost/home/用户 + notes\033\\"); - assertEquals(List.of("/root/hello world", "/home/用户 + notes"), mOutput.workingDirectoryChanges); + enterString("\033]7;kitty-shell-cwd://localhost/home/100%25 ready\033\\"); + assertEquals(List.of("/root/hello world", "/home/用户 + notes", "/home/100% ready"), mOutput.workingDirectoryChanges); enterString("\033]7;file://example.com/root\007"); enterString("\033]7;file://localhost/root/../etc\007"); enterString("\033]7;file://localhost/root/%1Bbad\007"); enterString("\033]7;file://localhost/root/%invalid\007"); enterString("\033]7;/root\007"); - assertEquals(2, mOutput.workingDirectoryChanges.size()); + assertEquals(3, mOutput.workingDirectoryChanges.size()); } public void testSetTitle() throws Exception {