Back to Blog

System Integration: Clipboard, Commands & Notifications | Kotlin Desktop #18

Sandy LaneSandy Lane

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.