diff --git a/core/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiOutput.java b/core/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiOutput.java index 22f7afffd107..8a06c59e3fdc 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiOutput.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiOutput.java @@ -31,6 +31,7 @@ * * @author Phillip Webb * @author Yong-Hyun Kim + * @author Philemon Hilscher * @since 1.0.0 */ public abstract class AnsiOutput { @@ -43,8 +44,6 @@ public abstract class AnsiOutput { private static @Nullable Boolean ansiCapable; - private static final String OPERATING_SYSTEM_NAME = System.getProperty("os.name").toLowerCase(Locale.ENGLISH); - private static final String ENCODE_START = "\033["; private static final String ENCODE_END = "m"; @@ -171,13 +170,35 @@ private static boolean detectIfAnsiCapable() { } } } - return !(OPERATING_SYSTEM_NAME.contains("win")); + if (isWindows(System.getProperty("os.name"))) { + return isWindowsAnsiCapable(System.getProperty("os.version")); + } + return true; } catch (Throwable ex) { return false; } } + static boolean isWindows(String osName) { + return osName.toLowerCase(Locale.ENGLISH).contains("win"); + } + + static boolean isWindowsAnsiCapable(String osVersion) { + String[] parts = osVersion.split("\\."); + if (parts.length >= 2) { + try { + int major = Integer.parseInt(parts[0]); + int minor = Integer.parseInt(parts[1]); + // ANSI support on Windows 10 = 10.0, Build 10586+ + return (major > 10) || (major == 10 && minor >= 0); + } catch (NumberFormatException ex) { + return false; + } + } + return false; + } + /** * Possible values to pass to {@link AnsiOutput#setEnabled}. Determines when to output * ANSI escape sequences for coloring application output. diff --git a/core/spring-boot/src/test/java/org/springframework/boot/ansi/AnsiOutputTests.java b/core/spring-boot/src/test/java/org/springframework/boot/ansi/AnsiOutputTests.java index a89c3ece113d..652bbec800f9 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/ansi/AnsiOutputTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/ansi/AnsiOutputTests.java @@ -16,18 +16,25 @@ package org.springframework.boot.ansi; +import java.util.stream.Stream; + import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.boot.ansi.AnsiOutput.Enabled; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; /** * Tests for {@link AnsiOutput}. * * @author Phillip Webb + * @author Philemon Hilscher */ class AnsiOutputTests { @@ -48,4 +55,42 @@ void encoding() { assertThat(encoded).isEqualTo("ABDEF"); } + private static Stream provideOsNames() { + return Stream.of( + Arguments.of("", false), + Arguments.of("Windows 7", true), + Arguments.of("Windows 8", true), + Arguments.of("Windows 8.1", true), + Arguments.of("Windows 10", true), + Arguments.of("Windows 11", true), + Arguments.of("Linux", false), + Arguments.of("Mac OS X", false), + Arguments.of("Mac OS", false) + ); + } + + @ParameterizedTest + @MethodSource("provideOsNames") + void testDetectIfIsWindows(String osName, boolean expected) { + boolean actual = AnsiOutput.isWindows(osName); + assertEquals(expected, actual); + } + + private static Stream provideOsVersionNumbers() { + return Stream.of( + Arguments.of("", false), + Arguments.of("6.1", false), // Windows 7 / Server 2008 R2 + Arguments.of("6.2", false), // Windows 8 / Server 2012 + Arguments.of("6.3", false), // Windows 8.1 / Server 2012 R2 + Arguments.of("10.0", true) // Windows 10 / 11 / Server 2016+ + ); + } + + @ParameterizedTest + @MethodSource("provideOsVersionNumbers") + void testDetectIfIsWindowsAnsiCapable(String osVersion, boolean expected) { + boolean actual = AnsiOutput.isWindowsAnsiCapable(osVersion); + assertEquals(expected, actual); + } + }