feat(platform): add Linux utility parity
This commit is contained in:
@@ -12,6 +12,8 @@ target_sources(izo
|
|||||||
src/time.cpp
|
src/time.cpp
|
||||||
$<$<PLATFORM_ID:Windows>:src/interaction_windows.cpp>
|
$<$<PLATFORM_ID:Windows>:src/interaction_windows.cpp>
|
||||||
$<$<PLATFORM_ID:Windows>:src/platform_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:Windows>:src/dialogs_windows.cpp>
|
||||||
$<$<PLATFORM_ID:Linux>:src/dialogs_linux.cpp>
|
$<$<PLATFORM_ID:Linux>:src/dialogs_linux.cpp>
|
||||||
)
|
)
|
||||||
@@ -33,6 +35,7 @@ if(WIN32)
|
|||||||
elseif(UNIX AND NOT APPLE)
|
elseif(UNIX AND NOT APPLE)
|
||||||
# No link-time desktop dependency. The Linux backend discovers zenity or
|
# No link-time desktop dependency. The Linux backend discovers zenity or
|
||||||
# kdialog at runtime so applications do not inherit GTK or Qt dependencies.
|
# kdialog at runtime so applications do not inherit GTK or Qt dependencies.
|
||||||
|
target_link_libraries(izo PRIVATE dl)
|
||||||
else()
|
else()
|
||||||
message(FATAL_ERROR "iZo currently supports Windows and Linux")
|
message(FATAL_ERROR "iZo currently supports Windows and Linux")
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
140
src/interaction_linux.cpp
Normal file
140
src/interaction_linux.cpp
Normal 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
171
src/platform_linux.cpp
Normal 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
|
||||||
Reference in New Issue
Block a user