System Integration: Clipboard, Commands & Notifications | Kotlin Desktop #18
Video: System Integration: Clipboard, Commands & Notifications | Kotlin Desktop #18 by Taught by Celeste AI - AI Coding Coach
Watch full page →System Integration: Clipboard, Commands & Notifications in Kotlin Desktop
This tutorial demonstrates how to leverage JVM system APIs within a Kotlin Compose Desktop app by building a System Toolbox featuring clipboard monitoring, shell command execution, and system information display. It covers real-time clipboard polling, running commands with timeout control, and integrating native notifications and navigation UI components.
Code
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ContentPaste
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Terminal
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.awt.Toolkit
import java.awt.datatransfer.DataFlavor
import java.awt.datatransfer.StringSelection
import java.lang.ProcessBuilder
import kotlin.system.exitProcess
@Composable
@Preview
fun SystemToolboxApp() {
val clipboard = Toolkit.getDefaultToolkit().systemClipboard
val scope = rememberCoroutineScope()
// Clipboard state and monitoring
var clipboardText by remember { mutableStateOf("") }
LaunchedEffect(Unit) {
var lastText = ""
while (true) {
val contents = clipboard.getContents(null)
val text = if (contents != null && contents.isDataFlavorSupported(DataFlavor.stringFlavor)) {
contents.getTransferData(DataFlavor.stringFlavor) as String
} else ""
if (text != lastText) {
clipboardText = text
lastText = text
}
delay(500) // Poll clipboard every 500ms
}
}
// Command execution state
var commandInput by remember { mutableStateOf("") }
var commandOutput by remember { mutableStateOf("") }
var isRunning by remember { mutableStateOf(false) }
fun runCommand(command: String) {
if (command.isBlank()) return
isRunning = true
commandOutput = ""
scope.launch {
try {
val process = ProcessBuilder("/bin/sh", "-c", command).start()
if (!process.waitFor(5, java.util.concurrent.TimeUnit.SECONDS)) {
process.destroyForcibly()
commandOutput = "Command timed out."
} else {
val output = process.inputStream.bufferedReader().readText()
commandOutput = output.ifBlank { "No output." }
}
} catch (e: Exception) {
commandOutput = "Error: ${e.message}"
} finally {
isRunning = false
}
}
}
// UI tabs state
var selectedTab by remember { mutableStateOf(0) }
Scaffold(
topBar = {
SmallTopAppBar(title = { Text("System Toolbox") })
}
) { padding ->
Row(modifier = Modifier.padding(padding).fillMaxSize()) {
NavigationRail {
NavigationRailItem(
selected = selectedTab == 0,
onClick = { selectedTab = 0 },
icon = { Icon(Icons.Default.ContentPaste, contentDescription = "Clipboard") },
label = { Text("Clipboard") }
)
NavigationRailItem(
selected = selectedTab == 1,
onClick = { selectedTab = 1 },
icon = { Icon(Icons.Default.Terminal, contentDescription = "Command") },
label = { Text("Command") }
)
NavigationRailItem(
selected = selectedTab == 2,
onClick = { selectedTab = 2 },
icon = { Icon(Icons.Default.Info, contentDescription = "System") },
label = { Text("System") }
)
}
Spacer(Modifier.width(16.dp))
when (selectedTab) {
0 -> ClipboardTab(clipboardText, clipboard)
1 -> CommandTab(commandInput, onCommandChange = { commandInput = it }, commandOutput, isRunning, ::runCommand)
2 -> SystemTab()
}
}
}
}
@Composable
fun ClipboardTab(clipboardText: String, clipboard: java.awt.datatransfer.Clipboard) {
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
Text("Clipboard History (latest):", style = MaterialTheme.typography.titleMedium)
Spacer(Modifier.height(8.dp))
Text(clipboardText.ifBlank { "(empty)" })
Spacer(Modifier.height(16.dp))
Button(onClick = {
val selection = StringSelection("Sample text copied from app")
clipboard.setContents(selection, null)
}) {
Text("Copy Sample Text")
}
}
}
@Composable
fun CommandTab(
commandInput: String,
onCommandChange: (String) -> Unit,
commandOutput: String,
isRunning: Boolean,
runCommand: (String) -> Unit
) {
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
Text("Run Shell Command:", style = MaterialTheme.typography.titleMedium)
Spacer(Modifier.height(8.dp))
OutlinedTextField(
value = commandInput,
onValueChange = onCommandChange,
label = { Text("Command") },
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.height(8.dp))
Button(
onClick = { runCommand(commandInput) },
enabled = !isRunning
) {
Text(if (isRunning) "Running..." else "Run")
}
Spacer(Modifier.height(16.dp))
Text("Output:", style = MaterialTheme.typography.titleSmall)
Spacer(Modifier.height(8.dp))
Surface(
modifier = Modifier.fillMaxWidth().weight(1f),
tonalElevation = 2.dp,
shape = MaterialTheme.shapes.medium
) {
Text(commandOutput, modifier = Modifier.padding(8.dp))
}
}
}
@Composable
fun SystemTab() {
val osName = System.getProperty("os.name")
val osArch = System.getProperty("os.arch")
val javaVersion = System.getProperty("java.version")
val envVars = System.getenv().toList().sortedBy { it.first }
var filter by remember { mutableStateOf("") }
val filteredEnv = remember(filter) {
envVars.filter { it.first.contains(filter, ignoreCase = true) || it.second.contains(filter, ignoreCase = true) }
}
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
Text("System Information:", style = MaterialTheme.typography.titleMedium)
Spacer(Modifier.height(8.dp))
Text("OS: $osName ($osArch)")
Text("Java Version: $javaVersion")
Spacer(Modifier.height(16.dp))
OutlinedTextField(
value = filter,
onValueChange = { filter = it },
label = { Text("Filter Environment Variables") },
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.height(8.dp))
Surface(
modifier = Modifier.fillMaxWidth().weight(1f),
tonalElevation = 2.dp,
shape = MaterialTheme.shapes.medium
) {
Column(modifier = Modifier.padding(8.dp)) {
filteredEnv.forEach { (key, value) ->
Text("$key = $value")
}
}
}
}
}
Key Points
- Polling the system clipboard with AWT Toolkit and coroutines provides reliable real-time clipboard monitoring.
- Shell commands can be executed safely using ProcessBuilder with timeout and forced termination handling.
- NavigationRail enables clean vertical tab navigation with Material icons for a desktop UI.
- System properties and environment variables are accessible via System.getProperty() and System.getenv().
- Compose Desktop supports native system tray integration and notifications for enhanced user interaction.