feat: move Files to its own process; add window server messaging (multi-format clipboard, drag and drop), undo/redo, sidebar, breadcrumbs, thumbnails, file type icons, New menu and proper text fields

This commit is contained in:
daniel committed 2026-10-24 12:44:20 +02:00
1 parent 07191b1286
commit d3e49a925d
66 files changed
+6855 -905

No files matched your search

+163 -2
View File
@@ -299,6 +299,19 @@ namespace montauk::abi {
thread ends the process rather than only that thread. */
static constexpr uint64_t SYS_EXIT_GROUP = 199; // (exitCode) -> does not return
/* Multi-format clipboard (see "Window server messaging" below). The text
calls above operate on its TRANSFER_MIME_TEXT format. */
static constexpr uint64_t SYS_CLIPBOARD_SET_DATA = 200; // (const TransferItem*, count) -> status
static constexpr uint64_t SYS_CLIPBOARD_FORMATS = 201; // (TransferFormatInfo*, max, uint64_t* serial) -> count
static constexpr uint64_t SYS_CLIPBOARD_GET_DATA = 202; // (mime, buf, max, uint32_t* len) -> status
/* Window server requests and drag and drop. */
static constexpr uint64_t SYS_WIN_REQUEST = 203; // (windowId, const WinRequest*) -> status
static constexpr uint64_t SYS_WIN_REQUEST_POLL = 204; // (WinRequest*) -> 1 / 0; compositor only
static constexpr uint64_t SYS_WIN_DRAG_BEGIN = 205; // (windowId or -1 for the compositor's own surface, const WinDragBegin*, const TransferItem*, count) -> serial
static constexpr uint64_t SYS_WIN_DRAG_FORMATS = 206; // (serial, TransferFormatInfo*, max) -> count
static constexpr uint64_t SYS_WIN_DRAG_GET_DATA = 207; // (serial, mime, buf, max, uint32_t* len) -> status
/* Kernel-owned process capabilities. User identities may namespace
per-user resources, but never participate in authorization decisions. */
static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0;
@@ -637,14 +650,52 @@ namespace montauk::abi {
};
// Window server shared types
enum WinEventType : uint8_t {
WIN_EVENT_KEY = 0,
WIN_EVENT_MOUSE = 1,
WIN_EVENT_RESIZE = 2,
WIN_EVENT_CLOSE = 3,
WIN_EVENT_SCALE = 4,
// Drag and drop (WinEvent::drag). A window may read the drag's data
// (SYS_WIN_DRAG_*) once it has received one of these for that serial.
WIN_EVENT_DRAG_ENTER = 5, // pointer entered the window during a drag
WIN_EVENT_DRAG_OVER = 6, // pointer moved; coalesced like mouse moves
WIN_EVENT_DRAG_LEAVE = 7, // pointer left, or the drag was cancelled
WIN_EVENT_DROP = 8, // released here; drag.actions = chosen action
WIN_EVENT_DRAG_END = 9, // to the source only; drag.actions = action
// taken (0 = cancelled / not accepted)
};
// Modifier keys held at the time of a drag event (WinEvent::drag.modifiers).
static constexpr uint8_t WIN_MOD_SHIFT = 1u << 0;
static constexpr uint8_t WIN_MOD_CTRL = 1u << 1;
static constexpr uint8_t WIN_MOD_ALT = 1u << 2;
// Drag actions; a source offers a set, a drop carries exactly one.
static constexpr uint8_t WIN_DRAG_COPY = 1u << 0;
static constexpr uint8_t WIN_DRAG_MOVE = 1u << 1;
static constexpr uint8_t WIN_DRAG_LINK = 1u << 2;
struct WinDragEvent {
int32_t x, y; // window-local; -1,-1 when not over the window
uint32_t serial; // identifies the drag
uint8_t actions; // WIN_DRAG_* (see WinEventType)
uint8_t modifiers; // WIN_MOD_*
uint8_t _pad[2];
};
static_assert(sizeof(WinDragEvent) == 16, "WinDragEvent must fit the WinEvent union");
struct WinEvent {
uint8_t type; // 0=key, 1=mouse, 2=resize, 3=close, 4=scale
uint8_t type; // WinEventType
uint8_t _pad[3];
union {
KeyEvent key; // 16 bytes: the union was already that size (mouse)
struct { int32_t x, y, scroll; uint8_t buttons, prev_buttons; } mouse;
// modifiers (WIN_MOD_*) was padding before; compositors that
// predate it send 0.
struct { int32_t x, y, scroll; uint8_t buttons, prev_buttons, modifiers; } mouse;
struct { int32_t w, h; } resize;
struct { int32_t scale; } scale;
WinDragEvent drag; // WIN_EVENT_DRAG_* / WIN_EVENT_DROP
};
};
static_assert(sizeof(WinEvent) == 20, "WinEvent layout is shared with existing binaries");
@@ -668,6 +719,116 @@ namespace montauk::abi {
uint64_t pixelVa; // VA of pixel buffer in caller's address space
};
// ---- Window server messaging ----------------------------------------
//
// Three pieces, shared by the compositor (the desktop) and every app:
// * WinEvent types 5-9 below: drag-and-drop events, compositor -> app.
// * WinRequest: typed records an app posts about one of its own windows
// (SYS_WIN_REQUEST); only the compositor can collect them
// (SYS_WIN_REQUEST_POLL). Unknown types are ignored, so new requests
// can be added without breaking older compositors or apps.
// * Transfers: multi-format payloads (one MIME type per format) carried
// by the clipboard (SYS_CLIPBOARD_*_DATA) and by drags
// (SYS_WIN_DRAG_*). The older text clipboard calls read and write the
// TRANSFER_MIME_TEXT format of the same clipboard.
// Transfers -----------------------------------------------------------
static constexpr uint32_t TRANSFER_MAX_FORMATS = 8;
static constexpr uint32_t TRANSFER_MIME_MAX = 64; // incl. NUL
static constexpr uint32_t TRANSFER_MAX_BYTES = 256 * 1024; // all formats together
// Well-known formats. Any other "type/subtype" string is allowed.
static constexpr const char* TRANSFER_MIME_TEXT = "text/plain;charset=utf-8";
// UTF-8 absolute paths ("0:/..."), one per line.
static constexpr const char* TRANSFER_MIME_FILE_LIST = "application/x-montauk-file-list";
// Clipboard companion to a file list: "copy" or "cut".
static constexpr const char* TRANSFER_MIME_FILE_OP = "application/x-montauk-file-op";
// One format to store. Pointers are carried as 64-bit values so the
// layout is the same for every caller.
struct TransferItem {
uint64_t mime; // const char*, NUL-terminated, < TRANSFER_MIME_MAX
uint64_t data; // const void*
uint32_t len;
uint32_t _reserved;
};
static_assert(sizeof(TransferItem) == 24, "TransferItem is ABI");
struct TransferFormatInfo {
char mime[TRANSFER_MIME_MAX];
uint32_t len;
uint32_t _reserved;
};
static_assert(sizeof(TransferFormatInfo) == 72, "TransferFormatInfo is ABI");
// Requests --------------------------------------------------------------
static constexpr uint32_t WIN_REQUEST_PAYLOAD_MAX = 240;
struct WinRequest {
uint16_t type; // WinRequestType
uint16_t size; // payload bytes in use
int32_t windowId; // window the request is about (the sender must own it
// when posting); -1 if it was destroyed since
int32_t senderPid; // filled in by the kernel
uint32_t serial; // filled in by the kernel; increases per request
uint8_t payload[WIN_REQUEST_PAYLOAD_MAX];
};
static_assert(sizeof(WinRequest) == 256, "WinRequest is ABI");
enum WinRequestType : uint16_t {
WIN_REQ_LAUNCH_ITEM = 1, // WinReqLaunchItem
WIN_REQ_SET_PARENT = 2, // WinReqSetParent
WIN_REQ_DRAG_STATUS = 3, // WinReqDragStatus
WIN_REQ_ACTIVATE = 4, // no payload: raise and focus the window
// Types from here on are generated by the kernel; SYS_WIN_REQUEST
// refuses them so an app cannot forge one.
WIN_REQ_KERNEL_BASE = 0x8000,
WIN_REQ_DRAG_BEGIN = 0x8001, // WinReqDragBegin (from SYS_WIN_DRAG_BEGIN)
};
// Open a desktop catalog item (an app manifest id, or a built-in such as
// "desktop-settings"), as if the user had picked it from the launcher, so
// it runs with the authority the desktop would give it. `args` reach only
// apps whose manifest declares files.types (they take a document path);
// anything else starts without them, so a request cannot steer a
// privileged app through its command line.
struct WinReqLaunchItem {
char itemId[48];
char args[192];
};
// Mark the window as a dialog of another window owned by the same
// process: the compositor centres it over the parent.
struct WinReqSetParent {
int32_t parentWindowId;
};
// Answer to DRAG_ENTER/DRAG_OVER: the action this window would perform
// on a drop at the current position (0 = would refuse). Advisory; it
// drives the compositor's drag feedback.
struct WinReqDragStatus {
uint32_t serial;
uint8_t action; // one WIN_DRAG_* or 0
uint8_t _pad[3];
};
// Describes a drag for the compositor's feedback. Supplied by the app to
// SYS_WIN_DRAG_BEGIN and relayed (with the serial) as WIN_REQ_DRAG_BEGIN.
struct WinDragBegin {
uint8_t actions; // WIN_DRAG_* offered
uint8_t _pad[3];
uint32_t itemCount; // for a "3 items" badge; 0 = do not show
char label[64]; // short description, e.g. the file name
};
static_assert(sizeof(WinDragBegin) == 72, "WinDragBegin is ABI");
struct WinReqDragBegin {
uint32_t serial;
WinDragBegin info;
};
struct DevInfo {
uint8_t category; // 0=CPU, 1=Interrupt, 2=Timer, 3=Input, 4=USB, 5=Network, 6=Display, 7=Storage, 8=PCI, 9=Audio, 10=ACPI
uint8_t _pad[3];
+380
View File
@@ -0,0 +1,380 @@
/*
* app_catalog.hpp
* The app catalog shared by the desktop and Files: installed app manifests
* (0:/apps/<dir>/manifest.toml) plus the desktop's built-in settings pages
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <montauk/syscall.h>
#include <montauk/string.h>
#include <montauk/heap.h>
#include <montauk/toml.h>
#include <montauk/config.h>
#include <gui/desktop.hpp>
#include <gui/svg.hpp>
namespace gui {
// Built-in settings pages the desktop implements itself. Other processes
// list them by id and open them with a WIN_REQ_LAUNCH_ITEM request.
struct CatalogBuiltinPage {
const char* id;
const char* label;
const char* icon_path;
};
inline constexpr CatalogBuiltinPage kCatalogDesktopSettings = {
"desktop-settings", "Desktop Settings", "0:/os/icons/preferences-desktop-apps.svg"
};
inline constexpr CatalogBuiltinPage kCatalogUserManagement = {
"user-management", "User Management", "0:/os/icons/user-home.svg"
};
inline constexpr const CatalogBuiltinPage* kCatalogBuiltinPages[] = {
&kCatalogDesktopSettings, &kCatalogUserManagement,
};
// out = a + b + c (+ d + e), truncated to out_max.
inline void app_catalog_join(char* out, int out_max, const char* a, const char* b,
const char* c, const char* d = "", const char* e = "") {
const char* parts[5] = { a, b, c, d, e };
int n = 0;
for (const char* part : parts) {
for (int i = 0; part && part[i] && n < out_max - 1; i++) out[n++] = part[i];
}
if (out_max > 0) out[n] = '\0';
}
// Extract the basename from a readdir entry, tolerating both current
// direct-child names and paths returned by older ramdisk kernels.
inline void app_catalog_basename(char* out, int outSz, const char* entry) {
// Strip trailing slash
int len = montauk::slen(entry);
while (len > 0 && entry[len - 1] == '/') len--;
// Find last slash before that
int last_slash = -1;
for (int i = 0; i < len; i++) {
if (entry[i] == '/') last_slash = i;
}
const char* base = (last_slash >= 0) ? entry + last_slash + 1 : entry;
int base_len = len - (last_slash >= 0 ? last_slash + 1 : 0);
if (base_len >= outSz) base_len = outSz - 1;
montauk::memcpy(out, base, base_len);
out[base_len] = '\0';
}
// Return a slot for the next desktop item, doubling the array as needed.
// Returns nullptr if allocation failed.
inline DesktopItem* app_catalog_next_slot(DesktopState* ds) {
if (ds->desktop_item_count >= ds->desktop_item_capacity) {
int new_cap = ds->desktop_item_capacity ? ds->desktop_item_capacity * 2 : 32;
auto* grown = (DesktopItem*)montauk::realloc(
ds->desktop_items, (uint64_t)new_cap * sizeof(DesktopItem));
if (!grown) return nullptr;
ds->desktop_items = grown;
ds->desktop_item_capacity = new_cap;
}
return &ds->desktop_items[ds->desktop_item_count];
}
// Append every valid app manifest under 0:/apps to ds->desktop_items.
inline void app_catalog_add_manifest_apps(DesktopState* ds) {
// Ensure the apps directory exists
montauk::fmkdir("0:/apps");
// Page through the directory with readdir_at; a single readdir call
// returns at most one batch and would silently drop apps past it.
constexpr int BATCH = 128;
const char* entries[BATCH];
int scanned = 0;
int got;
while ((got = montauk::readdir_at("0:/apps", entries, BATCH, scanned)) > 0) {
scanned += got;
for (int i = 0; i < got; i++) {
// Strip the directory marker and tolerate old root-relative entries.
char dirname[64];
app_catalog_basename(dirname, sizeof(dirname), entries[i]);
if (dirname[0] == '\0') continue;
// Try to open the manifest in this app directory
char manifest_path[128];
app_catalog_join(manifest_path, sizeof(manifest_path), "0:/apps/", dirname, "/manifest.toml");
int fh = montauk::open(manifest_path);
if (fh < 0) continue;
uint64_t sz = montauk::getsize(fh);
if (sz == 0 || sz > 4096) { montauk::close(fh); continue; }
char* text = (char*)montauk::malloc(sz + 1);
if (!text) { montauk::close(fh); continue; }
int bytes_read = montauk::read(fh, (uint8_t*)text, 0, sz);
montauk::close(fh);
if (bytes_read != (int)sz) {
montauk::mfree(text);
continue;
}
text[sz] = '\0';
auto doc = montauk::toml::parse(text);
montauk::mfree(text);
// Read manifest fields
const char* id = doc.get_string("app.id", dirname);
const char* name = doc.get_string("app.name", "Unknown");
const char* binary = doc.get_string("app.binary", "");
const char* icon_file = doc.get_string("app.icon", "");
const char* category = doc.get_string("menu.category", "Productivity");
// "Applications" was the old catch-all; keep older manifests out of
// a stray custom category.
if (!category[0] || montauk::streq(category, "Applications"))
category = "Productivity";
bool visible = doc.get_bool("menu.visible", true);
bool launch_with_home = doc.get_bool("launch.pass_home_dir", true);
const char* section_name = doc.get_string("desktop.section", "apps");
bool admin_only = doc.get_bool("desktop.admin_only", false);
if (id[0] == '\0' || binary[0] == '\0') {
montauk::print("app catalog: skipping app manifest with missing id or binary\n");
doc.destroy();
continue;
}
bool duplicate = false;
for (int existing = 0; existing < ds->desktop_item_count; existing++) {
if (montauk::streq(ds->desktop_items[existing].id, id)) {
duplicate = true;
break;
}
}
if (duplicate) {
montauk::print("app catalog: skipping app manifest with duplicate id\n");
doc.destroy();
continue;
}
DesktopItemSection section = DESKTOP_ITEM_SECTION_APPS;
if (montauk::streq(section_name, "settings")) {
section = DESKTOP_ITEM_SECTION_SETTINGS;
} else if (montauk::streq(section_name, "hidden")) {
section = DESKTOP_ITEM_SECTION_HIDDEN;
} else if (!montauk::streq(section_name, "apps")) {
montauk::print("app catalog: skipping app manifest with invalid desktop.section\n");
doc.destroy();
continue;
}
DesktopItem* item = app_catalog_next_slot(ds);
if (!item) {
doc.destroy();
return;
}
montauk::memset(item, 0, sizeof(*item));
montauk::strncpy(item->id, id, sizeof(item->id));
montauk::strncpy(item->name, name, sizeof(item->name));
montauk::strncpy(item->category, category, sizeof(item->category));
item->menu_visible = visible;
item->launch_with_home = launch_with_home;
item->admin_only = admin_only;
item->launch_kind = DESKTOP_ITEM_LAUNCH_EXECUTABLE;
item->builtin_id = -1;
item->section = section;
// Build full binary path: 0:/apps/<dir>/<binary>
app_catalog_join(item->binary_path, sizeof(item->binary_path),
"0:/apps/", dirname, "/", binary);
// File types the app opens, used by Files for default apps and
// "Open With". Stored lowercased as " ext1 ext2 " for whole-word
// matching; entries that would overflow the buffer are dropped.
if (auto* types = doc.get_array("files.types")) {
int len = 0;
item->file_types[len++] = ' ';
for (int t = 0; t < types->array.count; t++) {
auto* v = types->array.items[t];
if (v->type != montauk::toml::Type::String || !v->str[0]) continue;
const char* ext = v->str[0] == '.' ? v->str + 1 : v->str;
int elen = montauk::slen(ext);
if (elen == 0 || len + elen + 2 > (int)sizeof(item->file_types)) continue;
for (int k = 0; k < elen; k++) {
char ch = ext[k];
item->file_types[len++] = (ch >= 'A' && ch <= 'Z') ? ch + 32 : ch;
}
item->file_types[len++] = ' ';
}
item->file_types[len < 2 ? 0 : len] = '\0';
}
// Load icon from app directory
item->icon = {};
item->icon_path[0] = '\0';
if (icon_file[0]) {
app_catalog_join(item->icon_path, sizeof(item->icon_path),
"0:/apps/", dirname, "/", icon_file);
Color defColor = colors::ICON_COLOR;
item->icon = svg_load(item->icon_path, 20, 20, defColor);
}
doc.destroy();
ds->desktop_item_count++;
}
}
}
inline int app_catalog_compare_ci(const char* a, const char* b) {
while (*a && *b) {
char ca = (*a >= 'A' && *a <= 'Z') ? (*a + 32) : *a;
char cb = (*b >= 'A' && *b <= 'Z') ? (*b + 32) : *b;
if (ca != cb) return ca - cb;
a++; b++;
}
return (unsigned char)*a - (unsigned char)*b;
}
// Indices of the catalog items in `section` the session may see, sorted by
// name.
inline int app_catalog_list_indices(DesktopState* ds, DesktopItemSection section,
int* out, int max_entries) {
if (!ds || !out || max_entries <= 0) return 0;
int written = 0;
for (int i = 0; i < ds->desktop_item_count && written < max_entries; i++) {
const DesktopItem& item = ds->desktop_items[i];
if (item.section != section) continue;
if (item.admin_only && !ds->is_admin) continue;
out[written++] = i;
}
for (int i = 1; i < written; i++) {
int tmp = out[i];
int j = i - 1;
while (j >= 0 && app_catalog_compare_ci(ds->desktop_items[tmp].name,
ds->desktop_items[out[j]].name) < 0) {
out[j + 1] = out[j];
j--;
}
out[j + 1] = tmp;
}
return written;
}
// ---- File associations ------------------------------------------------------
//
// Which app opens a file: the user's "Always use" choice from
// <home>/config/associations.toml ([open_with] ext = "app id"), else the
// first app (by name) whose manifest lists the extension in files.types.
inline constexpr const char* kAssociationsConfig = "associations";
inline bool app_catalog_launchable(const DesktopState* ds, const DesktopItem& item) {
if (item.launch_kind != DESKTOP_ITEM_LAUNCH_EXECUTABLE || !item.binary_path[0]) return false;
return !(item.admin_only && !ds->is_admin);
}
// file_types is stored as " ext1 ext2 ", so a whole-word match is " ext ".
inline bool app_catalog_claims(const DesktopItem& item, const char* ext) {
if (!ext || !ext[0] || !item.file_types[0]) return false;
int elen = montauk::slen(ext);
for (const char* p = item.file_types; *p; p++) {
if (*p != ' ') continue;
int k = 0;
while (k < elen && p[1 + k] == ext[k]) k++;
if (k == elen && p[1 + elen] == ' ') return true;
}
return false;
}
// The config writer emits bare keys, so only extensions made of bare-key
// characters can be remembered.
inline bool app_catalog_ext_storable(const char* ext) {
if (!ext || !ext[0] || montauk::slen(ext) > 15) return false;
for (const char* p = ext; *p; p++) {
char c = *p;
bool ok = (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_' || c == '-';
if (!ok) return false;
}
return true;
}
inline void app_catalog_assoc_key(char* out, int out_max, const char* ext) {
app_catalog_join(out, out_max, "open_with.", ext, "");
}
// Lowercased extension after the last '.', "" if none (".profile" has none).
inline void app_catalog_extension(const char* name, char* out, int out_max) {
if (!out || out_max <= 0) return;
out[0] = '\0';
if (!name) return;
const char* dot = nullptr;
for (const char* p = name; *p; p++) {
if (*p == '.') dot = p;
if (*p == '/') dot = nullptr;
}
if (!dot || dot == name || dot[-1] == '/' || !dot[1]) return;
int n = 0;
for (const char* p = dot + 1; *p && n < out_max - 1; p++) {
char ch = *p;
out[n++] = (ch >= 'A' && ch <= 'Z') ? ch + 32 : ch;
}
out[n] = '\0';
}
// Default app for ext as a desktop_items index, or -1.
inline int app_catalog_default_for(DesktopState* ds, const char* ext) {
if (!ds || !ext || !ext[0]) return -1;
if (ds->current_user[0] && app_catalog_ext_storable(ext)) {
auto doc = montauk::config::load_user(ds->current_user, kAssociationsConfig);
char key[48];
app_catalog_assoc_key(key, sizeof(key), ext);
const char* id = doc.get_string(key, "");
int found = -1;
for (int i = 0; id[0] && i < ds->desktop_item_count; i++) {
const DesktopItem& item = ds->desktop_items[i];
if (montauk::streq(item.id, id) && app_catalog_launchable(ds, item)) { found = i; break; }
}
doc.destroy();
if (found >= 0) return found;
}
// Several apps may claim an extension; pick by name so the choice does
// not depend on directory enumeration order.
int best = -1;
for (int i = 0; i < ds->desktop_item_count; i++) {
const DesktopItem& item = ds->desktop_items[i];
if (!app_catalog_launchable(ds, item) || !app_catalog_claims(item, ext)) continue;
if (best < 0 || app_catalog_compare_ci(item.name, ds->desktop_items[best].name) < 0) best = i;
}
return best;
}
// True if the file looks like text (no NULs / stray control bytes in its
// first KiB). Empty files count as text.
inline bool app_catalog_file_looks_textual(const char* path) {
int fd = montauk::open(path);
if (fd < 0) return false;
uint64_t size = montauk::getsize(fd);
if (size == 0) {
montauk::close(fd);
return true;
}
uint8_t buf[1024];
uint64_t want = size < sizeof(buf) ? size : sizeof(buf);
int got = montauk::read(fd, buf, 0, want);
montauk::close(fd);
if (got <= 0) return false;
// NUL or C0 control bytes other than tab/newline/CR/form feed/escape mean
// binary data. Bytes >= 0x80 pass: UTF-8 and the 1252 fallback are text.
for (int i = 0; i < got; i++) {
uint8_t b = buf[i];
if (b >= 0x20 || b == '\t' || b == '\n' || b == '\r' || b == 0x0C || b == 0x1B) continue;
return false;
}
return true;
}
} // namespace gui
+62 -1
View File
@@ -96,6 +96,18 @@ struct DesktopSettings {
int tz_offset_minutes; // timezone offset from UTC in minutes
};
// The standard folders in a home directory, in display order, with their
// icons (0:/os/icons). Shared by the desktop launcher and Files.
inline constexpr int SPECIAL_FOLDER_COUNT = 6;
inline constexpr const char* kSpecialFolderNames[SPECIAL_FOLDER_COUNT] = {
"Documents", "Desktop", "Music", "Videos", "Pictures", "Downloads"
};
inline constexpr const char* kSpecialFolderIcons[SPECIAL_FOLDER_COUNT] = {
"folder-blue-documents.svg", "folder-blue-desktop.svg",
"folder-blue-music.svg", "folder-blue-videos.svg",
"folder-blue-pictures.svg", "folder-blue-downloads.svg"
};
struct DesktopState {
Framebuffer fb;
Window windows[MAX_WINDOWS];
@@ -128,6 +140,55 @@ struct DesktopState {
// releases are delivered too) so mouse handlers can see Ctrl/Shift.
bool kbd_shift_down;
bool kbd_ctrl_down;
bool kbd_alt_down;
// Drag and drop between windows (desktop/messaging.cpp). The source app
// starts it with SYS_WIN_DRAG_BEGIN; the desktop routes the pointer.
struct {
bool active;
uint32_t serial;
int source_ext_id;
uint8_t actions; // WIN_DRAG_* offered by the source
uint32_t item_count;
char label[64];
int target_ext_id; // window under the pointer, -1 = none
uint8_t target_action; // last WIN_REQ_DRAG_STATUS answer
bool target_answered;
int last_x, last_y;
uint8_t last_modifiers;
bool over_desktop; // pointer over the background, not a window
} drag;
// The user's Desktop folder, shown as icons on the background
// (desktop/desktop_icons.cpp).
struct DesktopIcon {
char name[128];
bool is_dir;
};
DesktopIcon* desktop_icons;
int desktop_icon_count;
int desktop_icon_cap;
uint64_t desktop_icons_hash; // detects changes between rescans
uint64_t desktop_icons_next_scan; // ms
int desktop_icon_selected; // -1 = none
int desktop_icon_press; // icon the left button went down on, -1 = none
int desktop_icon_press_x, desktop_icon_press_y;
int desktop_icon_last_click;
uint64_t desktop_icon_last_click_ms;
// An incoming drag over the background: its file list (read once per
// serial) and the icon highlighted as the drop target.
uint32_t desktop_drop_serial;
char* desktop_drop_list;
int desktop_drop_icon;
// WIN_REQ_SET_PARENT requests whose dialog window has not been mapped
// yet; applied as soon as it appears.
struct {
int child_ext_id;
int parent_ext_id;
uint64_t expires_ms;
} pending_parents[8];
int pending_parent_count;
SvgIcon icon_terminal;
SvgIcon icon_filemanager;
@@ -168,7 +229,7 @@ struct DesktopState {
SvgIcon icon_system_configuration_lg;
// Special user folder icons (16x16 and 48x48)
static constexpr int SPECIAL_FOLDER_COUNT = 6;
static constexpr int SPECIAL_FOLDER_COUNT = gui::SPECIAL_FOLDER_COUNT;
SvgIcon icon_special_folder[SPECIAL_FOLDER_COUNT];
SvgIcon icon_special_folder_lg[SPECIAL_FOLDER_COUNT];
+237
View File
@@ -0,0 +1,237 @@
/*
* 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 <montauk/syscall.h>
#include <montauk/string.h>
#include <montauk/heap.h>
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 <typename Fn>
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
+10
View File
@@ -223,6 +223,14 @@ extern "C" {
#define MTK_SYS_FUTEX_WAKE 197
#define MTK_SYS_ABORT 198
#define MTK_SYS_EXIT_GROUP 199
#define MTK_SYS_CLIPBOARD_SET_DATA 200
#define MTK_SYS_CLIPBOARD_FORMATS 201
#define MTK_SYS_CLIPBOARD_GET_DATA 202
#define MTK_SYS_WIN_REQUEST 203
#define MTK_SYS_WIN_REQUEST_POLL 204
#define MTK_SYS_WIN_DRAG_BEGIN 205
#define MTK_SYS_WIN_DRAG_FORMATS 206
#define MTK_SYS_WIN_DRAG_GET_DATA 207
/* @SYSCALLS-END */
/* Syscall status codes (montauk/statuscodes.h for C++). */
@@ -246,6 +254,8 @@ extern "C" {
#define MTK_STATUS_OUT_OF_MEMORY (-14)
#define MTK_STATUS_TIMED_OUT (-15)
#define MTK_STATUS_WOULD_BLOCK (-16)
#define MTK_STATUS_NOT_FOUND (-17)
#define MTK_STATUS_ALREADY_EXISTS (-18)
/* @STATUS-END */
#define MTK_SOCK_TCP 1
+8 -2
View File
@@ -29,11 +29,13 @@ typedef enum {
BufferTooSmall = 13,
OutOfMemory = 14,
TimedOut = 15,
WouldBlock = 16
WouldBlock = 16,
NotFound = 17,
AlreadyExists = 18
} StatusCode;
// Highest defined StatusCode; update when adding a code above.
enum { MaxStatusCode = WouldBlock };
enum { MaxStatusCode = AlreadyExists };
inline const char* GetStatusText(StatusCode statusCode) {
const char* statusText[] = {
@@ -54,6 +56,8 @@ inline const char* GetStatusText(StatusCode statusCode) {
"Out of memory", // 14
"Timed out", // 15
"Operation would block", // 16
"Not found", // 17
"Already exists", // 18
};
if (statusCode > 0 && statusCode <= MaxStatusCode) {
@@ -90,6 +94,8 @@ inline const char* GetStatusExplanationText(StatusCode statusCode) {
0, // 14
0, // 15
0, // 16
0, // 17
"Something with that name is already there.", // 18
};
if (statusCode > 0 && statusCode <= MaxStatusCode) {
+36
View File
@@ -795,6 +795,20 @@ namespace montauk {
inline int clipboard_clear() {
return (int)syscall0(montauk::abi::SYS_CLIPBOARD_CLEAR);
}
// Multi-format clipboard. The text calls above operate on its
// TRANSFER_MIME_TEXT format; gui/transfer.hpp has friendlier helpers.
inline int clipboard_set_data(const montauk::abi::TransferItem* items, uint32_t count) {
return (int)syscall2(montauk::abi::SYS_CLIPBOARD_SET_DATA, (uint64_t)items, (uint64_t)count);
}
inline int clipboard_formats(montauk::abi::TransferFormatInfo* out, uint32_t max,
uint64_t* outSerial = nullptr) {
return (int)syscall3(montauk::abi::SYS_CLIPBOARD_FORMATS, (uint64_t)out, (uint64_t)max,
(uint64_t)outSerial);
}
inline int clipboard_get_data(const char* mime, void* buf, uint32_t bufLen, uint32_t* outLen) {
return (int)syscall4(montauk::abi::SYS_CLIPBOARD_GET_DATA, (uint64_t)mime, (uint64_t)buf,
(uint64_t)bufLen, (uint64_t)outLen);
}
// Window server
inline int win_create(const char* title, int w, int h, montauk::abi::WinCreateResult* result) {
@@ -837,4 +851,26 @@ namespace montauk {
return (int)syscall2(montauk::abi::SYS_WINSETFLAGS, (uint64_t)id, (uint64_t)flags);
}
// Window server messaging (see Api/Syscall.hpp, gui/transfer.hpp).
inline int win_request(int id, const montauk::abi::WinRequest* request) {
return (int)syscall2(montauk::abi::SYS_WIN_REQUEST, (uint64_t)id, (uint64_t)request);
}
inline int win_request_poll(montauk::abi::WinRequest* out) {
return (int)syscall1(montauk::abi::SYS_WIN_REQUEST_POLL, (uint64_t)out);
}
inline int win_drag_begin(int id, const montauk::abi::WinDragBegin* info,
const montauk::abi::TransferItem* items, uint32_t count) {
return (int)syscall4(montauk::abi::SYS_WIN_DRAG_BEGIN, (uint64_t)id, (uint64_t)info,
(uint64_t)items, (uint64_t)count);
}
inline int win_drag_formats(uint32_t serial, montauk::abi::TransferFormatInfo* out, uint32_t max) {
return (int)syscall3(montauk::abi::SYS_WIN_DRAG_FORMATS, (uint64_t)serial, (uint64_t)out,
(uint64_t)max);
}
inline int win_drag_get_data(uint32_t serial, const char* mime, void* buf, uint32_t bufLen,
uint32_t* outLen) {
return (int)syscall5(montauk::abi::SYS_WIN_DRAG_GET_DATA, (uint64_t)serial, (uint64_t)mime,
(uint64_t)buf, (uint64_t)bufLen, (uint64_t)outLen);
}
}