diff --git a/CMakeLists.txt b/CMakeLists.txt index 9efe3ad..6302169 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,7 +32,7 @@ set_target_properties(izo PROPERTIES if(WIN32) target_compile_definitions(izo PRIVATE UNICODE _UNICODE WIN32_LEAN_AND_MEAN NOMINMAX) - target_link_libraries(izo PRIVATE comdlg32 ole32 shell32 uuid user32) + target_link_libraries(izo PRIVATE advapi32 comdlg32 ole32 shell32 uuid user32) elseif(UNIX AND NOT APPLE) # No link-time desktop dependency. The Linux backend discovers zenity or # kdialog at runtime so applications do not inherit GTK or Qt dependencies. diff --git a/include/izo/Clipboard.hpp b/include/izo/Clipboard.hpp index 6a3a350..c6d3faa 100644 --- a/include/izo/Clipboard.hpp +++ b/include/izo/Clipboard.hpp @@ -1,12 +1,29 @@ #pragma once +#include +#include #include #include #include +#include namespace izo { bool SetClipboardText(std::string_view text, std::string* errorMessage = nullptr); std::optional GetClipboardText(std::string* errorMessage = nullptr); +bool SetClipboardFiles(const std::vector& paths, + std::string* errorMessage = nullptr); +std::optional> GetClipboardFiles( + std::string* errorMessage = nullptr); + +struct ClipboardImage { + std::uint32_t width = 0; + std::uint32_t height = 0; + // Row-major, top-to-bottom RGBA8 pixels. Size must equal width * height * 4. + std::vector pixels; +}; + +bool SetClipboardImage(const ClipboardImage& image, std::string* errorMessage = nullptr); + } // namespace izo diff --git a/include/izo/Notifications.hpp b/include/izo/Notifications.hpp new file mode 100644 index 0000000..8ae0b15 --- /dev/null +++ b/include/izo/Notifications.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include +#include + +namespace izo { + +// Shows a non-blocking desktop notification. +bool ShowNotification(std::string_view title, std::string_view message, + std::string* errorMessage = nullptr); + +} // namespace izo diff --git a/include/izo/System.hpp b/include/izo/System.hpp index 0abe58e..fa3a167 100644 --- a/include/izo/System.hpp +++ b/include/izo/System.hpp @@ -13,6 +13,26 @@ struct SystemInfo { std::string osVersion; }; +struct BatteryInfo { + bool present = false; + bool charging = false; + // -1 means that the operating system did not provide a charge level. + int levelPercent = -1; + // -1 means that the operating system did not provide an estimate. + std::int64_t secondsRemaining = -1; +}; + SystemInfo GetSystemInfo(); +BatteryInfo GetBatteryInfo(std::string* errorMessage = nullptr); + +// PreventSleep and AllowSleep must be paired. On Windows they must be called +// from the same thread because the execution-state setting is thread-local. +bool PreventSleep(std::string* errorMessage = nullptr); +bool AllowSleep(std::string* errorMessage = nullptr); + +// Requests an immediate operating-system shutdown or restart. These operations +// commonly require elevated privileges. +bool Shutdown(std::string* errorMessage = nullptr); +bool Restart(std::string* errorMessage = nullptr); } // namespace izo diff --git a/include/izo/iZo.hpp b/include/izo/iZo.hpp index bd20839..33043c6 100644 --- a/include/izo/iZo.hpp +++ b/include/izo/iZo.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include diff --git a/src/interaction_linux.cpp b/src/interaction_linux.cpp index 241e963..8a14962 100644 --- a/src/interaction_linux.cpp +++ b/src/interaction_linux.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -69,6 +70,57 @@ std::string trimmed(std::string value) { return value; } +std::string uri_encode(std::string_view value) { + static constexpr char hex[] = "0123456789ABCDEF"; + std::string result; + for (const unsigned char character : value) { + if ((character >= 'a' && character <= 'z') || (character >= 'A' && character <= 'Z') || + (character >= '0' && character <= '9') || character == '/' || character == '-' || + character == '_' || character == '.' || character == '~') { + result += static_cast(character); + } else { + result += '%'; + result += hex[character >> 4]; + result += hex[character & 15]; + } + } + return result; +} + +int hex_value(char value) { + if (value >= '0' && value <= '9') return value - '0'; + if (value >= 'a' && value <= 'f') return value - 'a' + 10; + if (value >= 'A' && value <= 'F') return value - 'A' + 10; + return -1; +} + +std::optional uri_decode(std::string_view value) { + std::string result; + for (std::size_t index = 0; index < value.size(); ++index) { + if (value[index] != '%') { + result += value[index]; + continue; + } + if (index + 2 >= value.size()) return std::nullopt; + const int high = hex_value(value[index + 1]); + const int low = hex_value(value[index + 2]); + if (high < 0 || low < 0) return std::nullopt; + result += static_cast((high << 4) | low); + index += 2; + } + return result; +} + +void append_u16(std::string& value, std::uint16_t number) { + value.push_back(static_cast(number)); + value.push_back(static_cast(number >> 8)); +} + +void append_u32(std::string& value, std::uint32_t number) { + append_u16(value, static_cast(number)); + append_u16(value, static_cast(number >> 16)); +} + } // namespace MessageResponse ShowMessageBox(const MessageBoxOptions& options, std::string* errorMessage) { @@ -192,4 +244,119 @@ std::optional GetClipboardText(std::string* errorMessage) { return std::nullopt; } +bool SetClipboardFiles(const std::vector& paths, + std::string* errorMessage) { + if (paths.empty()) { + if (errorMessage) *errorMessage = "At least one clipboard file is required"; + return false; + } + std::string uris; + for (const auto& path : paths) { + std::error_code error; + const auto absolute = std::filesystem::absolute(path, error); + if (error) { + if (errorMessage) *errorMessage = error.message(); + return false; + } + uris += "file://" + uri_encode(absolute.string()) + "\r\n"; + } + std::vector args; + if (on_path("wl-copy")) args = {"wl-copy", "--type", "text/uri-list"}; + else if (on_path("xclip")) args = {"xclip", "-selection", "clipboard", "-in", "-t", "text/uri-list"}; + else { + if (errorMessage) *errorMessage = "No clipboard provider found; install wl-clipboard or xclip"; + return false; + } + const auto result = run(args, uris); + if (result.code == 0) return true; + if (errorMessage) *errorMessage = result.output.empty() ? "Setting clipboard files failed" : result.output; + return false; +} + +std::optional> GetClipboardFiles(std::string* errorMessage) { + std::vector args; + if (on_path("wl-paste")) args = {"wl-paste", "--no-newline", "--type", "text/uri-list"}; + else if (on_path("xclip")) args = {"xclip", "-selection", "clipboard", "-out", "-t", "text/uri-list"}; + else { + if (errorMessage) *errorMessage = "No clipboard provider found; install wl-clipboard or xclip"; + return std::nullopt; + } + auto result = run(args); + if (result.code != 0) { + if (errorMessage) *errorMessage = result.output.empty() ? "Reading clipboard files failed" : result.output; + return std::nullopt; + } + std::vector paths; + std::stringstream lines(result.output); + std::string line; + while (std::getline(lines, line)) { + if (!line.empty() && line.back() == '\r') line.pop_back(); + if (line.empty() || line.front() == '#') continue; + constexpr std::string_view prefix = "file://"; + if (line.compare(0, prefix.size(), prefix) != 0) continue; + auto decoded = uri_decode(std::string_view(line).substr(prefix.size())); + if (!decoded) { + if (errorMessage) *errorMessage = "Clipboard contains an invalid file URI"; + return std::nullopt; + } + paths.emplace_back(std::move(*decoded)); + } + return paths; +} + +bool SetClipboardImage(const ClipboardImage& image, std::string* errorMessage) { + const std::uint64_t pixel_bytes = static_cast(image.width) * image.height * 4; + if (!image.width || !image.height || pixel_bytes != image.pixels.size() || + pixel_bytes > UINT32_MAX - 54) { + if (errorMessage) *errorMessage = "Clipboard image must contain width * height RGBA8 pixels"; + return false; + } + std::string bitmap; + bitmap.reserve(static_cast(54 + pixel_bytes)); + bitmap += "BM"; + append_u32(bitmap, static_cast(54 + pixel_bytes)); + append_u32(bitmap, 0); + append_u32(bitmap, 54); + append_u32(bitmap, 40); + append_u32(bitmap, image.width); + append_u32(bitmap, image.height); + append_u16(bitmap, 1); + append_u16(bitmap, 32); + append_u32(bitmap, 0); + append_u32(bitmap, static_cast(pixel_bytes)); + append_u32(bitmap, 0); append_u32(bitmap, 0); append_u32(bitmap, 0); append_u32(bitmap, 0); + for (std::uint32_t row = image.height; row-- > 0;) { + for (std::uint32_t column = 0; column < image.width; ++column) { + const auto offset = (static_cast(row) * image.width + column) * 4; + bitmap.push_back(static_cast(image.pixels[offset + 2])); + bitmap.push_back(static_cast(image.pixels[offset + 1])); + bitmap.push_back(static_cast(image.pixels[offset])); + bitmap.push_back(static_cast(image.pixels[offset + 3])); + } + } + std::vector args; + if (on_path("wl-copy")) args = {"wl-copy", "--type", "image/bmp"}; + else if (on_path("xclip")) args = {"xclip", "-selection", "clipboard", "-in", "-t", "image/bmp"}; + else { + if (errorMessage) *errorMessage = "No clipboard provider found; install wl-clipboard or xclip"; + return false; + } + const auto result = run(args, bitmap); + if (result.code == 0) return true; + if (errorMessage) *errorMessage = result.output.empty() ? "Setting clipboard image failed" : result.output; + return false; +} + +bool ShowNotification(std::string_view title, std::string_view message, + std::string* errorMessage) { + if (!on_path("notify-send")) { + if (errorMessage) *errorMessage = "Notification provider not found; install notify-send"; + return false; + } + const auto result = run({"notify-send", "--app-name=iZo", std::string(title), std::string(message)}); + if (result.code == 0) return true; + if (errorMessage) *errorMessage = result.output.empty() ? "Showing notification failed" : result.output; + return false; +} + } // namespace izo diff --git a/src/interaction_windows.cpp b/src/interaction_windows.cpp index 3a613dc..d4613d7 100644 --- a/src/interaction_windows.cpp +++ b/src/interaction_windows.cpp @@ -1,11 +1,20 @@ #include #include +#include #include #include +#include +#include #include +#include +#include +#include +#include +#include #include +#include namespace izo { namespace { @@ -107,6 +116,34 @@ LRESULT CALLBACK input_window_proc(HWND window, UINT message, WPARAM wparam, LPA return DefWindowProcW(window, message, wparam, lparam); } +bool put_clipboard_memory(UINT format, HGLOBAL memory, std::string* errorMessage) { + if (!OpenClipboard(nullptr)) { + if (errorMessage) *errorMessage = windows_error("OpenClipboard"); + GlobalFree(memory); + return false; + } + if (!EmptyClipboard() || !SetClipboardData(format, memory)) { + if (errorMessage) *errorMessage = windows_error("SetClipboardData"); + GlobalFree(memory); + CloseClipboard(); + return false; + } + CloseClipboard(); + return true; +} + +struct notification_result { + bool shown = false; + std::string error; +}; + +void copy_notification_text(wchar_t* destination, std::size_t capacity, + const std::wstring& source) { + const auto count = (std::min)(capacity - 1, source.size()); + std::wmemcpy(destination, source.data(), count); + destination[count] = L'\0'; +} + } // namespace MessageResponse ShowMessageBox(const MessageBoxOptions& options, std::string* errorMessage) { @@ -251,4 +288,173 @@ std::optional GetClipboardText(std::string* errorMessage) { return result; } +bool SetClipboardFiles(const std::vector& paths, + std::string* errorMessage) { + if (paths.empty()) { + if (errorMessage) *errorMessage = "At least one clipboard file is required"; + return false; + } + std::vector values; + std::size_t characters = 1; + for (const auto& path : paths) { + std::error_code error; + auto absolute = std::filesystem::absolute(path, error); + if (error) { + if (errorMessage) *errorMessage = error.message(); + return false; + } + values.push_back(absolute.wstring()); + if (values.back().size() > (std::numeric_limits::max)() - characters - 1) { + if (errorMessage) *errorMessage = "Clipboard file list is too large"; + return false; + } + characters += values.back().size() + 1; + } + const std::size_t bytes = sizeof(DROPFILES) + characters * sizeof(wchar_t); + HGLOBAL memory = GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, bytes); + if (!memory) { + if (errorMessage) *errorMessage = windows_error("GlobalAlloc"); + return false; + } + auto* drop = static_cast(GlobalLock(memory)); + if (!drop) { + if (errorMessage) *errorMessage = windows_error("GlobalLock"); + GlobalFree(memory); + return false; + } + drop->pFiles = sizeof(DROPFILES); + drop->fWide = TRUE; + auto* output = reinterpret_cast(reinterpret_cast(drop) + sizeof(DROPFILES)); + for (const auto& value : values) { + std::memcpy(output, value.c_str(), (value.size() + 1) * sizeof(wchar_t)); + output += value.size() + 1; + } + *output = L'\0'; + GlobalUnlock(memory); + return put_clipboard_memory(CF_HDROP, memory, errorMessage); +} + +std::optional> GetClipboardFiles(std::string* errorMessage) { + if (!OpenClipboard(nullptr)) { + if (errorMessage) *errorMessage = windows_error("OpenClipboard"); + return std::nullopt; + } + HDROP drop = static_cast(GetClipboardData(CF_HDROP)); + if (!drop) { + if (errorMessage) *errorMessage = "Clipboard does not contain files"; + CloseClipboard(); + return std::nullopt; + } + std::vector paths; + const UINT count = DragQueryFileW(drop, 0xFFFFFFFF, nullptr, 0); + paths.reserve(count); + for (UINT index = 0; index < count; ++index) { + const UINT length = DragQueryFileW(drop, index, nullptr, 0); + std::wstring value(static_cast(length) + 1, L'\0'); + DragQueryFileW(drop, index, value.data(), length + 1); + value.resize(length); + paths.emplace_back(std::move(value)); + } + CloseClipboard(); + return paths; +} + +bool SetClipboardImage(const ClipboardImage& image, std::string* errorMessage) { + const std::uint64_t pixel_bytes = static_cast(image.width) * image.height * 4; + if (!image.width || !image.height || image.width > INT32_MAX || image.height > INT32_MAX || + pixel_bytes != image.pixels.size() || + pixel_bytes > (std::numeric_limits::max)() - sizeof(BITMAPINFOHEADER)) { + if (errorMessage) *errorMessage = "Clipboard image must contain width * height RGBA8 pixels"; + return false; + } + HGLOBAL memory = GlobalAlloc(GMEM_MOVEABLE, sizeof(BITMAPINFOHEADER) + + static_cast(pixel_bytes)); + if (!memory) { + if (errorMessage) *errorMessage = windows_error("GlobalAlloc"); + return false; + } + auto* header = static_cast(GlobalLock(memory)); + if (!header) { + if (errorMessage) *errorMessage = windows_error("GlobalLock"); + GlobalFree(memory); + return false; + } + *header = {}; + header->biSize = sizeof(BITMAPINFOHEADER); + header->biWidth = static_cast(image.width); + header->biHeight = static_cast(image.height); + header->biPlanes = 1; + header->biBitCount = 32; + header->biCompression = BI_RGB; + header->biSizeImage = static_cast(pixel_bytes); + auto* output = reinterpret_cast(header + 1); + for (std::uint32_t row = image.height; row-- > 0;) { + for (std::uint32_t column = 0; column < image.width; ++column) { + const auto source = (static_cast(row) * image.width + column) * 4; + *output++ = image.pixels[source + 2]; + *output++ = image.pixels[source + 1]; + *output++ = image.pixels[source]; + *output++ = image.pixels[source + 3]; + } + } + GlobalUnlock(memory); + return put_clipboard_memory(CF_DIB, memory, errorMessage); +} + +bool ShowNotification(std::string_view title, std::string_view message, + std::string* errorMessage) { + std::promise promise; + auto future = promise.get_future(); + std::thread([title = widen(title), message = widen(message), + promise = std::move(promise)]() mutable { + static constexpr wchar_t class_name[] = L"iZoNotificationWindow"; + WNDCLASSW descriptor{}; + descriptor.lpfnWndProc = DefWindowProcW; + descriptor.hInstance = GetModuleHandleW(nullptr); + descriptor.lpszClassName = class_name; + if (!RegisterClassW(&descriptor) && GetLastError() != ERROR_CLASS_ALREADY_EXISTS) { + promise.set_value({false, windows_error("RegisterClassW")}); + return; + } + HWND window = CreateWindowExW(0, class_name, L"", 0, 0, 0, 0, 0, + HWND_MESSAGE, nullptr, GetModuleHandleW(nullptr), nullptr); + if (!window) { + promise.set_value({false, windows_error("CreateWindowExW")}); + return; + } + NOTIFYICONDATAW icon{}; + icon.cbSize = sizeof(icon); + icon.hWnd = window; + icon.uID = 1; + icon.uFlags = NIF_ICON | NIF_TIP; + icon.hIcon = LoadIconW(nullptr, IDI_INFORMATION); + copy_notification_text(icon.szTip, sizeof(icon.szTip) / sizeof(*icon.szTip), L"iZo"); + if (!Shell_NotifyIconW(NIM_ADD, &icon)) { + const auto error = windows_error("Shell_NotifyIconW"); + DestroyWindow(window); + promise.set_value({false, error}); + return; + } + icon.uFlags = NIF_INFO; + icon.dwInfoFlags = NIIF_INFO | NIIF_RESPECT_QUIET_TIME; + copy_notification_text(icon.szInfoTitle, + sizeof(icon.szInfoTitle) / sizeof(*icon.szInfoTitle), title); + copy_notification_text(icon.szInfo, sizeof(icon.szInfo) / sizeof(*icon.szInfo), message); + if (!Shell_NotifyIconW(NIM_MODIFY, &icon)) { + const auto error = windows_error("Shell_NotifyIconW"); + Shell_NotifyIconW(NIM_DELETE, &icon); + DestroyWindow(window); + promise.set_value({false, error}); + return; + } + promise.set_value({true, {}}); + std::this_thread::sleep_for(std::chrono::seconds(10)); + Shell_NotifyIconW(NIM_DELETE, &icon); + DestroyWindow(window); + }).detach(); + auto result = future.get(); + if (!result.shown && errorMessage) *errorMessage = std::move(result.error); + return result.shown; +} + } // namespace izo diff --git a/src/platform_linux.cpp b/src/platform_linux.cpp index d8204e6..94aa0b8 100644 --- a/src/platform_linux.cpp +++ b/src/platform_linux.cpp @@ -9,6 +9,9 @@ #include #include #include +#ifdef __linux__ +#include +#endif #include #include #include @@ -17,6 +20,8 @@ #include #include #include +#include +#include #include #include @@ -42,6 +47,60 @@ void signal_handler(int value) { raise(value); } +std::mutex sleep_mutex; +pid_t sleep_inhibitor = -1; + +struct sleep_inhibitor_cleanup { + ~sleep_inhibitor_cleanup() { + if (sleep_inhibitor <= 0) return; + kill(-sleep_inhibitor, SIGTERM); + int status = 0; + while (waitpid(sleep_inhibitor, &status, 0) < 0 && errno == EINTR) {} + } +} cleanup_sleep_inhibitor; + +bool run_systemctl(const char* action, std::string* errorMessage) { + int error_pipe[2]; + if (pipe2(error_pipe, O_CLOEXEC) != 0) { + if (errorMessage) *errorMessage = std::strerror(errno); + return false; + } + const pid_t child = fork(); + if (child < 0) { + if (errorMessage) *errorMessage = std::strerror(errno); + close(error_pipe[0]); close(error_pipe[1]); + return false; + } + if (child == 0) { + close(error_pipe[0]); + execlp("systemctl", "systemctl", action, static_cast(nullptr)); + const int error = errno; + write(error_pipe[1], &error, sizeof(error)); + _exit(127); + } + close(error_pipe[1]); + int execution_error = 0; + const ssize_t bytes = read(error_pipe[0], &execution_error, sizeof(execution_error)); + close(error_pipe[0]); + int status = 0; + while (waitpid(child, &status, 0) < 0 && errno == EINTR) {} + if (bytes > 0) { + if (errorMessage) *errorMessage = std::string("Starting systemctl failed: ") + + std::strerror(execution_error); + return false; + } + if (WIFEXITED(status) && WEXITSTATUS(status) == 0) return true; + if (errorMessage) *errorMessage = std::string("systemctl ") + action + " failed"; + return false; +} + +std::string read_first_line(const std::filesystem::path& path) { + std::ifstream input(path); + std::string value; + std::getline(input, value); + return value; +} + } // namespace std::filesystem::path GetKnownPath(KnownPath path, std::string* errorMessage) { @@ -177,6 +236,97 @@ SystemInfo GetSystemInfo() { os.sysname, os.release}; } +BatteryInfo GetBatteryInfo(std::string* errorMessage) { + std::error_code error; + const std::filesystem::path root = "/sys/class/power_supply"; + if (!std::filesystem::exists(root, error)) { + if (error && errorMessage) *errorMessage = error.message(); + return {}; + } + for (const auto& entry : std::filesystem::directory_iterator(root, error)) { + if (read_first_line(entry.path() / "type") != "Battery") continue; + BatteryInfo result; + result.present = read_first_line(entry.path() / "present") != "0"; + const auto status = read_first_line(entry.path() / "status"); + result.charging = status == "Charging"; + const auto capacity = read_first_line(entry.path() / "capacity"); + if (!capacity.empty()) { + char* end = nullptr; + const long value = std::strtol(capacity.c_str(), &end, 10); + if (end == capacity.c_str() + capacity.size() && value >= 0 && value <= 100) + result.levelPercent = static_cast(value); + } + const auto remaining = read_first_line(entry.path() / "time_to_empty_now"); + if (!remaining.empty()) { + char* end = nullptr; + const long long value = std::strtoll(remaining.c_str(), &end, 10); + if (end == remaining.c_str() + remaining.size() && value >= 0) + result.secondsRemaining = value; + } + return result; + } + if (error && errorMessage) *errorMessage = error.message(); + return {}; +} + +bool PreventSleep(std::string* errorMessage) { + std::lock_guard lock(sleep_mutex); + if (sleep_inhibitor > 0) return true; + int error_pipe[2]; + if (pipe2(error_pipe, O_CLOEXEC) != 0) { + if (errorMessage) *errorMessage = std::strerror(errno); + return false; + } + const pid_t child = fork(); + if (child < 0) { + if (errorMessage) *errorMessage = std::strerror(errno); + close(error_pipe[0]); close(error_pipe[1]); + return false; + } + if (child == 0) { + close(error_pipe[0]); + setpgid(0, 0); +#ifdef __linux__ + prctl(PR_SET_PDEATHSIG, SIGTERM); +#endif + execlp("systemd-inhibit", "systemd-inhibit", "--what=sleep", "--who=iZo", + "--why=Application requested continuous execution", "--mode=block", + "sleep", "infinity", static_cast(nullptr)); + const int error = errno; + write(error_pipe[1], &error, sizeof(error)); + _exit(127); + } + close(error_pipe[1]); + int execution_error = 0; + const ssize_t bytes = read(error_pipe[0], &execution_error, sizeof(execution_error)); + close(error_pipe[0]); + if (bytes > 0) { + int status = 0; + while (waitpid(child, &status, 0) < 0 && errno == EINTR) {} + if (errorMessage) *errorMessage = std::string("Starting systemd-inhibit failed: ") + + std::strerror(execution_error); + return false; + } + sleep_inhibitor = child; + return true; +} + +bool AllowSleep(std::string* errorMessage) { + std::lock_guard lock(sleep_mutex); + if (sleep_inhibitor <= 0) return true; + if (kill(-sleep_inhibitor, SIGTERM) != 0 && errno != ESRCH) { + if (errorMessage) *errorMessage = std::strerror(errno); + return false; + } + int status = 0; + while (waitpid(sleep_inhibitor, &status, 0) < 0 && errno == EINTR) {} + sleep_inhibitor = -1; + return true; +} + +bool Shutdown(std::string* errorMessage) { return run_systemctl("poweroff", errorMessage); } +bool Restart(std::string* errorMessage) { return run_systemctl("reboot", errorMessage); } + void InstallCrashHandler(CrashCallback callback, void* userData) { crash_handler = callback; crash_user_data = userData; diff --git a/src/platform_windows.cpp b/src/platform_windows.cpp index 6ee06b6..811ddbe 100644 --- a/src/platform_windows.cpp +++ b/src/platform_windows.cpp @@ -52,6 +52,34 @@ std::string error_text(const char* operation, DWORD code = GetLastError()) { return result; } +bool request_power_action(UINT flags, std::string* errorMessage) { + HANDLE token = nullptr; + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &token)) { + if (errorMessage) *errorMessage = error_text("OpenProcessToken"); + return false; + } + TOKEN_PRIVILEGES privileges{}; + privileges.PrivilegeCount = 1; + if (!LookupPrivilegeValueW(nullptr, SE_SHUTDOWN_NAME, &privileges.Privileges[0].Luid)) { + if (errorMessage) *errorMessage = error_text("LookupPrivilegeValueW"); + CloseHandle(token); + return false; + } + privileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; + SetLastError(ERROR_SUCCESS); + const BOOL adjusted = AdjustTokenPrivileges(token, FALSE, &privileges, 0, nullptr, nullptr); + const DWORD adjustment_error = GetLastError(); + CloseHandle(token); + if (!adjusted || adjustment_error != ERROR_SUCCESS) { + if (errorMessage) *errorMessage = error_text("AdjustTokenPrivileges", adjustment_error); + return false; + } + if (ExitWindowsEx(flags | EWX_FORCEIFHUNG, + SHTDN_REASON_MAJOR_APPLICATION | SHTDN_REASON_FLAG_PLANNED)) return true; + if (errorMessage) *errorMessage = error_text("ExitWindowsEx"); + return false; +} + std::filesystem::path shell_folder(REFKNOWNFOLDERID id, std::string* errorMessage) { wchar_t* value = nullptr; const HRESULT result = SHGetKnownFolderPath(id, KF_FLAG_DEFAULT, nullptr, &value); @@ -217,6 +245,34 @@ SystemInfo GetSystemInfo() { '.' + std::to_string(version.dwBuildNumber)}; } +BatteryInfo GetBatteryInfo(std::string* errorMessage) { + SYSTEM_POWER_STATUS status{}; + if (!GetSystemPowerStatus(&status)) { + if (errorMessage) *errorMessage = error_text("GetSystemPowerStatus"); + return {}; + } + const bool present = status.BatteryFlag != 128 && status.BatteryFlag != 255; + return {present, present && (status.BatteryFlag & 8) != 0, + present && status.BatteryLifePercent != 255 ? status.BatteryLifePercent : -1, + present && status.BatteryLifeTime != static_cast(-1) + ? static_cast(status.BatteryLifeTime) : -1}; +} + +bool PreventSleep(std::string* errorMessage) { + if (SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED) != 0) return true; + if (errorMessage) *errorMessage = error_text("SetThreadExecutionState"); + return false; +} + +bool AllowSleep(std::string* errorMessage) { + if (SetThreadExecutionState(ES_CONTINUOUS) != 0) return true; + if (errorMessage) *errorMessage = error_text("SetThreadExecutionState"); + return false; +} + +bool Shutdown(std::string* errorMessage) { return request_power_action(EWX_POWEROFF, errorMessage); } +bool Restart(std::string* errorMessage) { return request_power_action(EWX_REBOOT, errorMessage); } + void InstallCrashHandler(CrashCallback callback, void* userData) { crash_user_data.store(userData); crash_handler.store(callback);