Implement TermuxLaunch feature#101
Conversation
- Added TermuxIntentLauncher utility to handle com.termux.RUN_COMMAND intent. - Updated AndroidManifest.xml with <queries> for Termux detection. - Extended ToolDetails and ToolMapper to include requireRoot field. - Updated ToolDetailViewModel with InstallState and runInstall logic. - Modified InstallCommandRow to include a "Run" button. - Updated ToolDetailScreen to handle installation feedback and root warnings.
|
👋 Jules, reporting for duty! I'm here to lend a hand with this pull request. When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job! For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 33 minutes and 56 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThis pull request introduces functionality to execute tool installation commands directly within Termux from the app. It adds a Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant ToolDetailScreen as Detail Screen
participant ViewModel as ToolDetailViewModel
participant Launcher as TermuxIntentLauncher
participant PM as PackageManager
participant Termux as Termux App
User->>ToolDetailScreen: Clicks "Run" button on install command
ToolDetailScreen->>ViewModel: runInstall(context, tool)
ViewModel->>Launcher: isTermuxInstalled(context)
Launcher->>PM: getPackageInfo("com.termux")
PM-->>Launcher: Package info / Exception
alt Termux Not Installed
Launcher-->>ViewModel: false
ViewModel->>ViewModel: installState = TermuxMissing
else Termux Installed
Launcher-->>ViewModel: true
ViewModel->>Launcher: run(context, joinedCommand)
Launcher->>Termux: startForegroundService/startService(intent)
Termux-->>Launcher: Service started
ViewModel->>ViewModel: installState = Launched
end
ViewModel-->>ToolDetailScreen: installState StateFlow emits
ToolDetailScreen->>ToolDetailScreen: Collect state & show snackbar/dialog
User->>ToolDetailScreen: Dismisses feedback
ToolDetailScreen->>ViewModel: resetInstallState()
ViewModel->>ViewModel: installState = Idle
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (1)
app/src/main/java/com/maazm7d/termuxhub/ui/screens/details/ToolDetailViewModel.kt (1)
18-35: Recommended: use a one-shot event stream instead ofStateFlowfor install outcomes.
InstallStateis being used as an event (show snackbar / show dialog once), butMutableStateFlowdeduplicates equal consecutive values, and the state persists untilresetInstallState()is called. That combination causes subtle UX bugs (see the corresponding comment inToolDetailScreen.kt).Channel<InstallEvent>(Channel.BUFFERED).receiveAsFlow()— orMutableSharedFlow<InstallEvent>(extraBufferCapacity = 1)— is a better fit and also removes the need forresetInstallState().♻️ Sketch
- private val _installState = MutableStateFlow<InstallState>(InstallState.Idle) - val installState: StateFlow<InstallState> = _installState.asStateFlow() + private val _installEvents = Channel<InstallEvent>(Channel.BUFFERED) + val installEvents: Flow<InstallEvent> = _installEvents.receiveAsFlow()and use
viewModelScope.launch { _installEvents.send(...) }at each emission site; dropresetInstallState(). TheInstallStatesealed class can be renamed toInstallEventand loseIdle.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/com/maazm7d/termuxhub/ui/screens/details/ToolDetailViewModel.kt` around lines 18 - 35, Replace the one-off UI outcome usage of MutableStateFlow<InstallState> with an event stream: rename InstallState to InstallEvent (drop Idle), convert the backing _installState in ToolDetailViewModel to either a Channel<InstallEvent>(Channel.BUFFERED) exposed as installEvents = _installEvents.receiveAsFlow() or a MutableSharedFlow<InstallEvent>(extraBufferCapacity = 1) exposed as a Flow, update all emission sites to use viewModelScope.launch { _installEvents.send(...) } or _installEvents.emit(...), and remove/resetResetInstallState() usage and the resetInstallState() function; also update ToolDetailScreen.kt to collect from installEvents instead of reading installState.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/src/main/AndroidManifest.xml`:
- Around line 6-11: The manifest is missing the required dangerous permission
for calling Termux's RunCommandService; add the declaration for
com.termux.permission.RUN_COMMAND to AndroidManifest (so
TermuxIntentLauncher.run() can call the service without SecurityException), and
ensure the app checks/requests that dangerous permission at runtime (e.g., via
ContextCompat.checkSelfPermission()/requestPermissions() or equivalent) before
invoking TermuxIntentLauncher.run() or startForegroundService() against
RunCommandService.
In
`@app/src/main/java/com/maazm7d/termuxhub/ui/screens/details/ToolDetailScreen.kt`:
- Around line 80-101: The security dialog only mentions allow-external-apps and
misses other causes of SecurityException/launch-without-effect; update the
AlertDialog shown when showSecurityDialog is true (the AlertDialog block in
ToolDetailScreen.kt) to include guidance to (a) ensure the app declares
com.termux.permission.RUN_COMMAND in its AndroidManifest and (b) for Android 10+
enable “Draw over other apps” (system alert window) so Termux can
auto-foreground sessions; keep the existing steps but append these two checks
and short remedial instructions in the dialog text so users can resolve missing
manifest permission and overlay permission.
- Around line 283-291: The per-row "Run" buttons currently call
viewModel.runInstall(context, tool) which executes the entire joined script;
change to per-row semantics by passing the specific cmd into runInstall and
executing only that line: update the call site inside the forEach (the
InstallCommandRow invocation) to call viewModel.runInstall(context, cmd) and
modify ToolDetailViewModel.runInstall (or add an overload) to accept a String
command (instead of or in addition to the Tool) and run only that single command
in Termux; keep InstallCommandRow and the filtering logic unchanged.
- Around line 58-78: The snackbar handling holds installState until showSnackbar
completes, causing duplicate Run taps to be ignored; modify the LaunchedEffect
in ToolDetailScreen so you clear the state before showing the snackbar (call
viewModel.resetInstallState() immediately when handling InstallState.Launched,
.TermuxMissing, and .Error) rather than after
snackbarHostState.showSnackbar(...), i.e., for cases using
snackbarHostState.showSnackbar move the viewModel.resetInstallState() invocation
to occur prior to calling snackbarHostState.showSnackbar; keep
InstallState.SecurityError handling unchanged (it still sets showSecurityDialog)
or alternatively migrate the ViewModel to emit InstallEvent via a
SharedFlow<InstallEvent>(replay=0) to avoid distinct-value deduplication.
In
`@app/src/main/java/com/maazm7d/termuxhub/ui/screens/details/ToolDetailViewModel.kt`:
- Around line 53-74: runInstall currently builds commands from
ToolDetails.installCommands and may launch Termux with an empty command, block
the main thread during PackageManager/startForegroundService calls, and can
mis-handle commented lines; fix by first trimming and checking commands (if
commands.isBlank() set _installState.value = InstallState.Error("No install
commands") and return), consider stripping or ignoring shell comment-only lines
before join to avoid comment concatenation, and run the TermuxIntentLauncher.run
call off the main thread by wrapping the work in
viewModelScope.launch(Dispatchers.Default) { ... } while preserving the existing
try/catch that sets _installState (Launched/SecurityError/Error) so state
updates happen on the main thread if needed.
In `@app/src/main/java/com/maazm7d/termuxhub/utils/TermuxIntentLauncher.kt`:
- Around line 16-17: The EXTRA_COMMAND_TERMINAL constant is using an invalid
Termux key; rename it to EXTRA_COMMAND_BACKGROUND and set its value to
"com.termux.RUN_COMMAND_BACKGROUND" (replace the existing EXTRA_COMMAND_TERMINAL
definition), then update any putExtra calls that use EXTRA_COMMAND_TERMINAL
(e.g., in the intent builder that currently does
putExtra(EXTRA_COMMAND_TERMINAL, true)) to use
putExtra(EXTRA_COMMAND_BACKGROUND, false) so the session opens in the
foreground; also update the inline comment near the putExtra call to state that
false means a foreground session.
---
Nitpick comments:
In
`@app/src/main/java/com/maazm7d/termuxhub/ui/screens/details/ToolDetailViewModel.kt`:
- Around line 18-35: Replace the one-off UI outcome usage of
MutableStateFlow<InstallState> with an event stream: rename InstallState to
InstallEvent (drop Idle), convert the backing _installState in
ToolDetailViewModel to either a Channel<InstallEvent>(Channel.BUFFERED) exposed
as installEvents = _installEvents.receiveAsFlow() or a
MutableSharedFlow<InstallEvent>(extraBufferCapacity = 1) exposed as a Flow,
update all emission sites to use viewModelScope.launch {
_installEvents.send(...) } or _installEvents.emit(...), and
remove/resetResetInstallState() usage and the resetInstallState() function; also
update ToolDetailScreen.kt to collect from installEvents instead of reading
installState.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: ee84d1ec-db4f-4adf-983b-27dbcafa0372
📒 Files selected for processing (7)
app/src/main/AndroidManifest.xmlapp/src/main/java/com/maazm7d/termuxhub/data/mapper/ToolMapper.ktapp/src/main/java/com/maazm7d/termuxhub/domain/model/Tool.ktapp/src/main/java/com/maazm7d/termuxhub/ui/screens/details/ToolDetailScreen.ktapp/src/main/java/com/maazm7d/termuxhub/ui/screens/details/ToolDetailViewModel.ktapp/src/main/java/com/maazm7d/termuxhub/ui/screens/details/components/InstallCommandRow.ktapp/src/main/java/com/maazm7d/termuxhub/utils/TermuxIntentLauncher.kt
| <uses-permission android:name="android.permission.INTERNET" /> | ||
|
|
||
| <queries> | ||
| <package android:name="com.termux" /> | ||
| </queries> | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Does a third-party app need <uses-permission android:name="com.termux.permission.RUN_COMMAND"/> in its AndroidManifest.xml to call Termux's RunCommandService, and what protection level does Termux declare for that permission?
💡 Result:
Yes, a third-party app needs <uses-permission android:name="com.termux.permission.RUN_COMMAND"/> in its AndroidManifest.xml to call Termux's RunCommandService. The permission must also be granted by the user via Android Settings under the app's permissions (often "Additional permissions"). Termux declares this permission with protectionLevel="dangerous".
Citations:
- 1: https://github.com/termux/termux-app/wiki/RUN_COMMAND-Intent
- 2: termux/termux-app@db3ff7b
- 3: Unable to start service Intent for RUN_COMMAND termux/termux-app#1932
🏁 Script executed:
# Search for TermuxIntentLauncher in the codebase
fd -type f -name "*.kt" -o -name "*.java" -o -name "*.xml" | head -50 | xargs rg -l "TermuxIntentLauncher" 2>/dev/null || rg "TermuxIntentLauncher" --type kt --type java 2>/dev/nullRepository: maazm7d/TermuxHub
Length of output: 402
🏁 Script executed:
# Search for Termux-related service or intent handling
rg "com\.termux" --type kt --type java --type xml -B2 -A2 | head -100Repository: maazm7d/TermuxHub
Length of output: 88
🏁 Script executed:
# Look for startService calls in the codebase
rg "startService" --type kt --type java -B2 -A2 | head -50Repository: maazm7d/TermuxHub
Length of output: 88
🏁 Script executed:
# Read the TermuxIntentLauncher.kt file to see implementation
cat -n app/src/main/java/com/maazm7d/termuxhub/utils/TermuxIntentLauncher.ktRepository: maazm7d/TermuxHub
Length of output: 2268
🏁 Script executed:
# Read the AndroidManifest.xml to verify current state
cat -n app/src/main/AndroidManifest.xmlRepository: maazm7d/TermuxHub
Length of output: 2172
Critical: missing <uses-permission> for com.termux.permission.RUN_COMMAND.
The code calls startService() or startForegroundService() (lines 41–45 of TermuxIntentLauncher.kt) on Termux's RunCommandService, which is declared with android:permission="com.termux.permission.RUN_COMMAND" (protection level dangerous). Without the matching <uses-permission> in this manifest, Android rejects the service call with SecurityException. As a result, every TermuxIntentLauncher.run(...) call will fail, and the InstallState.SecurityError branch will be triggered regardless of Termux's allow-external-apps setting, making any user-facing guidance about that setting misleading.
Add the missing permission:
<uses-permission android:name="com.termux.permission.RUN_COMMAND" />Note: In addition to declaring the permission in the manifest, ensure the app requests this dangerous permission at runtime (Android 6.0+) before calling TermuxIntentLauncher.run(), or use ContextCompat.checkSelfPermission() to verify it has been granted.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/src/main/AndroidManifest.xml` around lines 6 - 11, The manifest is
missing the required dangerous permission for calling Termux's
RunCommandService; add the declaration for com.termux.permission.RUN_COMMAND to
AndroidManifest (so TermuxIntentLauncher.run() can call the service without
SecurityException), and ensure the app checks/requests that dangerous permission
at runtime (e.g., via ContextCompat.checkSelfPermission()/requestPermissions()
or equivalent) before invoking TermuxIntentLauncher.run() or
startForegroundService() against RunCommandService.
| LaunchedEffect(installState) { | ||
| when (val state = installState) { | ||
| is InstallState.Launched -> { | ||
| snackbarHostState.showSnackbar("Launched in Termux") | ||
| viewModel.resetInstallState() | ||
| } | ||
| is InstallState.TermuxMissing -> { | ||
| snackbarHostState.showSnackbar("Termux is not installed") | ||
| viewModel.resetInstallState() | ||
| } | ||
| is InstallState.SecurityError -> { | ||
| showSecurityDialog = true | ||
| viewModel.resetInstallState() | ||
| } | ||
| is InstallState.Error -> { | ||
| snackbarHostState.showSnackbar("Error: ${state.message}") | ||
| viewModel.resetInstallState() | ||
| } | ||
| else -> {} | ||
| } | ||
| } |
There was a problem hiding this comment.
Minor: state persists during snackbar display → duplicate clicks are swallowed.
snackbarHostState.showSnackbar(...) suspends until the snackbar is dismissed. viewModel.resetInstallState() is called only after it returns, so while the snackbar is on screen installState stays at e.g. Launched. If the user taps Run again during that window, the ViewModel sets Launched again, and StateFlow's distinct-value dedup drops the emission — no new snackbar and no new intent feedback. Calling resetInstallState() before showing the snackbar (or switching to a SharedFlow<InstallEvent> with replay = 0 in the ViewModel) avoids this. See the related comment in ToolDetailViewModel.kt.
🔧 Suggested reorder (root fix is in the ViewModel)
is InstallState.Launched -> {
- snackbarHostState.showSnackbar("Launched in Termux")
viewModel.resetInstallState()
+ snackbarHostState.showSnackbar("Launched in Termux")
}
is InstallState.TermuxMissing -> {
- snackbarHostState.showSnackbar("Termux is not installed")
viewModel.resetInstallState()
+ snackbarHostState.showSnackbar("Termux is not installed")
}
is InstallState.SecurityError -> {
- showSecurityDialog = true
viewModel.resetInstallState()
+ showSecurityDialog = true
}
is InstallState.Error -> {
- snackbarHostState.showSnackbar("Error: ${state.message}")
viewModel.resetInstallState()
+ snackbarHostState.showSnackbar("Error: ${state.message}")
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| LaunchedEffect(installState) { | |
| when (val state = installState) { | |
| is InstallState.Launched -> { | |
| snackbarHostState.showSnackbar("Launched in Termux") | |
| viewModel.resetInstallState() | |
| } | |
| is InstallState.TermuxMissing -> { | |
| snackbarHostState.showSnackbar("Termux is not installed") | |
| viewModel.resetInstallState() | |
| } | |
| is InstallState.SecurityError -> { | |
| showSecurityDialog = true | |
| viewModel.resetInstallState() | |
| } | |
| is InstallState.Error -> { | |
| snackbarHostState.showSnackbar("Error: ${state.message}") | |
| viewModel.resetInstallState() | |
| } | |
| else -> {} | |
| } | |
| } | |
| LaunchedEffect(installState) { | |
| when (val state = installState) { | |
| is InstallState.Launched -> { | |
| viewModel.resetInstallState() | |
| snackbarHostState.showSnackbar("Launched in Termux") | |
| } | |
| is InstallState.TermuxMissing -> { | |
| viewModel.resetInstallState() | |
| snackbarHostState.showSnackbar("Termux is not installed") | |
| } | |
| is InstallState.SecurityError -> { | |
| viewModel.resetInstallState() | |
| showSecurityDialog = true | |
| } | |
| is InstallState.Error -> { | |
| viewModel.resetInstallState() | |
| snackbarHostState.showSnackbar("Error: ${state.message}") | |
| } | |
| else -> {} | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@app/src/main/java/com/maazm7d/termuxhub/ui/screens/details/ToolDetailScreen.kt`
around lines 58 - 78, The snackbar handling holds installState until
showSnackbar completes, causing duplicate Run taps to be ignored; modify the
LaunchedEffect in ToolDetailScreen so you clear the state before showing the
snackbar (call viewModel.resetInstallState() immediately when handling
InstallState.Launched, .TermuxMissing, and .Error) rather than after
snackbarHostState.showSnackbar(...), i.e., for cases using
snackbarHostState.showSnackbar move the viewModel.resetInstallState() invocation
to occur prior to calling snackbarHostState.showSnackbar; keep
InstallState.SecurityError handling unchanged (it still sets showSecurityDialog)
or alternatively migrate the ViewModel to emit InstallEvent via a
SharedFlow<InstallEvent>(replay=0) to avoid distinct-value deduplication.
| if (showSecurityDialog) { | ||
| AlertDialog( | ||
| onDismissRequest = { showSecurityDialog = false }, | ||
| title = { Text("Permission Required") }, | ||
| text = { | ||
| Column { | ||
| Text("Termux requires 'allow-external-apps' to be enabled to run commands from other apps.") | ||
| Spacer(modifier = Modifier.height(8.dp)) | ||
| Text( | ||
| text = state.message, | ||
| style = MaterialTheme.typography.bodyMedium, | ||
| color = MaterialTheme.colorScheme.onSurfaceVariant | ||
| "1. Open Termux\n2. Run: nano ~/.termux/termux.properties\n3. Set: allow-external-apps = true\n4. Save and run: termux-reload-settings", | ||
| style = MaterialTheme.typography.bodySmall, | ||
| fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace | ||
| ) | ||
| } | ||
| }, | ||
| confirmButton = { | ||
| TextButton(onClick = { showSecurityDialog = false }) { | ||
| Text("OK") | ||
| } | ||
| } | ||
| ) | ||
| } |
There was a problem hiding this comment.
Minor: security dialog instructions may not cover the actual failure mode.
The dialog only instructs users to toggle allow-external-apps. Two additional causes of SecurityException/launch-without-effect are:
- Missing
<uses-permission android:name="com.termux.permission.RUN_COMMAND" />in this app's manifest (seeAndroidManifest.xmlcomment — Android blocks the intent before Termux ever sees it). - On Android 10+, Termux needs the "Draw over other apps" permission to auto-foreground a session when triggered by another app.
Once (1) is fixed in the manifest, consider extending this dialog's text (or adding a second step) to cover the Draw-over-apps requirement, otherwise a subset of users will see "Launched in Termux" snackbars but no visible terminal.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@app/src/main/java/com/maazm7d/termuxhub/ui/screens/details/ToolDetailScreen.kt`
around lines 80 - 101, The security dialog only mentions allow-external-apps and
misses other causes of SecurityException/launch-without-effect; update the
AlertDialog shown when showSecurityDialog is true (the AlertDialog block in
ToolDetailScreen.kt) to include guidance to (a) ensure the app declares
com.termux.permission.RUN_COMMAND in its AndroidManifest and (b) for Android 10+
enable “Draw over other apps” (system alert window) so Termux can
auto-foreground sessions; keep the existing steps but append these two checks
and short remedial instructions in the dialog text so users can resolve missing
manifest permission and overlay permission.
| tool.installCommands | ||
| .lines() | ||
| .filter { it.isNotBlank() } | ||
| .forEach { cmd -> | ||
| InstallCommandRow(command = cmd) | ||
| InstallCommandRow( | ||
| command = cmd, | ||
| onRunClick = { viewModel.runInstall(context, tool) } | ||
| ) | ||
| } |
There was a problem hiding this comment.
Per-row Run button runs every install command — UX is misleading.
InstallCommandRow renders one row per filtered line, and each row's Run button calls viewModel.runInstall(context, tool). The ViewModel then joins all lines with && and launches them as a single session. Users clicking "Run" next to line 3 of a 5-line script will see the whole script execute, which contradicts the per-row affordance. Pick one of:
- Per-row semantics: pass the specific
cmdintorunInstalland have the ViewModel execute only that line. - Script semantics: remove the per-row Run button and render a single "Run in Termux" button above or below the command block.
♻️ Option A — per-row execution
- tool.installCommands
- .lines()
- .filter { it.isNotBlank() }
- .forEach { cmd ->
- InstallCommandRow(
- command = cmd,
- onRunClick = { viewModel.runInstall(context, tool) }
- )
- }
+ tool.installCommands
+ .lines()
+ .filter { it.isNotBlank() }
+ .forEach { cmd ->
+ InstallCommandRow(
+ command = cmd,
+ onRunClick = { viewModel.runInstall(context, cmd) }
+ )
+ }(Adjust ToolDetailViewModel.runInstall signature to accept a String command, or add an overload.)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@app/src/main/java/com/maazm7d/termuxhub/ui/screens/details/ToolDetailScreen.kt`
around lines 283 - 291, The per-row "Run" buttons currently call
viewModel.runInstall(context, tool) which executes the entire joined script;
change to per-row semantics by passing the specific cmd into runInstall and
executing only that line: update the call site inside the forEach (the
InstallCommandRow invocation) to call viewModel.runInstall(context, cmd) and
modify ToolDetailViewModel.runInstall (or add an overload) to accept a String
command (instead of or in addition to the Tool) and run only that single command
in Termux; keep InstallCommandRow and the filtering logic unchanged.
| fun runInstall(context: Context, tool: ToolDetails) { | ||
| if (!TermuxIntentLauncher.isTermuxInstalled(context)) { | ||
| _installState.value = InstallState.TermuxMissing | ||
| return | ||
| } | ||
|
|
||
| val commands = tool.installCommands | ||
| .lines() | ||
| .filter { it.isNotBlank() } | ||
| .joinToString(" && ") | ||
|
|
||
| try { | ||
| TermuxIntentLauncher.run(context, commands) | ||
| _installState.value = InstallState.Launched | ||
| } catch (e: SecurityException) { | ||
| Timber.e(e, "SecurityException while launching Termux intent") | ||
| _installState.value = InstallState.SecurityError | ||
| } catch (e: Exception) { | ||
| Timber.e(e, "Error launching Termux intent") | ||
| _installState.value = InstallState.Error(e.message ?: "Unknown error") | ||
| } | ||
| } |
There was a problem hiding this comment.
Minor: edge cases in runInstall.
- If
tool.installCommandsis blank or contains only blank lines,commandsbecomes""and the code still launchesbash -c ""and reportsLaunched. Cheap guard: ifcommands.isBlank()→ setInstallState.Error("No install commands")and return. - Joining by
&&means a trailing#comment on any earlier line consumes the following joined commands. Unlikely for typicalpkg install Xlines but worth noting for scripts with comments. runInstallexecutesPackageManager.getPackageInfoandstartForegroundServiceon the caller thread (main). ConsiderviewModelScope.launch(Dispatchers.Default) { ... }for robustness (e.g., package manager is occasionally slow on first call after boot).
🔧 Proposed guard
val commands = tool.installCommands
.lines()
.filter { it.isNotBlank() }
.joinToString(" && ")
+
+ if (commands.isBlank()) {
+ _installState.value = InstallState.Error("No install commands to run")
+ return
+ }
try {
TermuxIntentLauncher.run(context, commands)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| fun runInstall(context: Context, tool: ToolDetails) { | |
| if (!TermuxIntentLauncher.isTermuxInstalled(context)) { | |
| _installState.value = InstallState.TermuxMissing | |
| return | |
| } | |
| val commands = tool.installCommands | |
| .lines() | |
| .filter { it.isNotBlank() } | |
| .joinToString(" && ") | |
| try { | |
| TermuxIntentLauncher.run(context, commands) | |
| _installState.value = InstallState.Launched | |
| } catch (e: SecurityException) { | |
| Timber.e(e, "SecurityException while launching Termux intent") | |
| _installState.value = InstallState.SecurityError | |
| } catch (e: Exception) { | |
| Timber.e(e, "Error launching Termux intent") | |
| _installState.value = InstallState.Error(e.message ?: "Unknown error") | |
| } | |
| } | |
| fun runInstall(context: Context, tool: ToolDetails) { | |
| if (!TermuxIntentLauncher.isTermuxInstalled(context)) { | |
| _installState.value = InstallState.TermuxMissing | |
| return | |
| } | |
| val commands = tool.installCommands | |
| .lines() | |
| .filter { it.isNotBlank() } | |
| .joinToString(" && ") | |
| if (commands.isBlank()) { | |
| _installState.value = InstallState.Error("No install commands to run") | |
| return | |
| } | |
| try { | |
| TermuxIntentLauncher.run(context, commands) | |
| _installState.value = InstallState.Launched | |
| } catch (e: SecurityException) { | |
| Timber.e(e, "SecurityException while launching Termux intent") | |
| _installState.value = InstallState.SecurityError | |
| } catch (e: Exception) { | |
| Timber.e(e, "Error launching Termux intent") | |
| _installState.value = InstallState.Error(e.message ?: "Unknown error") | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@app/src/main/java/com/maazm7d/termuxhub/ui/screens/details/ToolDetailViewModel.kt`
around lines 53 - 74, runInstall currently builds commands from
ToolDetails.installCommands and may launch Termux with an empty command, block
the main thread during PackageManager/startForegroundService calls, and can
mis-handle commented lines; fix by first trimming and checking commands (if
commands.isBlank() set _installState.value = InstallState.Error("No install
commands") and return), consider stripping or ignoring shell comment-only lines
before join to avoid comment concatenation, and run the TermuxIntentLauncher.run
call off the main thread by wrapping the work in
viewModelScope.launch(Dispatchers.Default) { ... } while preserving the existing
try/catch that sets _installState (Launched/SecurityError/Error) so state
updates happen on the main thread if needed.
| private const val EXTRA_COMMAND_TERMINAL = "$TERMUX_PACKAGE.RUN_COMMAND_TERMINAL" | ||
| private const val EXTRA_COMMAND_SESSION_ACTION = "$TERMUX_PACKAGE.RUN_COMMAND_SESSION_ACTION" |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
What are the valid extras for Termux RUN_COMMAND intent and does RUN_COMMAND_TERMINAL exist or is RUN_COMMAND_BACKGROUND the correct key to open a foreground terminal session?
💡 Result:
The valid extras for the Termux RUN_COMMAND intent are listed in the official Termux documentation. They include: - RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH (String, mandatory): absolute path of command. - RUN_COMMAND_SERVICE.EXTRA_ARGUMENTS (String[]): arguments to the executable. - RUN_COMMAND_SERVICE.EXTRA_REPLACE_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS (boolean): replace comma alternatives in arguments (default false, >= v0.115). - RUN_COMMAND_SERVICE.EXTRA_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS (String): specific comma alternative characters (>= v0.115). - RUN_COMMAND_SERVICE.EXTRA_STDIN (String): stdin for the command (>= v0.109). - RUN_COMMAND_SERVICE.EXTRA_WORKDIR (String): working directory (default: $HOME). - RUN_COMMAND_SERVICE.EXTRA_BACKGROUND (boolean): run in background (true) or foreground terminal session (false, default). - RUN_COMMAND_SERVICE.EXTRA_BACKGROUND_CUSTOM_LOG_LEVEL (String): log level for background commands (>= v0.118). - RUN_COMMAND_SERVICE.EXTRA_SESSION_ACTION (String): session action for foreground commands (default: switch to new session and open activity, >= v0.109). - RUN_COMMAND_SERVICE.EXTRA_COMMAND_LABEL (String): label for the command (>= v0.109). - RUN_COMMAND_SERVICE.EXTRA_COMMAND_DESCRIPTION (String, markdown): short description (>= v0.109). - RUN_COMMAND_SERVICE.EXTRA_COMMAND_HELP (String, markdown): help info, e.g., URL (>= v0.109). - RUN_COMMAND_SERVICE.EXTRA_PENDING_INTENT (Parcelable): for receiving results back (>= v0.109). - RUN_COMMAND_SERVICE.EXTRA_RESULT_DIRECTORY (String): directory for result files (>= v0.115). RUN_COMMAND_TERMINAL does not exist. To open a foreground terminal session, set RUN_COMMAND_SERVICE.EXTRA_BACKGROUND to false (default behavior). RUN_COMMAND_BACKGROUND is the correct key (boolean extra): true for background (no terminal UI), false for foreground terminal session.
Citations:
- 1: https://github.com/termux/termux-app/wiki/RUN_COMMAND-Intent
- 2: https://github.com/termux/termux-app/wiki/RUN_COMMAND-Intent/8c97bd22e01438155249db8eff04f5c8bad492a4
- 3: https://github.com/termux/termux-app/wiki/RUN_COMMAND-Intent/1c25701707222511d2c8ae84e994ec2cd938e542
🏁 Script executed:
find . -name "TermuxIntentLauncher.kt" -type fRepository: maazm7d/TermuxHub
Length of output: 133
🏁 Script executed:
# Once we find the file, let's examine the relevant lines
cd app/src/main/java/com/maazm7d/termuxhub/utils/ 2>/dev/null && cat -n TermuxIntentLauncher.kt | head -50Repository: maazm7d/TermuxHub
Length of output: 2268
🏁 Script executed:
# Also check the full file to see all usages
wc -l app/src/main/java/com/maazm7d/termuxhub/utils/TermuxIntentLauncher.kt 2>/dev/nullRepository: maazm7d/TermuxHub
Length of output: 134
Critical: RUN_COMMAND_TERMINAL is not a valid Termux extra — use RUN_COMMAND_BACKGROUND instead.
The Termux RUN_COMMAND intent contract exposes com.termux.RUN_COMMAND_BACKGROUND (boolean) to control terminal display; there is no com.termux.RUN_COMMAND_TERMINAL extra. The current code uses an invalid key that Termux silently ignores, so the "opens Termux window" comment only works by accident via default behavior.
To open a foreground terminal session, replace the constant and its usage:
- Line 16: Change
EXTRA_COMMAND_TERMINALtoEXTRA_COMMAND_BACKGROUNDwith value"com.termux.RUN_COMMAND_BACKGROUND" - Line 37: Change
putExtra(EXTRA_COMMAND_TERMINAL, true)toputExtra(EXTRA_COMMAND_BACKGROUND, false)and update the comment to clarify thatfalsemeans foreground session
This applies to the same pattern at lines 37–38.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/src/main/java/com/maazm7d/termuxhub/utils/TermuxIntentLauncher.kt` around
lines 16 - 17, The EXTRA_COMMAND_TERMINAL constant is using an invalid Termux
key; rename it to EXTRA_COMMAND_BACKGROUND and set its value to
"com.termux.RUN_COMMAND_BACKGROUND" (replace the existing EXTRA_COMMAND_TERMINAL
definition), then update any putExtra calls that use EXTRA_COMMAND_TERMINAL
(e.g., in the intent builder that currently does
putExtra(EXTRA_COMMAND_TERMINAL, true)) to use
putExtra(EXTRA_COMMAND_BACKGROUND, false) so the session opens in the
foreground; also update the inline comment near the putExtra call to state that
false means a foreground session.
- Added TermuxIntentLauncher utility. - Declared com.termux.permission.RUN_COMMAND in AndroidManifest.xml. - Added <queries> for com.termux detection. - Updated ToolDetails and ToolMapper with requireRoot. - Added InstallState and runInstall to ToolDetailViewModel. - Updated ToolDetailScreen with "Run" buttons, root warning chips, and a detailed two-step security setup dialog.
This change implements the TermuxLaunch feature, allowing users to execute installation commands directly in Termux with a single tap. It handles Termux detection, security permissions, and provides clear feedback to the user.
PR created automatically by Jules for task 8784499958352989684 started by @maazm7d
Summary by CodeRabbit