feat(platform): add native clipboard notification and power APIs
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
12
include/izo/Notifications.hpp
Normal file
12
include/izo/Notifications.hpp
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user