Skip to content

Implement TermuxLaunch feature#101

Closed
maazm7d wants to merge 2 commits into
mainfrom
feature/termux-launch-8784499958352989684
Closed

Implement TermuxLaunch feature#101
maazm7d wants to merge 2 commits into
mainfrom
feature/termux-launch-8784499958352989684

Conversation

@maazm7d

@maazm7d maazm7d commented Apr 24, 2026

Copy link
Copy Markdown
Owner

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

  • New Features
    • Tools now display root permission warnings when applicable.
    • Users can execute installation commands directly via Termux integration.
    • Improved install feedback with alerts for errors, security issues, and missing Termux.

- 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.
@google-labs-jules

Copy link
Copy Markdown

👋 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 @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@coderabbitai

coderabbitai Bot commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@maazm7d has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 33 minutes and 56 seconds before requesting another review.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3561c261-b7dd-48e7-992c-9206b5883a80

📥 Commits

Reviewing files that changed from the base of the PR and between 8e56cff and cf6b7d3.

📒 Files selected for processing (2)
  • app/src/main/AndroidManifest.xml
  • app/src/main/java/com/maazm7d/termuxhub/ui/screens/details/ToolDetailScreen.kt
📝 Walkthrough

Walkthrough

This pull request introduces functionality to execute tool installation commands directly within Termux from the app. It adds a requireRoot field to the ToolDetails model, implements install state management via a StateFlow, creates a TermuxIntentLauncher utility for Termux integration, and updates the detail screen UI to support command execution with feedback via snackbars and dialogs.

Changes

Cohort / File(s) Summary
Termux Integration
app/src/main/AndroidManifest.xml, app/src/main/java/com/maazm7d/termuxhub/utils/TermuxIntentLauncher.kt
Adds package visibility queries for com.termux and introduces a new singleton utility that detects Termux installation and launches commands via Termux intents with support for bash, working directory, and API level differentiation (foreground vs. regular service start).
Data Model Extension
app/src/main/java/com/maazm7d/termuxhub/domain/model/Tool.kt, app/src/main/java/com/maazm7d/termuxhub/data/mapper/ToolMapper.kt
Extends ToolDetails with a new requireRoot boolean field and updates the mapper to propagate this field from the entity layer.
Detail Screen Features
app/src/main/java/com/maazm7d/termuxhub/ui/screens/details/ToolDetailScreen.kt, app/src/main/java/com/maazm7d/termuxhub/ui/screens/details/ToolDetailViewModel.kt, app/src/main/java/com/maazm7d/termuxhub/ui/screens/details/components/InstallCommandRow.kt
Restructures the detail screen with install state management (via InstallState sealed class and StateFlow), adds contextual snackbar/dialog feedback for launch outcomes, displays a warning banner when root is required, and integrates the run action into command rows with proper state reset handling.

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 Commands now leap with Termux's might,
Root warnings shimmer, banners bright,
Intent launchers bound and true,
Install flows dance in StateFlow's hue,
One click, one service, all feels right! 🚀

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: implementing a feature to launch/execute commands via Termux. It directly corresponds to the PR's primary objective.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/termux-launch-8784499958352989684

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 of StateFlow for install outcomes.

InstallState is being used as an event (show snackbar / show dialog once), but MutableStateFlow deduplicates equal consecutive values, and the state persists until resetInstallState() is called. That combination causes subtle UX bugs (see the corresponding comment in ToolDetailScreen.kt). Channel<InstallEvent>(Channel.BUFFERED).receiveAsFlow() — or MutableSharedFlow<InstallEvent>(extraBufferCapacity = 1) — is a better fit and also removes the need for resetInstallState().

♻️ 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; drop resetInstallState(). The InstallState sealed class can be renamed to InstallEvent and lose Idle.

🤖 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

📥 Commits

Reviewing files that changed from the base of the PR and between 718a7d7 and 8e56cff.

📒 Files selected for processing (7)
  • app/src/main/AndroidManifest.xml
  • app/src/main/java/com/maazm7d/termuxhub/data/mapper/ToolMapper.kt
  • app/src/main/java/com/maazm7d/termuxhub/domain/model/Tool.kt
  • app/src/main/java/com/maazm7d/termuxhub/ui/screens/details/ToolDetailScreen.kt
  • app/src/main/java/com/maazm7d/termuxhub/ui/screens/details/ToolDetailViewModel.kt
  • app/src/main/java/com/maazm7d/termuxhub/ui/screens/details/components/InstallCommandRow.kt
  • app/src/main/java/com/maazm7d/termuxhub/utils/TermuxIntentLauncher.kt

Comment on lines 6 to +11
<uses-permission android:name="android.permission.INTERNET" />

<queries>
<package android:name="com.termux" />
</queries>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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:


🏁 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/null

Repository: 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 -100

Repository: maazm7d/TermuxHub

Length of output: 88


🏁 Script executed:

# Look for startService calls in the codebase
rg "startService" --type kt --type java -B2 -A2 | head -50

Repository: 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.kt

Repository: maazm7d/TermuxHub

Length of output: 2268


🏁 Script executed:

# Read the AndroidManifest.xml to verify current state
cat -n app/src/main/AndroidManifest.xml

Repository: 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.

Comment on lines +58 to +78
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 -> {}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

Comment on lines +80 to +101
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")
}
}
)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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:

  1. Missing <uses-permission android:name="com.termux.permission.RUN_COMMAND" /> in this app's manifest (see AndroidManifest.xml comment — Android blocks the intent before Termux ever sees it).
  2. 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.

Comment on lines 283 to 291
tool.installCommands
.lines()
.filter { it.isNotBlank() }
.forEach { cmd ->
InstallCommandRow(command = cmd)
InstallCommandRow(
command = cmd,
onRunClick = { viewModel.runInstall(context, tool) }
)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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 cmd into runInstall and 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.

Comment on lines +53 to +74
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")
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Minor: edge cases in runInstall.

  • If tool.installCommands is blank or contains only blank lines, commands becomes "" and the code still launches bash -c "" and reports Launched. Cheap guard: if commands.isBlank() → set InstallState.Error("No install commands") and return.
  • Joining by && means a trailing # comment on any earlier line consumes the following joined commands. Unlikely for typical pkg install X lines but worth noting for scripts with comments.
  • runInstall executes PackageManager.getPackageInfo and startForegroundService on the caller thread (main). Consider viewModelScope.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.

Suggested change
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.

Comment on lines +16 to +17
private const val EXTRA_COMMAND_TERMINAL = "$TERMUX_PACKAGE.RUN_COMMAND_TERMINAL"
private const val EXTRA_COMMAND_SESSION_ACTION = "$TERMUX_PACKAGE.RUN_COMMAND_SESSION_ACTION"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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:


🏁 Script executed:

find . -name "TermuxIntentLauncher.kt" -type f

Repository: 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 -50

Repository: 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/null

Repository: 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_TERMINAL to EXTRA_COMMAND_BACKGROUND with value "com.termux.RUN_COMMAND_BACKGROUND"
  • Line 37: Change putExtra(EXTRA_COMMAND_TERMINAL, true) to putExtra(EXTRA_COMMAND_BACKGROUND, false) and update the comment to clarify that false means 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.
@maazm7d maazm7d closed this Apr 26, 2026
@maazm7d maazm7d deleted the feature/termux-launch-8784499958352989684 branch May 5, 2026 07:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant