[network/kdeconnect-android] src/main/java/org/kde/kdeconnect/plugins/runcommand: Allow seeing the output of multiple commands at once
Albert Vaca Cintora <[email protected]> Tue, 4 Aug 2026 12:57:04 +0000 (UTC)
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit c8a59f0d6f2d34911abac4a96ff07ab493abf0af by Albert Vaca Cintora.
Committed on 04/08/2026 at 12:56.
Pushed by albertvaka into branch 'master'.
Allow seeing the output of multiple commands at once
M +22 -2 src/main/java/org/kde/kdeconnect/plugins/runcommand/RunCommandActivity.kt
M +49 -41 src/main/java/org/kde/kdeconnect/plugins/runcommand/RunCommandPlugin.java
M +6 -1 src/main/java/org/kde/kdeconnect/plugins/runcommand/RunCommandScreen.kt
https://invent.kde.org/network/kdeconnect-android/-/commit/c8a59f0d6f2d34911abac4a96ff07ab493abf0af
diff --git a/src/main/java/org/kde/kdeconnect/plugins/runcommand/RunCommandActivity.kt b/src/main/java/org/kde/kdeconnect/plugins/runcommand/RunCommandActivity.kt
index ee9f2297c..d46dae4e2 100644
--- a/src/main/java/org/kde/kdeconnect/plugins/runcommand/RunCommandActivity.kt
+++ b/src/main/java/org/kde/kdeconnect/plugins/runcommand/RunCommandActivity.kt
@@ -13,7 +13,9 @@ import android.util.Log
import android.widget.Toast
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.Clipboard
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.toClipEntry
@@ -118,7 +120,25 @@ class RunCommandActivity : AppCompatActivity() {
}
}
+/**
+ * Kotlin's `mutableStateOf` isn't directly callable from Java (its default arguments aren't
+ * exposed as Java overloads), so this is a small wrapper to be called from Java.
+ */
+fun mutableBooleanStateFor(initial: Boolean): MutableState<Boolean> = mutableStateOf(initial)
+
+enum class RunCommandStatus {
+ STDERR,
+ STDOUT,
+ COMMAND_RUNNING,
+ COMMAND_SUCCESSFUL,
+ COMMAND_FAILED,
+}
+
data class RunCommandOutput(
- var string: String,
+ val commandStatus: RunCommandStatus,
+ val string: String,
+ val id: Int,
+) {
val isCommand: Boolean
-)
\ No newline at end of file
+ get() = commandStatus == RunCommandStatus.COMMAND_RUNNING || commandStatus == RunCommandStatus.COMMAND_SUCCESSFUL || commandStatus == RunCommandStatus.COMMAND_FAILED
+}
\ No newline at end of file
diff --git a/src/main/java/org/kde/kdeconnect/plugins/runcommand/RunCommandPlugin.java b/src/main/java/org/kde/kdeconnect/plugins/runcommand/RunCommandPlugin.java
index 3eb57fabe..593015cbf 100644
--- a/src/main/java/org/kde/kdeconnect/plugins/runcommand/RunCommandPlugin.java
+++ b/src/main/java/org/kde/kdeconnect/plugins/runcommand/RunCommandPlugin.java
@@ -35,12 +35,13 @@ import org.kde.kdeconnect_tp.R;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
import java.util.List;
-import java.util.Objects;
-import java.util.stream.Collectors;
+import java.util.Map;
+import java.util.Set;
import kotlin.Unit;
-import kotlin.jvm.functions.Function1;
@PluginFactory.LoadablePlugin
public class RunCommandPlugin extends Plugin {
@@ -55,6 +56,13 @@ public class RunCommandPlugin extends Plugin {
private final ArrayList<CommandEntry> commandItems = new ArrayList<>();
private final SnapshotStateList<RunCommandOutput> output = new SnapshotStateList<>();
+ // Ids of the commands that are currently running on the remote device. Currently only
+ // used to show/hide the stop button (when this is empty).
+ private final Set<Integer> runningProcesses = new LinkedHashSet<>();
+ // Position in `output` of the "$ command" line for each running/finished command id, so
+ // we can go back and update its color once we know whether it succeeded or failed.
+ private final Map<Integer, Integer> commandLineIndexById = new HashMap<>();
+
private SharedPreferences sharedPreferences;
private boolean canAddCommand;
@@ -70,31 +78,8 @@ public class RunCommandPlugin extends Plugin {
void update();
}
- public MutableState<Boolean> commandRunning = new MutableState<>() {
- private boolean value = false;
-
- @Override
- public Boolean getValue() {
- return value;
- }
-
- @Override
- public void setValue(Boolean aBoolean) {
- value = aBoolean;
- }
-
- // You need to override these, but they are not being used
- @Override
- public Boolean component1() {
- return null;
- }
-
- @NonNull
- @Override
- public Function1<Boolean, Unit> component2() {
- return null;
- }
- };
+ // Replace with mutableStateOf when we migrate this class to Kotlin.
+ public final MutableState<Boolean> commandRunning = RunCommandActivityKt.mutableBooleanStateFor(false);
public SnapshotStateList<RunCommandOutput> getOutput() {
return output;
@@ -197,33 +182,57 @@ public class RunCommandPlugin extends Plugin {
canAddCommand = np.getBoolean("canAddCommand", false);
return true;
- } else if (np.has("stdout")) {
+ } else if (np.has("commandStarted")) {
+ int id = np.getInt("id");
+ String command = np.getString("command");
+
+ Log.i("RunCommandPlugin", "commandStarted " + id);
+
+ runningProcesses.add(id);
+ commandRunning.setValue(true);
+
+ output.add(new RunCommandOutput(RunCommandStatus.COMMAND_RUNNING, "$ " + command, id));
+ commandLineIndexById.put(id, output.size() - 1);
+
+ return true;
+ } else if (np.has("commandOutput")) {
List<String> stdOut = np.getStringList("stdout");
List<String> stdErr = np.getStringList("stderr");
+ int id = np.getInt("id");
assert stdOut != null;
assert stdErr != null;
for (String line : stdOut) {
Log.d("STDOUT", "Line:" + line);
- output.add(new RunCommandOutput(line, false));
+ output.add(new RunCommandOutput(RunCommandStatus.STDOUT, line, id));
}
for (String line : stdErr) {
Log.d("STDERR", "Line:" + line);
- output.add(new RunCommandOutput(line, false));
+ output.add(new RunCommandOutput(RunCommandStatus.STDERR, line, id));
}
return true;
} else if (np.has("commandFinished")) {
- commandRunning.setValue(false);
-
- RunCommandOutput newCommand = new RunCommandOutput(">", true);
- if (Objects.equals(output.get(output.size() - 1), newCommand)) {
- return true;
+ int id = np.getInt("id");
+ boolean success = np.getBoolean("success", true);
+
+ Log.i("RunCommandPlugin", "commandFinished " + id);
+ runningProcesses.remove(id);
+ commandRunning.setValue(!runningProcesses.isEmpty());
+
+ Integer index = commandLineIndexById.remove(id);
+ if (index != null && index < output.size()) {
+ RunCommandOutput commandLine = output.get(index);
+ // Build a brand-new instance instead of mutating commandLine in place: SnapshotStateList
+ // doesn't notify Compose when calling output.set() with the same (mutated) reference.
+ // When we migrate this class to Kotlin this can be simplified to commandLine.copy() where
+ // we only change its commandStatus.
+ output.set(index, new RunCommandOutput(
+ success ? RunCommandStatus.COMMAND_SUCCESSFUL : RunCommandStatus.COMMAND_FAILED,
+ commandLine.getString(),
+ id
+ ));
}
- output.removeAll(output.stream().filter(output -> output.getString().equals(">")).collect(Collectors.toList()));
-
- output.add(newCommand);
-
return true;
}
return false;
@@ -244,7 +253,6 @@ public class RunCommandPlugin extends Plugin {
NetworkPacket np = new NetworkPacket(PACKET_TYPE_RUNCOMMAND_REQUEST);
np.set("key", cmdKey);
getDevice().sendPacket(np);
- commandRunning.setValue(true);
}
private void requestCommandList() {
diff --git a/src/main/java/org/kde/kdeconnect/plugins/runcommand/RunCommandScreen.kt b/src/main/java/org/kde/kdeconnect/plugins/runcommand/RunCommandScreen.kt
index a8e536cfc..8c9e0f1f2 100644
--- a/src/main/java/org/kde/kdeconnect/plugins/runcommand/RunCommandScreen.kt
+++ b/src/main/java/org/kde/kdeconnect/plugins/runcommand/RunCommandScreen.kt
@@ -260,7 +260,12 @@ private fun OutputCard(
.fillMaxWidth(),
text = text.string,
style = MaterialTheme.typography.bodyLarge,
- fontWeight = if (text.isCommand) FontWeight.ExtraBold else FontWeight.Normal
+ fontWeight = if (text.isCommand) FontWeight.ExtraBold else FontWeight.Normal,
+ color = when (text.commandStatus) {
+ RunCommandStatus.COMMAND_SUCCESSFUL -> Color(0xFF2E7D32)
+ RunCommandStatus.COMMAND_FAILED -> MaterialTheme.colorScheme.error
+ else -> Color.Unspecified
+ }
)
}
}