Skip to content

Commit fdc3275

Browse files
pporvatovintellij-monorepo-bot
authored andcommitted
IJPL-54591 Implement IDE theme sync with OS on Linux
- Fixed comments from CR (cherry picked from commit 115bea227f7bedd58fe401d8376b3ee7b05862c3) IJ-CR-192442 GitOrigin-RevId: 5ef849ccb84e2fa7056d36d138ebeeadc5115746
1 parent 4bc7f2f commit fdc3275

File tree

3 files changed

+70
-51
lines changed

3 files changed

+70
-51
lines changed

platform/platform-impl/src/com/intellij/ide/ui/laf/DBusSettingsMonitorService.kt

Lines changed: 41 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,27 @@
11
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
22
package com.intellij.ide.ui.laf
33

4+
import com.intellij.openapi.application.EDT
5+
import com.intellij.openapi.application.ModalityState
6+
import com.intellij.openapi.application.asContextElement
47
import com.intellij.openapi.components.Service
58
import com.intellij.openapi.diagnostic.thisLogger
69
import com.intellij.openapi.util.SystemInfoRt
710
import com.intellij.openapi.wm.impl.ExecResult
8-
import com.intellij.openapi.wm.impl.X11UiUtilKt
11+
import com.intellij.openapi.wm.impl.LinuxUiUtil
912
import com.intellij.openapi.wm.impl.output
1013
import com.intellij.util.concurrency.ThreadingAssertions
1114
import kotlinx.coroutines.CoroutineScope
15+
import kotlinx.coroutines.Dispatchers
16+
import kotlinx.coroutines.FlowPreview
17+
import kotlinx.coroutines.channels.BufferOverflow
1218
import kotlinx.coroutines.flow.MutableSharedFlow
1319
import kotlinx.coroutines.flow.MutableStateFlow
1420
import kotlinx.coroutines.flow.StateFlow
1521
import kotlinx.coroutines.flow.asStateFlow
1622
import kotlinx.coroutines.flow.debounce
1723
import kotlinx.coroutines.launch
24+
import kotlinx.coroutines.withContext
1825
import java.util.concurrent.atomic.AtomicReference
1926
import kotlin.time.Duration.Companion.seconds
2027

@@ -41,14 +48,13 @@ private val QUERY_COLOR_SCHEME = arrayOf(
4148
"string:org.freedesktop.appearance",
4249
"string:color-scheme")
4350

44-
@Suppress("OPT_IN_USAGE")
4551
@Service
4652
internal class DBusSettingsMonitorService(private val scope: CoroutineScope) {
4753

4854
private var LOG = thisLogger()
4955

5056
private val darkSchemeFlow = MutableStateFlow<Boolean?>(null)
51-
private val darkSchemeDebounceFlow = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
57+
private val darkSchemeDebounceFlow = MutableSharedFlow<Unit>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_LATEST)
5258
private var dbusMonitorProcess = AtomicReference<Process?>(null)
5359

5460
val isServiceAllowed: Boolean
@@ -62,6 +68,7 @@ internal class DBusSettingsMonitorService(private val scope: CoroutineScope) {
6268
darkSchemeFlow.value = calcDarkScheme()
6369

6470
try {
71+
@OptIn(FlowPreview::class)
6572
darkSchemeDebounceFlow.debounce(DEBOUNCE_DURATION).collect {
6673
darkSchemeFlow.value = calcDarkScheme()
6774
}
@@ -87,7 +94,7 @@ internal class DBusSettingsMonitorService(private val scope: CoroutineScope) {
8794
return
8895
}
8996

90-
scope.launch {
97+
scope.launch(Dispatchers.EDT + ModalityState.any().asContextElement()) {
9198
darkScheme.collect {
9299
listener(it ?: false)
93100
}
@@ -97,7 +104,7 @@ internal class DBusSettingsMonitorService(private val scope: CoroutineScope) {
97104
private fun calcDarkScheme(): Boolean? {
98105
ThreadingAssertions.assertBackgroundThread()
99106

100-
val output = X11UiUtilKt.exec("DBusSettingsMonitorService gets color scheme", *QUERY_COLOR_SCHEME).output() ?: return null
107+
val output = LinuxUiUtil.exec("DBusSettingsMonitorService gets color scheme", *QUERY_COLOR_SCHEME).output() ?: return null
101108

102109
val split = output.splitOutput()
103110
val value = split.lastOrNull()?.toIntOrNull()
@@ -124,36 +131,40 @@ internal class DBusSettingsMonitorService(private val scope: CoroutineScope) {
124131
return result
125132
}
126133

127-
private fun startDbusMonitorListener() {
128-
ThreadingAssertions.assertBackgroundThread()
129-
130-
if (X11UiUtilKt.exec("DBusSettingsMonitorService checks dbus-monitor", *CHECK_MONITOR_CMD) !is ExecResult.Success) {
131-
return
132-
}
134+
/**
135+
* The method starts a process and reads its output for the entire lifetime of the IDE.
136+
* It uses I/O operations and doesn't obey coroutine cancellation
137+
*/
138+
private suspend fun startDbusMonitorListener() {
139+
withContext(Dispatchers.IO) {
140+
if (LinuxUiUtil.exec("DBusSettingsMonitorService checks dbus-monitor", *CHECK_MONITOR_CMD) !is ExecResult.Success) {
141+
return@withContext
142+
}
133143

134-
try {
135-
dbusMonitorProcess.set(ProcessBuilder(*MONITOR_CMD).start())
136-
LOG.info("DBus listener started")
137-
}
138-
catch (e: Throwable) {
139-
LOG.info("DBus listener cannot start", e)
140-
}
144+
try {
145+
dbusMonitorProcess.set(ProcessBuilder(*MONITOR_CMD).start())
146+
LOG.info("DBus listener started")
147+
}
148+
catch (e: Throwable) {
149+
LOG.info("DBus listener cannot start", e)
150+
}
141151

142-
val process = dbusMonitorProcess.get() ?: return
152+
val process = dbusMonitorProcess.get() ?: return@withContext
143153

144-
try {
145-
process.inputStream.bufferedReader().forEachLine { line ->
146-
val split = line.splitOutput()
147-
if (split.size > 2 && split[split.size - 2] == SETTINGS_INTERFACE && split[split.size - 1] == SETTINGS_MEMBER) {
148-
LOG.info("SettingChanged received: $line")
149-
darkSchemeDebounceFlow.tryEmit(Unit)
154+
try {
155+
process.inputStream.bufferedReader().forEachLine { line ->
156+
val split = line.splitOutput()
157+
if (split.size > 2 && split[split.size - 2] == SETTINGS_INTERFACE && split[split.size - 1] == SETTINGS_MEMBER) {
158+
LOG.info("SettingChanged received: $line")
159+
check(darkSchemeDebounceFlow.tryEmit(Unit))
160+
}
150161
}
151-
}
152162

153-
LOG.info("DBus listener stopped: output ended unexpectedly (no errors)")
154-
}
155-
catch (e: Throwable) {
156-
LOG.info("DBus listener stopped", e)
163+
LOG.info("DBus listener stopped: output ended unexpectedly (no errors)")
164+
}
165+
catch (e: Throwable) {
166+
LOG.info("DBus listener stopped", e)
167+
}
157168
}
158169
}
159170

platform/platform-impl/src/com/intellij/openapi/wm/impl/X11UiUtilKt.kt renamed to platform/platform-impl/src/com/intellij/openapi/wm/impl/LinuxUiUtil.kt

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
22
package com.intellij.openapi.wm.impl
33

4+
import com.intellij.execution.ExecutionException
5+
import com.intellij.execution.configurations.GeneralCommandLine
6+
import com.intellij.execution.process.ProcessNotCreatedException
7+
import com.intellij.execution.util.ExecUtil
48
import com.intellij.openapi.diagnostic.logger
59
import com.intellij.util.concurrency.ThreadingAssertions
610
import org.jetbrains.annotations.ApiStatus
7-
import java.nio.charset.StandardCharsets
811
import java.util.concurrent.ConcurrentHashMap
9-
import java.util.concurrent.TimeUnit
1012

1113
@ApiStatus.Internal
1214
sealed interface ExecResult {
@@ -20,12 +22,16 @@ sealed interface ExecResult {
2022
}
2123

2224
@ApiStatus.Internal
23-
internal object X11UiUtilKt {
25+
object LinuxUiUtil {
2426

25-
private val LOG = logger<X11UiUtilKt>()
27+
private val LOG = logger<LinuxUiUtil>()
2628

2729
private val unsupportedCommands = ConcurrentHashMap<String, Boolean>()
2830

31+
/**
32+
* Executes the command and returns its output. If the command is unsupported by OS, returns [ExecResult.Failure] and doesn't
33+
* try to execute/log the problem anymore
34+
*/
2935
@JvmStatic
3036
fun exec(errorMessage: String, vararg command: String): ExecResult {
3137
ThreadingAssertions.assertBackgroundThread()
@@ -41,36 +47,38 @@ internal object X11UiUtilKt {
4147
}
4248

4349
try {
44-
val process = ProcessBuilder(*command).start()
45-
if (!process.waitFor(5, TimeUnit.SECONDS)) {
46-
LOG.info("$errorMessage: timeout")
47-
process.destroyForcibly()
48-
return ExecResult.Failure()
50+
val processOutput = ExecUtil.execAndGetOutput(GeneralCommandLine(*command), 5000)
51+
val exitCode = processOutput.exitCode
52+
if (exitCode != 0) {
53+
LOG.debug("$errorMessage: exit code $exitCode")
54+
return ExecResult.ExitValue(exitCode)
4955
}
5056

51-
if (process.exitValue() != 0) {
52-
LOG.info(errorMessage + ": exit code " + process.exitValue())
53-
return ExecResult.ExitValue(process.exitValue())
54-
}
55-
val output = process.inputReader(StandardCharsets.UTF_8).readText().trim { it <= ' ' }
57+
val output = processOutput.stdout.trim { it <= ' ' }
5658
return ExecResult.Success(output)
5759
}
58-
catch (e: Exception) {
59-
val exceptionMessage = e.message
60-
if (exceptionMessage?.contains("No such file or directory") == true) {
60+
catch (e: ExecutionException) {
61+
if (e is ProcessNotCreatedException && isNoFileOrDirectory(e.cause)) {
6162
unsupportedCommands[command[0]] = true
62-
LOG.info("$errorMessage: $exceptionMessage")
63+
LOG.info("$errorMessage: ${e.message}")
6364
LOG.trace(e)
6465
}
6566
else {
6667
LOG.info(errorMessage, e)
6768
}
68-
6969
return ExecResult.Failure()
7070
}
7171
}
7272
}
7373

74+
/**
75+
* There is no good API in jdk for such a check. The string comes from the JDK and is not localizable
76+
* (see os.cpp: `X(ENOENT, "No such file or directory")`), therefore, this solution should work on different OS-s and locales.
77+
*/
78+
private fun isNoFileOrDirectory(e: Throwable?): Boolean {
79+
return e?.message?.contains("No such file or directory") == true
80+
}
81+
7482
@ApiStatus.Internal
7583
fun ExecResult.output(): String? {
7684
return (this as? ExecResult.Success)?.output

platform/platform-impl/src/com/intellij/openapi/wm/impl/X11UiUtil.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -520,8 +520,8 @@ private static Field field(Class<?> aClass, String name) throws Exception {
520520
}
521521

522522
private static @Nullable String exec(String errorMessage, String... command) {
523-
var execResult = X11UiUtilKt.exec(errorMessage, command);
524-
return X11UiUtilKtKt.output(execResult);
523+
var execResult = LinuxUiUtil.exec(errorMessage, command);
524+
return LinuxUiUtilKt.output(execResult);
525525
}
526526

527527
private static List<String> grepFile(@SuppressWarnings("SameParameterValue") String errorMessage, Path file, Pattern pattern) {

0 commit comments

Comments
 (0)