diff --git a/CMakeLists.txt b/CMakeLists.txt index ddfa2ca..7c69211 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,6 +12,8 @@ target_sources(izo src/time.cpp $<$:src/interaction_windows.cpp> $<$:src/platform_windows.cpp> + $<$:src/interaction_linux.cpp> + $<$:src/platform_linux.cpp> $<$:src/dialogs_windows.cpp> $<$: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() diff --git a/src/interaction_linux.cpp b/src/interaction_linux.cpp new file mode 100644 index 0000000..28652d1 --- /dev/null +++ b/src/interaction_linux.cpp @@ -0,0 +1,140 @@ +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +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& 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 argv; + for (const auto& arg : args) argv.push_back(const_cast(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(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(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 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 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 get_clipboard_text(std::string* error_message) { + std::vector 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 diff --git a/src/platform_linux.cpp b/src/platform_linux.cpp new file mode 100644 index 0000000..aaa1681 --- /dev/null +++ b/src/platform_linux.cpp @@ -0,0 +1,171 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +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(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 values{options.executable.string()}; + values.insert(values.end(), options.arguments.begin(), options.arguments.end()); + std::vector 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(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 get_environment_variable(std::string_view name) { + std::string key(name); + const char* value = std::getenv(key.c_str()); + return value ? std::optional(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(memory.totalram) * memory.mem_unit, + static_cast(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