/* * transfer.hpp * Multi-format clipboard, drag and drop, and window server requests for * MontaukOS GUI apps (the raw ABI is "Window server messaging" in * Api/Syscall.hpp) * Copyright (c) 2026 Daniel Hammer */ #pragma once #include #include #include namespace gui { using montauk::abi::TransferItem; using montauk::abi::TransferFormatInfo; // ---- Building a transfer ---------------------------------------------- // Collects the formats of one clipboard write or drag. Holds pointers // only: the strings and buffers passed to add() must outlive the call // that hands the transfer to the kernel (which copies them). struct TransferBuilder { TransferItem items[montauk::abi::TRANSFER_MAX_FORMATS]; uint32_t count = 0; bool add(const char* mime, const void* data, uint32_t len) { if (count >= montauk::abi::TRANSFER_MAX_FORMATS || !mime) return false; items[count].mime = (uint64_t)(uintptr_t)mime; items[count].data = (uint64_t)(uintptr_t)data; items[count].len = len; items[count]._reserved = 0; count++; return true; } bool add_text(const char* mime, const char* text) { return add(mime, text, text ? (uint32_t)montauk::slen(text) : 0); } }; // ---- Clipboard --------------------------------------------------------- inline bool clipboard_set(const TransferBuilder& transfer) { return montauk::clipboard_set_data(transfer.items, transfer.count) == 0; } inline bool clipboard_has_format(const char* mime) { uint32_t len = 0; int rc = montauk::clipboard_get_data(mime, nullptr, 0, &len); return rc == 0 || montauk::StatusFromResult(rc) == montauk::BufferTooSmall; } inline uint64_t clipboard_serial() { uint64_t serial = 0; montauk::clipboard_formats(nullptr, 0, &serial); return serial; } // Read a whole format into a new NUL-terminated buffer (montauk::mfree // it). Returns nullptr if the format is absent. inline char* clipboard_get_alloc(const char* mime, uint32_t* out_len = nullptr) { if (out_len) *out_len = 0; // The clipboard may change between asking for the size and the // copy; retry a few times rather than return a torn read. for (int attempt = 0; attempt < 4; attempt++) { uint32_t len = 0; int rc = montauk::clipboard_get_data(mime, nullptr, 0, &len); if (rc < 0 && montauk::StatusFromResult(rc) != montauk::BufferTooSmall) return nullptr; char* buf = (char*)montauk::malloc(len + 1); if (!buf) return nullptr; uint32_t got = 0; rc = montauk::clipboard_get_data(mime, buf, len, &got); if (rc == 0 && got <= len) { buf[got] = '\0'; if (out_len) *out_len = got; return buf; } montauk::mfree(buf); if (montauk::StatusFromResult(rc) != montauk::BufferTooSmall) return nullptr; } return nullptr; } // ---- Drag and drop ----------------------------------------------------- // Start a drag from window `win_id` (call it on the mouse move that // crosses the drag threshold). Returns the drag serial, or 0 on failure. // The compositor then shows the drag, sends DRAG_ENTER/OVER/LEAVE and // DROP to the windows under the pointer, and DRAG_END back to this one. inline uint32_t drag_begin(int win_id, const TransferBuilder& transfer, uint8_t actions, uint32_t item_count, const char* label) { montauk::abi::WinDragBegin info = {}; info.actions = actions; info.itemCount = item_count; if (label) montauk::strncpy(info.label, label, sizeof(info.label)); int rc = montauk::win_drag_begin(win_id, &info, transfer.items, transfer.count); return rc > 0 ? (uint32_t)rc : 0; } inline bool drag_has_format(uint32_t serial, const char* mime) { uint32_t len = 0; int rc = montauk::win_drag_get_data(serial, mime, nullptr, 0, &len); return rc == 0 || montauk::StatusFromResult(rc) == montauk::BufferTooSmall; } // Read a whole drag format into a new NUL-terminated buffer (montauk::mfree // it). Allowed once this window has received a drag event for `serial`. inline char* drag_get_alloc(uint32_t serial, const char* mime, uint32_t* out_len = nullptr) { if (out_len) *out_len = 0; uint32_t len = 0; int rc = montauk::win_drag_get_data(serial, mime, nullptr, 0, &len); if (rc < 0 && montauk::StatusFromResult(rc) != montauk::BufferTooSmall) return nullptr; char* buf = (char*)montauk::malloc(len + 1); if (!buf) return nullptr; uint32_t got = 0; rc = montauk::win_drag_get_data(serial, mime, buf, len, &got); if (rc != 0 || got > len) { montauk::mfree(buf); return nullptr; } buf[got] = '\0'; if (out_len) *out_len = got; return buf; } // The conventional modifier mapping for a drop: Ctrl copies, Shift moves, // otherwise `preferred` (e.g. move within a volume, copy across them). // Falls back to whatever the source offers. 0 if nothing is possible. inline uint8_t drag_pick_action(uint8_t offered, uint8_t modifiers, uint8_t preferred) { using namespace montauk::abi; uint8_t want = preferred; if (modifiers & WIN_MOD_CTRL) want = WIN_DRAG_COPY; else if (modifiers & WIN_MOD_SHIFT) want = WIN_DRAG_MOVE; if (offered & want) return want; if (offered & WIN_DRAG_COPY) return WIN_DRAG_COPY; if (offered & WIN_DRAG_MOVE) return WIN_DRAG_MOVE; if (offered & WIN_DRAG_LINK) return WIN_DRAG_LINK; return 0; } // ---- File lists (TRANSFER_MIME_FILE_LIST) ------------------------------ // Calls fn(path) for each non-empty line of a file list, in place // (temporarily NUL-terminating each line). Returns the number of paths. template inline int file_list_each(char* list, Fn fn) { int n = 0; char* p = list; while (p && *p) { char* end = p; while (*end && *end != '\n') end++; char saved = *end; *end = '\0'; char* line = p; int len = (int)(end - p); if (len > 0 && line[len - 1] == '\r') line[len - 1] = '\0'; if (line[0]) { fn((const char*)line); n++; } *end = saved; p = saved ? end + 1 : end; } return n; } // ---- Window server requests --------------------------------------------- inline bool win_request_send(int win_id, uint16_t type, const void* payload, uint16_t size) { if (size > montauk::abi::WIN_REQUEST_PAYLOAD_MAX) return false; montauk::abi::WinRequest req = {}; req.type = type; req.size = size; req.windowId = win_id; if (payload && size) montauk::memcpy(req.payload, payload, size); return montauk::win_request(win_id, &req) == 0; } // Ask the desktop to open a catalog item (app manifest id or a built-in // such as "desktop-settings") as if the user had launched it. inline bool win_request_launch_item(int win_id, const char* item_id, const char* args = nullptr) { montauk::abi::WinReqLaunchItem p = {}; montauk::strncpy(p.itemId, item_id ? item_id : "", sizeof(p.itemId)); if (args) montauk::strncpy(p.args, args, sizeof(p.args)); return win_request_send(win_id, montauk::abi::WIN_REQ_LAUNCH_ITEM, &p, sizeof(p)); } // Declare `win_id` a dialog of `parent_id` (same process): the desktop // centres it over the parent. inline bool win_request_set_parent(int win_id, int parent_id) { montauk::abi::WinReqSetParent p = { parent_id }; return win_request_send(win_id, montauk::abi::WIN_REQ_SET_PARENT, &p, sizeof(p)); } // Bring one of this app's windows to the front (e.g. re-show an open // dialog instead of opening a second one). inline bool win_request_activate(int win_id) { return win_request_send(win_id, montauk::abi::WIN_REQ_ACTIVATE, nullptr, 0); } // Answer DRAG_ENTER/DRAG_OVER with what a drop here would do (0 = refuse). inline bool win_request_drag_status(int win_id, uint32_t serial, uint8_t action) { montauk::abi::WinReqDragStatus p = {}; p.serial = serial; p.action = action; return win_request_send(win_id, montauk::abi::WIN_REQ_DRAG_STATUS, &p, sizeof(p)); } // ---- File drops for document apps ---------------------------------------- // For document apps that open a dropped file. Call it for every window // event; it answers drag events (copy, if the drag carries files and // `accept` is true) and returns true on a drop, with the first dropped // path in out_path. Anything else returns false and can be handled as // usual. inline bool handle_file_drop(int win_id, const montauk::abi::WinEvent& ev, char* out_path, int out_max, bool accept = true) { using namespace montauk::abi; if (ev.type == WIN_EVENT_DRAG_ENTER || ev.type == WIN_EVENT_DRAG_OVER) { bool files = accept && drag_has_format(ev.drag.serial, TRANSFER_MIME_FILE_LIST); win_request_drag_status(win_id, ev.drag.serial, files ? WIN_DRAG_COPY : 0); return false; } if (ev.type != WIN_EVENT_DROP || !accept || !out_path || out_max <= 0) return false; char* list = drag_get_alloc(ev.drag.serial, TRANSFER_MIME_FILE_LIST); if (!list) return false; bool got = false; file_list_each(list, [&](const char* path) { if (got) return; montauk::strncpy(out_path, path, out_max); got = true; }); montauk::mfree(list); return got; } } // namespace gui