feat(platform): add Linux utility parity

This commit is contained in:
2026-06-18 19:26:21 -05:00
parent 27b90b0840
commit 49f6155ee6
3 changed files with 314 additions and 0 deletions

View File

@@ -12,6 +12,8 @@ target_sources(izo
src/time.cpp
$<$<PLATFORM_ID:Windows>:src/interaction_windows.cpp>
$<$<PLATFORM_ID:Windows>:src/platform_windows.cpp>
$<$<PLATFORM_ID:Linux>:src/interaction_linux.cpp>
$<$<PLATFORM_ID:Linux>:src/platform_linux.cpp>
$<$<PLATFORM_ID:Windows>:src/dialogs_windows.cpp>
$<$<PLATFORM_ID:Linux>:src/dialogs_linux.cpp>
)
@@ -33,6 +35,7 @@ if(WIN32)
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.
target_link_libraries(izo PRIVATE dl)
else()
message(FATAL_ERROR "iZo currently supports Windows and Linux")
endif()

140
src/interaction_linux.cpp Normal file
View File

@@ -0,0 +1,140 @@
#include <izo/clipboard.hpp>
#include <izo/message_box.hpp>
#include <sys/wait.h>
#include <unistd.h>
#include <cerrno>
#include <cstdlib>
#include <cstring>
#include <sstream>
#include <vector>
namespace izo {
namespace {
bool on_path(const char* name) {
const char* path = std::getenv("PATH");
if (!path) return false;
std::stringstream stream(path);
std::string directory;
while (std::getline(stream, directory, ':')) {
if (directory.empty()) directory = ".";
if (access((directory + '/' + name).c_str(), X_OK) == 0) return true;
}
return false;
}
struct command_result { int code; std::string output; std::string error; };
command_result run(const std::vector<std::string>& args, std::string_view input = {}) {
int output_pipe[2];
int input_pipe[2];
if (pipe(output_pipe) || pipe(input_pipe)) return {-1, {}, std::strerror(errno)};
const pid_t child = fork();
if (child < 0) return {-1, {}, std::strerror(errno)};
if (child == 0) {
dup2(input_pipe[0], STDIN_FILENO);
dup2(output_pipe[1], STDOUT_FILENO);
dup2(output_pipe[1], STDERR_FILENO);
close(input_pipe[0]); close(input_pipe[1]); close(output_pipe[0]); close(output_pipe[1]);
std::vector<char*> argv;
for (const auto& arg : args) argv.push_back(const_cast<char*>(arg.c_str()));
argv.push_back(nullptr);
execvp(argv[0], argv.data());
_exit(127);
}
close(input_pipe[0]); close(output_pipe[1]);
std::size_t written = 0;
while (written < input.size()) {
const auto count = write(input_pipe[1], input.data() + written, input.size() - written);
if (count <= 0) break;
written += static_cast<std::size_t>(count);
}
close(input_pipe[1]);
std::string output;
char buffer[4096];
ssize_t count;
while ((count = read(output_pipe[0], buffer, sizeof(buffer))) > 0)
output.append(buffer, static_cast<std::size_t>(count));
close(output_pipe[0]);
int status = 0;
while (waitpid(child, &status, 0) < 0 && errno == EINTR) {}
return {WIFEXITED(status) ? WEXITSTATUS(status) : -1, std::move(output), {}};
}
} // namespace
message_response show_message_box(const message_box_options& options, std::string* error_message) {
std::vector<std::string> args;
if (on_path("zenity")) {
args = {"zenity"};
if (options.buttons == message_buttons::yes_no || options.buttons == message_buttons::yes_no_cancel)
args.push_back("--question");
else if (options.icon == message_icon::error) args.push_back("--error");
else if (options.icon == message_icon::warning) args.push_back("--warning");
else args.push_back("--info");
args.push_back("--title=" + options.title);
args.push_back("--text=" + options.message);
if (options.buttons == message_buttons::yes_no_cancel) {
args.push_back("--extra-button=No"); args.push_back("--cancel-label=Cancel");
}
if (options.buttons == message_buttons::ok_cancel) {
args.push_back("--ok-label=OK"); args.push_back("--cancel-label=Cancel");
}
} else if (on_path("kdialog")) {
args = {"kdialog"};
if (options.buttons == message_buttons::yes_no_cancel) args.push_back("--yesnocancel");
else if (options.buttons == message_buttons::yes_no) args.push_back("--yesno");
else if (options.icon == message_icon::error) args.push_back("--error");
else if (options.icon == message_icon::warning) args.push_back("--sorry");
else args.push_back("--msgbox");
args.push_back(options.message);
args.push_back("--title"); args.push_back(options.title);
} else {
if (error_message) *error_message = "No supported message box provider found; install zenity or kdialog";
return message_response::error;
}
const auto result = run(args);
if (options.buttons == message_buttons::yes_no_cancel && result.output == "No\n")
return message_response::no;
if (result.code == 0)
return (options.buttons == message_buttons::yes_no || options.buttons == message_buttons::yes_no_cancel)
? message_response::yes : message_response::ok;
if (result.code == 1)
return (options.buttons == message_buttons::yes_no) ? message_response::no : message_response::cancel;
if (result.code == 2 && options.buttons == message_buttons::yes_no_cancel)
return message_response::cancel;
if (error_message) *error_message = result.output.empty() ? "Message box failed" : result.output;
return message_response::error;
}
bool set_clipboard_text(std::string_view text, std::string* error_message) {
std::vector<std::string> args;
if (on_path("wl-copy")) args = {"wl-copy", "--type", "text/plain;charset=utf-8"};
else if (on_path("xclip")) args = {"xclip", "-selection", "clipboard", "-in"};
else {
if (error_message) *error_message = "No clipboard provider found; install wl-clipboard or xclip";
return false;
}
const auto result = run(args, text);
if (result.code == 0) return true;
if (error_message) *error_message = result.output.empty() ? "Setting clipboard text failed" : result.output;
return false;
}
std::optional<std::string> get_clipboard_text(std::string* error_message) {
std::vector<std::string> args;
if (on_path("wl-paste")) args = {"wl-paste", "--no-newline", "--type", "text"};
else if (on_path("xclip")) args = {"xclip", "-selection", "clipboard", "-out"};
else {
if (error_message) *error_message = "No clipboard provider found; install wl-clipboard or xclip";
return std::nullopt;
}
auto result = run(args);
if (result.code == 0) return std::move(result.output);
if (error_message) *error_message = result.output.empty() ? "Reading clipboard text failed" : result.output;
return std::nullopt;
}
} // namespace izo

171
src/platform_linux.cpp Normal file
View File

@@ -0,0 +1,171 @@
#include <izo/debug.hpp>
#include <izo/dynamic_library.hpp>
#include <izo/environment.hpp>
#include <izo/paths.hpp>
#include <izo/process.hpp>
#include <izo/system.hpp>
#include <dlfcn.h>
#include <signal.h>
#include <sys/sysinfo.h>
#include <sys/utsname.h>
#include <sys/wait.h>
#include <unistd.h>
#include <cerrno>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <thread>
#include <vector>
namespace izo {
namespace {
std::filesystem::path home() {
const char* value = std::getenv("HOME");
return value ? std::filesystem::path(value) : std::filesystem::path{};
}
std::filesystem::path env_path(const char* name, std::filesystem::path fallback) {
const char* value = std::getenv(name);
return value && *value ? std::filesystem::path(value) : std::move(fallback);
}
crash_callback crash_handler = nullptr;
void* crash_user_data = nullptr;
void signal_handler(int value) {
if (crash_handler) crash_handler(strsignal(value), crash_user_data);
signal(value, SIG_DFL);
raise(value);
}
} // namespace
std::filesystem::path get_known_path(known_path path, std::string* error_message) {
switch (path) {
case known_path::app_data: return env_path("XDG_DATA_HOME", home() / ".local/share");
case known_path::local_app_data: return env_path("XDG_DATA_HOME", home() / ".local/share");
case known_path::config: return env_path("XDG_CONFIG_HOME", home() / ".config");
case known_path::cache: return env_path("XDG_CACHE_HOME", home() / ".cache");
case known_path::documents: return home() / "Documents";
case known_path::downloads: return home() / "Downloads";
case known_path::desktop: return home() / "Desktop";
case known_path::temporary: return env_path("TMPDIR", "/tmp");
case known_path::executable_directory: {
std::string value(4096, '\0');
const auto count = readlink("/proc/self/exe", value.data(), value.size());
if (count < 0) {
if (error_message) *error_message = std::strerror(errno);
return {};
}
value.resize(static_cast<std::size_t>(count));
return std::filesystem::path(value).parent_path();
}
case known_path::current_directory: {
std::error_code ec;
auto result = std::filesystem::current_path(ec);
if (ec && error_message) *error_message = ec.message();
return result;
}
}
return {};
}
process_result launch_process(const process_options& options) {
if (options.executable.empty()) return {false, 0, "Executable path is required"};
const pid_t child = fork();
if (child < 0) return {false, 0, std::strerror(errno)};
if (child == 0) {
if (options.detached) setsid();
if (!options.working_directory.empty() && chdir(options.working_directory.c_str()) != 0) _exit(126);
std::vector<std::string> values{options.executable.string()};
values.insert(values.end(), options.arguments.begin(), options.arguments.end());
std::vector<char*> argv;
for (auto& value : values) argv.push_back(value.data());
argv.push_back(nullptr);
execvp(argv[0], argv.data());
_exit(127);
}
std::thread([child] {
int status = 0;
while (waitpid(child, &status, 0) < 0 && errno == EINTR) {}
}).detach();
return {true, static_cast<std::uint32_t>(child), {}};
}
dynamic_library::~dynamic_library() { reset(); }
dynamic_library::dynamic_library(dynamic_library&& other) noexcept : handle_(other.handle_) {
other.handle_ = nullptr;
}
dynamic_library& dynamic_library::operator=(dynamic_library&& other) noexcept {
if (this != &other) { reset(); handle_ = other.handle_; other.handle_ = nullptr; }
return *this;
}
void dynamic_library::reset() noexcept { if (handle_) dlclose(handle_); handle_ = nullptr; }
void* dynamic_library::symbol(std::string_view name, std::string* error_message) const {
if (!handle_) {
if (error_message) *error_message = "Dynamic library is not loaded";
return nullptr;
}
dlerror();
std::string terminated(name);
void* result = dlsym(handle_, terminated.c_str());
if (const char* error = dlerror()) {
if (error_message) *error_message = error;
return nullptr;
}
return result;
}
dynamic_library load_dynamic_library(const std::filesystem::path& path, std::string* error_message) {
void* handle = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL);
if (!handle && error_message) *error_message = dlerror();
return dynamic_library(handle);
}
std::optional<std::string> get_environment_variable(std::string_view name) {
std::string key(name);
const char* value = std::getenv(key.c_str());
return value ? std::optional<std::string>(value) : std::nullopt;
}
bool set_environment_variable(std::string_view name, std::string_view value, std::string* error_message) {
const int result = setenv(std::string(name).c_str(), std::string(value).c_str(), 1);
if (!result) return true;
if (error_message) *error_message = std::strerror(errno);
return false;
}
bool unset_environment_variable(std::string_view name, std::string* error_message) {
const int result = unsetenv(std::string(name).c_str());
if (!result) return true;
if (error_message) *error_message = std::strerror(errno);
return false;
}
system_info get_system_info() {
struct sysinfo memory{};
sysinfo(&memory);
struct utsname os{};
uname(&os);
return {std::thread::hardware_concurrency(),
static_cast<std::uint64_t>(memory.totalram) * memory.mem_unit,
static_cast<std::uint64_t>(memory.freeram + memory.bufferram) * memory.mem_unit,
os.sysname, os.release};
}
void install_crash_handler(crash_callback callback, void* user_data) {
crash_handler = callback;
crash_user_data = user_data;
for (int value : {SIGSEGV, SIGABRT, SIGFPE, SIGILL}) signal(value, callback ? signal_handler : SIG_DFL);
}
bool is_debugger_attached() noexcept {
std::ifstream status("/proc/self/status");
std::string line;
while (std::getline(status, line))
if (line.rfind("TracerPid:", 0) == 0) return std::atoi(line.c_str() + 10) != 0;
return false;
}
void debug_break() noexcept { if (is_debugger_attached()) raise(SIGTRAP); }
void debug_output(std::string_view message) { write(STDERR_FILENO, message.data(), message.size()); }
} // namespace izo