From 031049dc93cb193981e26bc48fa612bb93db865b Mon Sep 17 00:00:00 2001 From: Alex Huszagh Date: Sat, 7 Sep 2024 17:48:04 -0500 Subject: [PATCH] Add improved support for macOS. --- .github/workflows/lint.yml | 2 +- .github/workflows/theme.yml | 2 +- example/breeze_theme.hpp | 125 +++++++++++++++++++++++++++++++++--- example/breeze_theme.py | 12 +++- 4 files changed, 128 insertions(+), 13 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6b30919..e1b52c8 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -46,7 +46,7 @@ jobs: lint-cpp: strategy: matrix: - os: [ubuntu-latest, windows-latest] # TODO: Restore macos-latest + os: [ubuntu-latest, windows-latest, macos-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/theme.yml b/.github/workflows/theme.yml index 5e3cf73..d40e58a 100644 --- a/.github/workflows/theme.yml +++ b/.github/workflows/theme.yml @@ -29,7 +29,7 @@ jobs: theme-cpp: strategy: matrix: - os: [ubuntu-latest] # TODO: Restore macos-latest + os: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 diff --git a/example/breeze_theme.hpp b/example/breeze_theme.hpp index f51b964..04603db 100644 --- a/example/breeze_theme.hpp +++ b/example/breeze_theme.hpp @@ -11,8 +11,7 @@ * This currently supports: * - Windows * - Linux - * - * Enhancements for macOS would be greatly appreciated. + * - macOS * * This is subject to the following license terms: * @@ -96,14 +95,17 @@ #include #include +#include #include #include #include #include +#include #include #include #include #include +#include #include #include @@ -156,6 +158,8 @@ namespace breeze_stylesheets start = end + 1; end = value.find(delimiter, start); } + if (start != end) + result.emplace_back(value.substr(start, end - start)); return result; } @@ -169,6 +173,41 @@ namespace breeze_stylesheets return (c <= 'Z' && c >= 'A') ? c - ('Z' - 'z') : c; } + /// @brief Trim from the start (in place). + /// @param s The string to trim. + inline void + _ltrim(::std::string &s) + { + s.erase(s.begin(), ::std::find_if(s.begin(), s.end(), [](unsigned char ch) { return !::std::isspace(ch); })); + } + + /// @brief Trim from the end (in place). + /// @param s The string to trim. + inline void + _rtrim(::std::string &s) + { + s.erase(::std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) { return !::std::isspace(ch); }).base(), s.end()); + } + + /// @brief Trim from both ends (in place). + /// @param s The string to trim. + inline void + _trim(::std::string &s) + { + _rtrim(s); + _ltrim(s); + } + + /// @brief Check if a string contains a substring. + /// @param value The full string to search. + /// @param substr The substring to search for. + /// @return If value contains substr. + inline bool + _contains(const ::std::string &value, const ::std::string &substr) + { + return value.find(substr) != ::std::string::npos; + } + #if __APPLE__ || __linux__ /// @brief Determine if the file is an executable. @@ -362,7 +401,7 @@ namespace breeze_stylesheets return ::std::make_tuple( int32_t(major), int32_t(minor), - std::stoi(build), + ::std::stoi(build), int32_t(platformId), int32_t(0), int32_t(0) @@ -408,10 +447,81 @@ namespace breeze_stylesheets } #elif __APPLE__ - # error "macOS not yet supported." - #elif __linux__ - /// @brief Get the current system theme. This requires Windows 10+. + /// @brief Get if the macOS version supports theme detection. + /// @return If the macOS version supports theme detection. + inline bool + _macos_supported_version() + { + // This gives an output similar to: + // ProductName: Mac OS X + // ProductVersion: 10.12.5 + // BuildVersion: 16F73 + auto [stdout, code] = _run_command("sw_vers r"); + if (code != EXIT_SUCCESS) + return false; + auto lines = _split(stdout.value(), '\n'); + const ::std::string version_key = "ProductVersion:"; + for (auto &line : lines) + { + // find if we have a matching line and get the right value. + if (line.rfind(version_key, 0) != 0) + continue; + auto version = line.substr(version_key.length()); + _trim(version); + auto parts = _split(version, '.'); + auto major = ::std::stoi(parts.at(0)); + auto minor = ::std::stoi(parts.at(1)); + if (major < 10) + return false; + if (major >= 11) + return true; + return minor >= 14; + } + return false; + } + + /// @brief Get the current system theme. + /// @return The type of the system theme. + inline ::breeze_stylesheets::theme + get_theme() + { + // old macOS versions were always light + if (!_macos_supported_version()) + return ::breeze_stylesheets::theme::light; + + // we could technically work through the Obj-C API but this is + // a lot of work for a machine I can't test on when we can directly + // get the results anyway via popen. It's way easier, no worries about + // segfaults. there's no good way to capture stderr so we just redirect + // it to stdout, knowing we get stdout on success (dark) and stderr + // otherwise. + auto [stdout, code] = _run_command("defaults read -globalDomain AppleInterfaceStyle 2>&1"); + + // if we had an error getting our response, leave early. + if (!stdout.has_value()) + return ::breeze_stylesheets::theme::unknown; + + // had a successful response. normally this only occurs on a dark theme + auto output = stdout.value(); + _trim(output); + if (code == EXIT_SUCCESS) + return output == "Dark" ? ::breeze_stylesheets::theme::dark : ::breeze_stylesheets::theme::light; + + // if we've had an error, it's generally because the key pair wasn't set. + auto not_exist = _contains(output, "does not exist"); + auto any_app = _contains(output, "kCFPreferencesAnyApplication"); + auto interface_style = _contains(output, "AppleInterfaceStyle"); + if (not_exist && any_app && interface_style) + return ::breeze_stylesheets::theme::light; + + // no idea, fall back to unknown + return ::breeze_stylesheets::theme::unknown; + } + +#elif __linux__ + + /// @brief Get the current system theme. /// @return The type of the system theme. inline ::breeze_stylesheets::theme get_theme() @@ -439,8 +549,7 @@ namespace breeze_stylesheets auto value = stdout.value(); ::std::transform(value.begin(), value.end(), value.begin(), _to_ascii_lowercase); - auto is_dark = value.find("-dark") != ::std::string::npos; - return is_dark ? ::breeze_stylesheets::theme::dark : ::breeze_stylesheets::theme::light; + return _contains(value, "-dark") ? ::breeze_stylesheets::theme::dark : ::breeze_stylesheets::theme::light; } #else diff --git a/example/breeze_theme.py b/example/breeze_theme.py index 817af58..653c41e 100644 --- a/example/breeze_theme.py +++ b/example/breeze_theme.py @@ -273,7 +273,7 @@ def _initialize_advapi32() -> ctypes.CDLL: # region macos -def macos_supported_version() -> bool: +def _macos_supported_version() -> bool: '''Determine if we use a support macOS version.''' # NOTE: This is typically 10.14.2 or 12.3 @@ -292,8 +292,14 @@ def macos_supported_version() -> bool: def _get_theme_macos() -> Theme: '''Get the current theme, as light or dark, for the system on macOS.''' + # old macOS versions were always light + if not _macos_supported_version(): + return Theme.LIGHT + # NOTE: This can segfault on M1 and M2 Macs on Big Sur 11.4+. So, we also - # try reading directly using subprocess. + # try reading directly using subprocess. Specifically, it's documented that + # if dark mode is set, this command returns `Dark`, otherwise it returns + # that the key pair doesn't exist. try: command = ['defaults', 'read', '-globalDomain', 'AppleInterfaceStyle'] process = subprocess.run(command, capture_output=True, check=True) @@ -538,7 +544,7 @@ def listener(callback: CallbackFn) -> None: def register_functions() -> tuple[ThemeFn, ListenerFn]: '''Register our global functions for our themes and listeners.''' - if sys.platform == 'darwin' and macos_supported_version(): + if sys.platform == 'darwin': return (_get_theme_macos, _listener_macos) if sys.platform == 'win32' and platform.release().isdigit() and int(platform.release()) >= 10: # Checks if running Windows 10 version 10.0.14393 (Anniversary Update) OR HIGHER.