feat(platform): add native clipboard notification and power APIs

This commit is contained in:
2026-06-18 21:08:22 -05:00
parent 7172a00e2e
commit 9bf475c416
9 changed files with 630 additions and 1 deletions

View File

@@ -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.

View File

@@ -1,12 +1,29 @@
#pragma once
#include <cstdint>
#include <filesystem>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
namespace izo {
bool SetClipboardText(std::string_view text, std::string* errorMessage = nullptr);
std::optional<std::string> GetClipboardText(std::string* errorMessage = nullptr);
bool SetClipboardFiles(const std::vector<std::filesystem::path>& paths,
std::string* errorMessage = nullptr);
std::optional<std::vector<std::filesystem::path>> 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<std::uint8_t> pixels;
};
bool SetClipboardImage(const ClipboardImage& image, std::string* errorMessage = nullptr);
} // namespace izo

View File

@@ -0,0 +1,12 @@
#pragma once
#include <string>
#include <string_view>
namespace izo {
// Shows a non-blocking desktop notification.
bool ShowNotification(std::string_view title, std::string_view message,
std::string* errorMessage = nullptr);
} // namespace izo

View File

@@ -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

View File

@@ -8,6 +8,7 @@
#include <izo/DynamicLibrary.hpp>
#include <izo/Environment.hpp>
#include <izo/MessageBox.hpp>
#include <izo/Notifications.hpp>
#include <izo/Paths.hpp>
#include <izo/Process.hpp>
#include <izo/System.hpp>

View File

@@ -1,5 +1,6 @@
#include <izo/Clipboard.hpp>
#include <izo/MessageBox.hpp>
#include <izo/Notifications.hpp>
#include <sys/wait.h>
#include <unistd.h>
@@ -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<char>(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<std::string> 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<char>((high << 4) | low);
index += 2;
}
return result;
}
void append_u16(std::string& value, std::uint16_t number) {
value.push_back(static_cast<char>(number));
value.push_back(static_cast<char>(number >> 8));
}
void append_u32(std::string& value, std::uint32_t number) {
append_u16(value, static_cast<std::uint16_t>(number));
append_u16(value, static_cast<std::uint16_t>(number >> 16));
}
} // namespace
MessageResponse ShowMessageBox(const MessageBoxOptions& options, std::string* errorMessage) {
@@ -192,4 +244,119 @@ std::optional<std::string> GetClipboardText(std::string* errorMessage) {
return std::nullopt;
}
bool SetClipboardFiles(const std::vector<std::filesystem::path>& 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<std::string> 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<std::vector<std::filesystem::path>> GetClipboardFiles(std::string* errorMessage) {
std::vector<std::string> 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<std::filesystem::path> 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<std::uint64_t>(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<std::size_t>(54 + pixel_bytes));
bitmap += "BM";
append_u32(bitmap, static_cast<std::uint32_t>(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<std::uint32_t>(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<std::size_t>(row) * image.width + column) * 4;
bitmap.push_back(static_cast<char>(image.pixels[offset + 2]));
bitmap.push_back(static_cast<char>(image.pixels[offset + 1]));
bitmap.push_back(static_cast<char>(image.pixels[offset]));
bitmap.push_back(static_cast<char>(image.pixels[offset + 3]));
}
}
std::vector<std::string> 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

View File

@@ -1,11 +1,20 @@
#include <izo/Clipboard.hpp>
#include <izo/MessageBox.hpp>
#include <izo/Notifications.hpp>
#include <windows.h>
#include <commdlg.h>
#include <shellapi.h>
#include <shlobj.h>
#include <array>
#include <algorithm>
#include <chrono>
#include <cstring>
#include <future>
#include <limits>
#include <string>
#include <thread>
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<std::string> GetClipboardText(std::string* errorMessage) {
return result;
}
bool SetClipboardFiles(const std::vector<std::filesystem::path>& paths,
std::string* errorMessage) {
if (paths.empty()) {
if (errorMessage) *errorMessage = "At least one clipboard file is required";
return false;
}
std::vector<std::wstring> 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<std::size_t>::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<DROPFILES*>(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<wchar_t*>(reinterpret_cast<unsigned char*>(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<std::vector<std::filesystem::path>> GetClipboardFiles(std::string* errorMessage) {
if (!OpenClipboard(nullptr)) {
if (errorMessage) *errorMessage = windows_error("OpenClipboard");
return std::nullopt;
}
HDROP drop = static_cast<HDROP>(GetClipboardData(CF_HDROP));
if (!drop) {
if (errorMessage) *errorMessage = "Clipboard does not contain files";
CloseClipboard();
return std::nullopt;
}
std::vector<std::filesystem::path> 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<std::size_t>(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<std::uint64_t>(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<std::size_t>::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<std::size_t>(pixel_bytes));
if (!memory) {
if (errorMessage) *errorMessage = windows_error("GlobalAlloc");
return false;
}
auto* header = static_cast<BITMAPINFOHEADER*>(GlobalLock(memory));
if (!header) {
if (errorMessage) *errorMessage = windows_error("GlobalLock");
GlobalFree(memory);
return false;
}
*header = {};
header->biSize = sizeof(BITMAPINFOHEADER);
header->biWidth = static_cast<LONG>(image.width);
header->biHeight = static_cast<LONG>(image.height);
header->biPlanes = 1;
header->biBitCount = 32;
header->biCompression = BI_RGB;
header->biSizeImage = static_cast<DWORD>(pixel_bytes);
auto* output = reinterpret_cast<std::uint8_t*>(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<std::size_t>(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<notification_result> 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

View File

@@ -9,6 +9,9 @@
#include <fcntl.h>
#include <signal.h>
#include <sys/sysinfo.h>
#ifdef __linux__
#include <sys/prctl.h>
#endif
#include <sys/utsname.h>
#include <sys/wait.h>
#include <unistd.h>
@@ -17,6 +20,8 @@
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <mutex>
#include <sstream>
#include <thread>
#include <vector>
@@ -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<char*>(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<int>(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<std::mutex> 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<char*>(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<std::mutex> 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;

View File

@@ -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<DWORD>(-1)
? static_cast<std::int64_t>(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);