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

+1 -1
View File
@@ -12,4 +12,4 @@
#pragma once
#define MONTAUK_BUILD_NUMBER 7
#define MONTAUK_BUILD_NUMBER 16
+94 -71
View File
@@ -1,6 +1,6 @@
/*
* Clipboard.hpp
* SYS_CLIPBOARD_* plain-text clipboard syscalls
* SYS_CLIPBOARD_* syscalls: per-user multi-format clipboard
* Copyright (c) 2026 Daniel Hammer
*/
@@ -11,24 +11,25 @@
#include <CppLib/Spinlock.hpp>
#include "Syscall.hpp"
#include "DataTransfer.hpp"
namespace montauk::abi {
static constexpr int ClipboardMaxSlots = 32;
// One clipboard per user. It holds a multi-format transfer; the text
// syscalls read and replace its TRANSFER_MIME_TEXT format, so text-only
// apps and multi-format apps share the same clipboard.
struct ClipboardSlot {
bool inUse;
char user[32];
char* text;
uint32_t len;
DataTransfer::Transfer transfer;
uint64_t serial;
};
static ClipboardSlot g_clipboardSlots[ClipboardMaxSlots];
static kcp::Mutex g_clipboardLock;
static int Sys_ClipboardClear();
static const char* ClipboardCurrentUser() {
auto* proc = Sched::GetCurrentProcessPtr();
if (proc == nullptr) return nullptr;
@@ -81,57 +82,73 @@ namespace montauk::abi {
freeSlot->inUse = true;
ClipboardCopyUser(freeSlot->user, user);
freeSlot->text = nullptr;
freeSlot->len = 0;
freeSlot->transfer.count = 0;
freeSlot->serial = 0;
return freeSlot;
}
// Swap `incoming` into the caller's clipboard (it becomes empty) and free
// what was there. An empty transfer clears the clipboard.
static int ClipboardReplace(DataTransfer::Transfer& incoming) {
const char* currentUser = ClipboardCurrentUser();
if (currentUser == nullptr) {
DataTransfer::Free(incoming);
return GenericFailure;
}
char user[32];
ClipboardCopyUser(user, currentUser);
DataTransfer::Transfer old;
old.count = 0;
g_clipboardLock.Acquire();
ClipboardSlot* slot = ClipboardEnsureSlotLocked(user);
if (slot == nullptr) {
g_clipboardLock.Release();
DataTransfer::Free(incoming);
return Busy;
}
old = slot->transfer;
slot->transfer = incoming;
slot->serial = ClipboardNextSerial(slot->serial);
g_clipboardLock.Release();
incoming.count = 0;
DataTransfer::Free(old);
return Success;
}
static int Sys_ClipboardClear() {
DataTransfer::Transfer empty;
empty.count = 0;
return ClipboardReplace(empty);
}
static int Sys_ClipboardSetText(const char* data, uint32_t len) {
if (len == 0)
return Sys_ClipboardClear();
if (data == nullptr || len > CLIPBOARD_MAX_TEXT_BYTES)
return InvalidArgument;
if (Memory::g_heap == nullptr)
return OutOfMemory;
for (uint32_t i = 0; i < len; i++) {
if (data[i] == '\0')
return InvalidArgument;
}
const char* currentUser = ClipboardCurrentUser();
if (currentUser == nullptr) return GenericFailure;
DataTransfer::Transfer transfer;
int rc = DataTransfer::BuildText(data, len, transfer);
if (rc != Success) return rc;
return ClipboardReplace(transfer);
}
char user[32];
ClipboardCopyUser(user, currentUser);
char* newText = (char*)Memory::g_heap->Request(len + 1);
if (newText == nullptr) return OutOfMemory;
for (uint32_t i = 0; i < len; i++)
newText[i] = data[i];
newText[len] = '\0';
char* oldText = nullptr;
g_clipboardLock.Acquire();
ClipboardSlot* slot = ClipboardEnsureSlotLocked(user);
if (slot == nullptr) {
g_clipboardLock.Release();
Memory::g_heap->Free(newText);
return -1;
}
oldText = slot->text;
slot->text = newText;
slot->len = len;
slot->serial = ClipboardNextSerial(slot->serial);
g_clipboardLock.Release();
if (oldText != nullptr)
Memory::g_heap->Free(oldText);
return Success;
static int Sys_ClipboardSetData(const TransferItem* items, uint32_t count) {
if (count == 0) return Sys_ClipboardClear();
DataTransfer::Transfer transfer;
int rc = DataTransfer::BuildFromUser(items, count, transfer);
if (rc != Success) return rc;
return ClipboardReplace(transfer);
}
static int Sys_ClipboardGetInfo(ClipboardInfo* out) {
@@ -150,7 +167,8 @@ namespace montauk::abi {
g_clipboardLock.Acquire();
ClipboardSlot* slot = ClipboardFindSlotLocked(user);
if (slot != nullptr) {
out->textBytes = slot->len;
const DataTransfer::Format* text = DataTransfer::Find(slot->transfer, TRANSFER_MIME_TEXT);
out->textBytes = text ? text->len : 0;
out->serial = slot->serial;
}
g_clipboardLock.Release();
@@ -171,52 +189,57 @@ namespace montauk::abi {
g_clipboardLock.Acquire();
ClipboardSlot* slot = ClipboardFindSlotLocked(user);
if (slot == nullptr || slot->len == 0) {
if (slot != nullptr && outSerial != nullptr)
*outSerial = slot->serial;
if (slot != nullptr && outSerial != nullptr)
*outSerial = slot->serial;
const DataTransfer::Format* text =
slot ? DataTransfer::Find(slot->transfer, TRANSFER_MIME_TEXT) : nullptr;
if (text == nullptr || text->len == 0) {
g_clipboardLock.Release();
return Success;
}
*outLen = slot->len;
if (outSerial != nullptr) *outSerial = slot->serial;
if (buffer == nullptr || bufferLen < slot->len) {
g_clipboardLock.Release();
return BufferTooSmall;
}
for (uint32_t i = 0; i < slot->len; i++)
buffer[i] = slot->text[i];
int rc = DataTransfer::CopyOut(*text, buffer, bufferLen, outLen);
g_clipboardLock.Release();
return Success;
return rc;
}
static int Sys_ClipboardClear() {
static int Sys_ClipboardFormats(TransferFormatInfo* out, uint32_t max, uint64_t* outSerial) {
if (outSerial != nullptr) *outSerial = 0;
const char* currentUser = ClipboardCurrentUser();
if (currentUser == nullptr) return GenericFailure;
char user[32];
ClipboardCopyUser(user, currentUser);
char* oldText = nullptr;
g_clipboardLock.Acquire();
ClipboardSlot* slot = ClipboardFindSlotLocked(user);
uint32_t n = 0;
if (slot != nullptr) {
if (outSerial != nullptr) *outSerial = slot->serial;
n = out ? DataTransfer::ListFormats(slot->transfer, out, max) : 0;
if (out == nullptr || max == 0) n = slot->transfer.count;
}
g_clipboardLock.Release();
return (int)n;
}
static int Sys_ClipboardGetData(const char* mime, void* buffer, uint32_t bufferLen, uint32_t* outLen) {
if (outLen == nullptr) return InvalidArgument;
*outLen = 0;
const char* currentUser = ClipboardCurrentUser();
if (currentUser == nullptr) return GenericFailure;
char user[32];
ClipboardCopyUser(user, currentUser);
g_clipboardLock.Acquire();
ClipboardSlot* slot = ClipboardEnsureSlotLocked(user);
if (slot == nullptr) {
g_clipboardLock.Release();
return -1;
}
oldText = slot->text;
slot->text = nullptr;
slot->len = 0;
slot->serial = ClipboardNextSerial(slot->serial);
ClipboardSlot* slot = ClipboardFindSlotLocked(user);
const DataTransfer::Format* format = slot ? DataTransfer::Find(slot->transfer, mime) : nullptr;
int rc = format ? DataTransfer::CopyOut(*format, buffer, bufferLen, outLen) : NotFound;
g_clipboardLock.Release();
if (oldText != nullptr)
Memory::g_heap->Free(oldText);
return Success;
return rc;
}
}
+156
View File
@@ -0,0 +1,156 @@
/*
* DataTransfer.cpp
* Multi-format payloads shared by the clipboard and drag and drop
* Copyright (c) 2026 Daniel Hammer
*/
#include "DataTransfer.hpp"
#include "UserMemory.hpp"
#include <Memory/Heap.hpp>
namespace DataTransfer {
using namespace montauk::abi;
static bool MimeEquals(const char* a, const char* b) {
int i = 0;
for (; a[i] && b[i]; i++) {
char ca = (a[i] >= 'A' && a[i] <= 'Z') ? a[i] + 32 : a[i];
char cb = (b[i] >= 'A' && b[i] <= 'Z') ? b[i] + 32 : b[i];
if (ca != cb) return false;
}
return a[i] == b[i];
}
// Length of the "type/subtype" part, before any ";parameters".
static int MimeBaseLen(const char* mime) {
int n = 0;
while (mime[n] && mime[n] != ';') n++;
while (n > 0 && mime[n - 1] == ' ') n--;
return n;
}
static bool MimeBaseEquals(const char* a, const char* b) {
int la = MimeBaseLen(a);
int lb = MimeBaseLen(b);
if (la != lb) return false;
for (int i = 0; i < la; i++) {
char ca = (a[i] >= 'A' && a[i] <= 'Z') ? a[i] + 32 : a[i];
char cb = (b[i] >= 'A' && b[i] <= 'Z') ? b[i] + 32 : b[i];
if (ca != cb) return false;
}
return true;
}
bool ValidMime(const char* mime) {
if (mime == nullptr) return false;
bool slash = false;
uint32_t i = 0;
for (; mime[i]; i++) {
if (i + 1 >= TRANSFER_MIME_MAX) return false;
char c = mime[i];
if (c < 0x20 || c > 0x7E) return false;
if (c == '/') slash = true;
}
return i > 0 && slash;
}
void Free(Transfer& transfer) {
for (uint32_t i = 0; i < transfer.count; i++) {
if (transfer.formats[i].data != nullptr) Memory::g_heap->Free(transfer.formats[i].data);
transfer.formats[i].data = nullptr;
transfer.formats[i].len = 0;
}
transfer.count = 0;
}
static int AddFormat(Transfer& transfer, const char* mime, const uint8_t* data, uint32_t len) {
if (transfer.count >= TRANSFER_MAX_FORMATS) return InvalidArgument;
Format& f = transfer.formats[transfer.count];
uint32_t m = 0;
for (; mime[m] && m + 1 < TRANSFER_MIME_MAX; m++) f.mime[m] = mime[m];
f.mime[m] = '\0';
// Never hand out a null pointer for an empty format; one byte keeps
// "present but empty" distinct from "absent".
f.data = (uint8_t*)Memory::g_heap->Request(len > 0 ? len : 1);
if (f.data == nullptr) return OutOfMemory;
for (uint32_t i = 0; i < len; i++) f.data[i] = data[i];
f.len = len;
transfer.count++;
return Success;
}
int BuildFromUser(const TransferItem* items, uint32_t count, Transfer& out) {
out.count = 0;
if (items == nullptr || count == 0 || count > TRANSFER_MAX_FORMATS) return InvalidArgument;
if (Memory::g_heap == nullptr) return OutOfMemory;
// Validate everything before allocating anything.
uint64_t total = 0;
for (uint32_t i = 0; i < count; i++) {
if (!UserMemory::String(items[i].mime, TRANSFER_MIME_MAX)) return BadPointer;
if (!ValidMime((const char*)items[i].mime)) return InvalidArgument;
if (items[i].len > 0 && !UserMemory::Range(items[i].data, items[i].len, false)) return BadPointer;
total += items[i].len;
for (uint32_t j = 0; j < i; j++) {
if (MimeEquals((const char*)items[i].mime, (const char*)items[j].mime))
return InvalidArgument;
}
}
if (total > TRANSFER_MAX_BYTES) return InvalidArgument;
for (uint32_t i = 0; i < count; i++) {
int rc = AddFormat(out, (const char*)items[i].mime,
(const uint8_t*)items[i].data, items[i].len);
if (rc != Success) {
Free(out);
return rc;
}
}
return Success;
}
int BuildText(const char* text, uint32_t len, Transfer& out) {
out.count = 0;
if (Memory::g_heap == nullptr) return OutOfMemory;
int rc = AddFormat(out, TRANSFER_MIME_TEXT, (const uint8_t*)text, len);
if (rc != Success) Free(out);
return rc;
}
const Format* Find(const Transfer& transfer, const char* mime) {
if (mime == nullptr) return nullptr;
for (uint32_t i = 0; i < transfer.count; i++) {
if (MimeEquals(transfer.formats[i].mime, mime)) return &transfer.formats[i];
}
bool has_params = false;
for (int i = 0; mime[i]; i++) {
if (mime[i] == ';') { has_params = true; break; }
}
if (has_params) return nullptr;
for (uint32_t i = 0; i < transfer.count; i++) {
if (MimeBaseEquals(transfer.formats[i].mime, mime)) return &transfer.formats[i];
}
return nullptr;
}
uint32_t ListFormats(const Transfer& transfer, TransferFormatInfo* out, uint32_t max) {
uint32_t n = 0;
for (; n < transfer.count && n < max; n++) {
for (uint32_t c = 0; c < TRANSFER_MIME_MAX; c++) out[n].mime[c] = transfer.formats[n].mime[c];
out[n].len = transfer.formats[n].len;
out[n]._reserved = 0;
}
return n;
}
int CopyOut(const Format& format, void* buffer, uint32_t bufferLen, uint32_t* outLen) {
if (outLen != nullptr) *outLen = format.len;
if (format.len == 0) return Success;
if (buffer == nullptr || bufferLen < format.len) return BufferTooSmall;
uint8_t* dst = (uint8_t*)buffer;
for (uint32_t i = 0; i < format.len; i++) dst[i] = format.data[i];
return Success;
}
}
+52
View File
@@ -0,0 +1,52 @@
/*
* DataTransfer.hpp
* Multi-format payloads shared by the clipboard and drag and drop
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include "Syscall.hpp"
// A transfer is a small set of formats, each one MIME type plus bytes, all
// owned by the kernel heap. Producers offer every representation they can
// (a file list and its paths as text, say) and each consumer reads the one it
// understands. The clipboard holds one transfer per user; the window server
// holds the active drag's.
namespace DataTransfer {
struct Format {
char mime[montauk::abi::TRANSFER_MIME_MAX];
uint8_t* data;
uint32_t len;
};
struct Transfer {
Format formats[montauk::abi::TRANSFER_MAX_FORMATS];
uint32_t count;
};
// Copy `count` formats in from user memory, validating every pointer the
// items carry (the item array itself must already be validated). On
// failure `out` is left empty. Returns a StatusCodes value.
int BuildFromUser(const montauk::abi::TransferItem* items, uint32_t count, Transfer& out);
// A transfer holding just `text` as TRANSFER_MIME_TEXT.
int BuildText(const char* text, uint32_t len, Transfer& out);
void Free(Transfer& transfer);
// Exact match first; a request without parameters ("text/plain") also
// matches a stored format that has them ("text/plain;charset=utf-8").
const Format* Find(const Transfer& transfer, const char* mime);
uint32_t ListFormats(const Transfer& transfer, montauk::abi::TransferFormatInfo* out, uint32_t max);
// Copy a format to a (validated) user buffer. *outLen always receives the
// full length; BufferTooSmall when it does not fit, nothing copied then.
int CopyOut(const Format& format, void* buffer, uint32_t bufferLen, uint32_t* outLen);
// MIME string checks shared by every entry point: "type/subtype" with
// optional parameters, printable ASCII, shorter than TRANSFER_MIME_MAX.
bool ValidMime(const char* mime);
}
+47 -2
View File
@@ -31,7 +31,7 @@
#include "Device.hpp" // SYS_DEVLIST, SYS_DISKINFO
#include "Input.hpp" // SYS_INPUT_WAIT
#include "Storage.hpp" // SYS_PARTLIST, SYS_VOLINFO, SYS_DISKREAD, SYS_DISKWRITE
#include "Window.hpp" // SYS_WINCREATE, SYS_WINDESTROY, SYS_WINPRESENT, SYS_WINPOLL, SYS_WINENUM, SYS_WINMAP, SYS_WINSENDEVENT, SYS_WINRESIZE, SYS_WINSETCURSOR, SYS_WINSETFLAGS, SYS_WINSETSCALE, SYS_WINGETSCALE
#include "Window.hpp" // SYS_WIN_REQUEST*, SYS_WIN_DRAG_*, SYS_WINCREATE, SYS_WINDESTROY, SYS_WINPRESENT, SYS_WINPOLL, SYS_WINENUM, SYS_WINMAP, SYS_WINSENDEVENT, SYS_WINRESIZE, SYS_WINSETCURSOR, SYS_WINSETFLAGS, SYS_WINSETSCALE, SYS_WINGETSCALE
#include "Audio.hpp" // SYS_AUDIOOPEN, SYS_AUDIOCLOSE, SYS_AUDIOWRITE, SYS_AUDIOCTL
#include "BluetoothSyscall.hpp" // SYS_BTSCAN, SYS_BTCONNECT, SYS_BTDISCONNECT, SYS_BTLIST, SYS_BTINFO
#include "Usb.hpp" // generic process-owned USB interface access
@@ -39,7 +39,7 @@
#include "IpcSyscall.hpp" // SYS_DUPHANDLE, SYS_WAIT_HANDLE, SYS_STREAM_CREATE, SYS_STREAM_READ, SYS_STREAM_WRITE, SYS_MAILBOX_CREATE, SYS_MAILBOX_SEND, SYS_MAILBOX_RECV, SYS_WAITSET_CREATE, SYS_WAITSET_ADD, SYS_WAITSET_REMOVE, SYS_WAITSET_WAIT, SYS_PROC_OPEN, SYS_SURFACE_CREATE, SYS_SURFACE_MAP, SYS_SURFACE_RESIZE
#include "LibSyscall.hpp" // SYS_LOAD_LIB, SYS_UNLOAD_LIB, SYS_DLSYM
#include "CrashReportSyscall.hpp" // SYS_CRASH_REPORT
#include "Clipboard.hpp" // SYS_CLIPBOARD_*
#include "Clipboard.hpp" // SYS_CLIPBOARD_* (text and multi-format)
#include "UserMemory.hpp"
// Assembly entry point
@@ -726,6 +726,51 @@ namespace montauk::abi {
UserMemory::IsUserPtr(frame->arg4) ? (uint64_t*)frame->arg4 : nullptr);
case SYS_CLIPBOARD_CLEAR:
return Sys_ClipboardClear();
case SYS_CLIPBOARD_SET_DATA:
if (frame->arg2 > TRANSFER_MAX_FORMATS) return InvalidArgument;
if (!UserMemory::Range(frame->arg1, frame->arg2 * sizeof(TransferItem), false)) return BadPointer;
return Sys_ClipboardSetData((const TransferItem*)frame->arg1, (uint32_t)frame->arg2);
case SYS_CLIPBOARD_FORMATS: {
uint64_t max = frame->arg2 > TRANSFER_MAX_FORMATS ? TRANSFER_MAX_FORMATS : frame->arg2;
if (!UserMemory::OptionalRange(frame->arg1, max * sizeof(TransferFormatInfo), true)) return BadPointer;
if (!UserMemory::OptionalRange(frame->arg3, sizeof(uint64_t), true)) return BadPointer;
return Sys_ClipboardFormats(frame->arg1 ? (TransferFormatInfo*)frame->arg1 : nullptr,
(uint32_t)max,
frame->arg3 ? (uint64_t*)frame->arg3 : nullptr);
}
case SYS_CLIPBOARD_GET_DATA:
if (!UserMemory::String(frame->arg1, TRANSFER_MIME_MAX)) return BadPointer;
if (frame->arg3 > 0 && !UserMemory::Range(frame->arg2, frame->arg3, true)) return BadPointer;
if (!UserMemory::Writable<uint32_t>(frame->arg4)) return BadPointer;
return Sys_ClipboardGetData((const char*)frame->arg1,
frame->arg3 > 0 ? (void*)frame->arg2 : nullptr,
(uint32_t)frame->arg3, (uint32_t*)frame->arg4);
case SYS_WIN_REQUEST:
if (!UserMemory::Readable<WinRequest>(frame->arg2)) return BadPointer;
return Sys_WinRequest((int)frame->arg1, (const WinRequest*)frame->arg2);
case SYS_WIN_REQUEST_POLL:
if (!UserMemory::Writable<WinRequest>(frame->arg1)) return BadPointer;
return Sys_WinRequestPoll((WinRequest*)frame->arg1);
case SYS_WIN_DRAG_BEGIN:
if (!UserMemory::Readable<WinDragBegin>(frame->arg2)) return BadPointer;
if (frame->arg4 == 0 || frame->arg4 > TRANSFER_MAX_FORMATS) return InvalidArgument;
if (!UserMemory::Range(frame->arg3, frame->arg4 * sizeof(TransferItem), false)) return BadPointer;
return Sys_WinDragBegin((int)frame->arg1, (const WinDragBegin*)frame->arg2,
(const TransferItem*)frame->arg3, (uint32_t)frame->arg4);
case SYS_WIN_DRAG_FORMATS: {
uint64_t max = frame->arg3 > TRANSFER_MAX_FORMATS ? TRANSFER_MAX_FORMATS : frame->arg3;
if (!UserMemory::OptionalRange(frame->arg2, max * sizeof(TransferFormatInfo), true)) return BadPointer;
return Sys_WinDragFormats((uint32_t)frame->arg1,
frame->arg2 ? (TransferFormatInfo*)frame->arg2 : nullptr,
(uint32_t)max);
}
case SYS_WIN_DRAG_GET_DATA:
if (!UserMemory::String(frame->arg2, TRANSFER_MIME_MAX)) return BadPointer;
if (frame->arg4 > 0 && !UserMemory::Range(frame->arg3, frame->arg4, true)) return BadPointer;
if (!UserMemory::Writable<uint32_t>(frame->arg5)) return BadPointer;
return Sys_WinDragGetData((uint32_t)frame->arg1, (const char*)frame->arg2,
frame->arg4 > 0 ? (void*)frame->arg3 : nullptr,
(uint32_t)frame->arg4, (uint32_t*)frame->arg5);
case SYS_INPUT_WAIT:
return (int64_t)Sys_InputWait(frame->arg1, frame->arg2);
case SYS_LOG_WRITE: {
+163 -2
View File
@@ -386,6 +386,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;
@@ -706,14 +719,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");
@@ -737,6 +788,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 MemStats {
uint64_t totalBytes;
uint64_t freeBytes;
+194 -2
View File
@@ -73,7 +73,14 @@ namespace WinServer {
return result;
}
static void OrphanRequestsLocked(int windowId);
static void ReleaseSlotResourcesLocked(WindowSlot& slot, bool unmapOwner) {
// Queued requests stay deliverable (a dialog may post "open this
// app" and close at once), but no longer name a window that could be
// handed to someone else.
OrphanRequestsLocked((int)(&slot - g_slots));
if (unmapOwner && slot.liveSurface != nullptr) {
auto* owner = Sched::GetProcessByPid(slot.ownerPid);
if (owner != nullptr) {
@@ -94,13 +101,57 @@ namespace WinServer {
ResetSlotLocked(slot);
}
// ---- Messaging state ----
static constexpr int MaxRequests = 64;
// One app may not hold more than this many queued requests, so a single
// misbehaving process cannot starve the others out of the queue.
static constexpr int MaxRequestsPerSender = 16;
static constexpr int MaxDragReaders = 8;
static montauk::abi::WinRequest g_requests[MaxRequests];
static int g_requestHead = 0; // oldest
static int g_requestCount = 0;
static uint32_t g_requestSerial = 0;
struct DragState {
bool active;
uint32_t serial;
int sourceWindow;
int sourcePid;
DataTransfer::Transfer transfer;
int readers[MaxDragReaders];
int readerCount;
};
static DragState g_drag;
static uint32_t g_dragSerial = 0;
static bool IsDragEvent(uint8_t type) {
return type >= montauk::abi::WIN_EVENT_DRAG_ENTER && type <= montauk::abi::WIN_EVENT_DRAG_END;
}
static void GrantDragReaderLocked(uint32_t serial, int pid) {
if (!g_drag.active || g_drag.serial != serial || pid <= 0) return;
if (pid == g_drag.sourcePid) return;
for (int i = 0; i < g_drag.readerCount; i++) {
if (g_drag.readers[i] == pid) return;
}
if (g_drag.readerCount < MaxDragReaders) g_drag.readers[g_drag.readerCount++] = pid;
}
static int SendEventLocked(int windowId, const montauk::abi::WinEvent* event) {
if (windowId < 0 || windowId >= MaxWindows || event == nullptr) return InvalidArgument;
WindowSlot& slot = g_slots[windowId];
if (!slot.used || slot.eventMailbox == nullptr) return InvalidArgument;
int rc = (event->type == 1)
? Ipc::MailboxSendCoalescedMouse(slot.eventMailbox, 0, event, sizeof(*event))
// Delivering a drag event is what lets the receiving app read the
// drag's data: it needs the formats to decide whether to accept.
if (IsDragEvent(event->type)) GrantDragReaderLocked(event->drag.serial, slot.ownerPid);
bool motion = event->type == montauk::abi::WIN_EVENT_MOUSE ||
event->type == montauk::abi::WIN_EVENT_DRAG_OVER;
int rc = motion
? Ipc::MailboxSendCoalescedMotion(slot.eventMailbox, 0, event, sizeof(*event))
: Ipc::MailboxSend(slot.eventMailbox, 0, event, sizeof(*event));
if (rc < 0) return GenericFailure;
return rc > 0 ? Success : Busy;
@@ -376,12 +427,153 @@ namespace WinServer {
return g_uiScale;
}
static void OrphanRequestsLocked(int windowId) {
for (int i = 0; i < g_requestCount; i++) {
montauk::abi::WinRequest& r = g_requests[(g_requestHead + i) % MaxRequests];
if (r.windowId == windowId) r.windowId = -1;
}
}
static void EndDragLocked() {
DataTransfer::Free(g_drag.transfer);
g_drag.active = false;
g_drag.readerCount = 0;
}
void CleanupProcess(int pid) {
WsGuard guard;
for (int i = 0; i < MaxWindows; i++) {
if (!g_slots[i].used || g_slots[i].ownerPid != pid) continue;
ReleaseSlotResourcesLocked(g_slots[i], false);
}
// Requests the process already posted are still delivered: they are
// messages in flight, and a process may well exit right after its
// last one ("open this file" from a dialog-only Files).
if (g_drag.active && g_drag.sourcePid == pid) EndDragLocked();
}
static int EnqueueRequestLocked(montauk::abi::WinRequest& request) {
if (g_requestCount >= MaxRequests) return WouldBlock;
request.serial = ++g_requestSerial;
if (request.serial == 0) request.serial = ++g_requestSerial;
g_requests[(g_requestHead + g_requestCount) % MaxRequests] = request;
g_requestCount++;
return Success;
}
static bool OwnsWindowLocked(int windowId, int pid) {
return windowId >= 0 && windowId < MaxWindows && g_slots[windowId].used &&
g_slots[windowId].ownerPid == pid;
}
int PostRequest(int windowId, int callerPid, const montauk::abi::WinRequest& request) {
using namespace montauk::abi;
if (request.type == 0 || request.type >= WIN_REQ_KERNEL_BASE) return InvalidArgument;
if (request.size > WIN_REQUEST_PAYLOAD_MAX) return InvalidArgument;
WsGuard guard;
if (!OwnsWindowLocked(windowId, callerPid)) return PermissionDenied;
int mine = 0;
for (int i = 0; i < g_requestCount; i++) {
if (g_requests[(g_requestHead + i) % MaxRequests].senderPid == callerPid) mine++;
}
if (mine >= MaxRequestsPerSender) return WouldBlock;
WinRequest copy = request;
copy.windowId = windowId;
copy.senderPid = callerPid;
// Bytes past `size` are not the sender's business to pass along.
for (uint32_t i = copy.size; i < WIN_REQUEST_PAYLOAD_MAX; i++) copy.payload[i] = 0;
return EnqueueRequestLocked(copy);
}
int PollRequest(montauk::abi::WinRequest* out) {
WsGuard guard;
while (g_requestCount > 0) {
montauk::abi::WinRequest& r = g_requests[g_requestHead];
g_requestHead = (g_requestHead + 1) % MaxRequests;
g_requestCount--;
// A destroyed window's requests arrive with windowId -1 (see
// OrphanRequestsLocked); anything else must still match.
if (r.windowId >= 0 && !OwnsWindowLocked(r.windowId, r.senderPid)) continue;
*out = r;
return 1;
}
return 0;
}
int DragBegin(int windowId, int callerPid, bool compositor,
const montauk::abi::WinDragBegin& info, DataTransfer::Transfer& transfer) {
using namespace montauk::abi;
WsGuard guard;
// The compositor drags from its own surface (desktop icons) with
// windowId -1; it already knows about such a drag, so no request.
bool fromSurface = compositor && windowId == -1;
if (!fromSurface && !OwnsWindowLocked(windowId, callerPid)) {
DataTransfer::Free(transfer);
return PermissionDenied;
}
uint32_t serial = ++g_dragSerial;
if (serial == 0) serial = ++g_dragSerial;
if (!fromSurface) {
WinRequest request = {};
request.type = WIN_REQ_DRAG_BEGIN;
request.size = sizeof(WinReqDragBegin);
request.windowId = windowId;
request.senderPid = callerPid;
WinReqDragBegin* payload = (WinReqDragBegin*)request.payload;
payload->serial = serial;
payload->info = info;
payload->info.label[sizeof(payload->info.label) - 1] = '\0';
int rc = EnqueueRequestLocked(request);
if (rc != Success) {
DataTransfer::Free(transfer);
return rc;
}
}
// A new drag replaces the previous one (only one pointer exists).
if (g_drag.active) EndDragLocked();
g_drag.active = true;
g_drag.serial = serial;
g_drag.sourceWindow = windowId;
g_drag.sourcePid = callerPid;
g_drag.transfer = transfer;
g_drag.readerCount = 0;
transfer.count = 0;
return (int)serial;
}
static bool DragReadableLocked(uint32_t serial, int callerPid, bool compositor) {
if (!g_drag.active || g_drag.serial != serial) return false;
if (compositor || callerPid == g_drag.sourcePid) return true;
for (int i = 0; i < g_drag.readerCount; i++) {
if (g_drag.readers[i] == callerPid) return true;
}
return false;
}
int DragFormats(uint32_t serial, int callerPid, bool compositor,
montauk::abi::TransferFormatInfo* out, uint32_t max) {
WsGuard guard;
if (!g_drag.active || g_drag.serial != serial) return NotFound;
if (!DragReadableLocked(serial, callerPid, compositor)) return PermissionDenied;
if (out == nullptr || max == 0) return (int)g_drag.transfer.count;
return (int)DataTransfer::ListFormats(g_drag.transfer, out, max);
}
int DragGetData(uint32_t serial, int callerPid, bool compositor, const char* mime,
void* buffer, uint32_t bufferLen, uint32_t* outLen) {
WsGuard guard;
if (outLen != nullptr) *outLen = 0;
if (!g_drag.active || g_drag.serial != serial) return NotFound;
if (!DragReadableLocked(serial, callerPid, compositor)) return PermissionDenied;
const DataTransfer::Format* format = DataTransfer::Find(g_drag.transfer, mime);
if (format == nullptr) return NotFound;
return DataTransfer::CopyOut(*format, buffer, bufferLen, outLen);
}
}
+23
View File
@@ -8,6 +8,7 @@
#include "Syscall.hpp"
#include <cstdint>
#include <Ipc/Ipc.hpp>
#include "DataTransfer.hpp"
namespace WinServer {
@@ -44,4 +45,26 @@ namespace WinServer {
int SetScale(int scale);
int GetScale();
// ---- Messaging (see "Window server messaging" in Api/Syscall.hpp) ----
// Queue an app request about one of the caller's own windows.
int PostRequest(int windowId, int callerPid, const montauk::abi::WinRequest& request);
// Pop the oldest request (compositor only; the caller checks the cap).
// Returns 1 with *out filled, or 0 when the queue is empty.
int PollRequest(montauk::abi::WinRequest* out);
// Start a drag from the caller's window, or (compositor only, windowId
// -1) from the desktop surface itself. Takes ownership of `transfer` (it
// is emptied either way) and returns the new drag serial (> 0) or a
// negative StatusCodes value.
int DragBegin(int windowId, int callerPid, bool compositor,
const montauk::abi::WinDragBegin& info, DataTransfer::Transfer& transfer);
// Read the drag's formats or data. Allowed for the source process, the
// compositor, and processes the compositor has sent a drag event for this
// serial to.
int DragFormats(uint32_t serial, int callerPid, bool compositor,
montauk::abi::TransferFormatInfo* out, uint32_t max);
int DragGetData(uint32_t serial, int callerPid, bool compositor, const char* mime,
void* buffer, uint32_t bufferLen, uint32_t* outLen);
}
+40 -1
View File
@@ -2,7 +2,9 @@
* Window.hpp
* SYS_WINCREATE, SYS_WINDESTROY, SYS_WINPRESENT, SYS_WINPOLL,
* SYS_WINENUM, SYS_WINMAP, SYS_WINSENDEVENT, SYS_WINRESIZE,
* SYS_WINSETCURSOR, SYS_WINSETFLAGS, SYS_WINSETSCALE, SYS_WINGETSCALE syscalls
* SYS_WINSETCURSOR, SYS_WINSETFLAGS, SYS_WINSETSCALE, SYS_WINGETSCALE syscalls,
* and window server messaging: SYS_WIN_REQUEST, SYS_WIN_REQUEST_POLL,
* SYS_WIN_DRAG_BEGIN, SYS_WIN_DRAG_FORMATS, SYS_WIN_DRAG_GET_DATA
* Copyright (c) 2026 Daniel Hammer
*/
@@ -92,4 +94,41 @@ namespace montauk::abi {
static int Sys_WinGetScale() {
return WinServer::GetScale();
}
// Any app may post requests, but only about its own windows (checked in
// WinServer); only the compositor may read them back.
static int Sys_WinRequest(int windowId, const WinRequest* request) {
if (request == nullptr) return InvalidArgument;
WinRequest copy = *request;
return WinServer::PostRequest(windowId, Sched::GetCurrentPid(), copy);
}
static int Sys_WinRequestPoll(WinRequest* out) {
if (!Sched::HasCapability(CAP_COMPOSITOR)) return CapabilityDenied;
if (out == nullptr) return InvalidArgument;
return WinServer::PollRequest(out);
}
static int Sys_WinDragBegin(int windowId, const WinDragBegin* info,
const TransferItem* items, uint32_t count) {
if (info == nullptr) return InvalidArgument;
WinDragBegin infoCopy = *info;
DataTransfer::Transfer transfer;
int rc = DataTransfer::BuildFromUser(items, count, transfer);
if (rc != Success) return rc;
return WinServer::DragBegin(windowId, Sched::GetCurrentPid(),
Sched::HasCapability(CAP_COMPOSITOR), infoCopy, transfer);
}
static int Sys_WinDragFormats(uint32_t serial, TransferFormatInfo* out, uint32_t max) {
return WinServer::DragFormats(serial, Sched::GetCurrentPid(),
Sched::HasCapability(CAP_COMPOSITOR), out, max);
}
static int Sys_WinDragGetData(uint32_t serial, const char* mime, void* buffer,
uint32_t bufferLen, uint32_t* outLen) {
return WinServer::DragGetData(serial, Sched::GetCurrentPid(),
Sched::HasCapability(CAP_COMPOSITOR), mime,
buffer, bufferLen, outLen);
}
};
+3 -1
View File
@@ -25,5 +25,7 @@ enum StatusCodes {
BufferTooSmall = -13,
OutOfMemory = -14,
TimedOut = -15,
WouldBlock = -16
WouldBlock = -16,
NotFound = -17,
AlreadyExists = -18
};
+12 -10
View File
@@ -1003,16 +1003,21 @@ namespace Ipc {
return MailboxSendInternal(mailbox, -1, msgType, data, len, -1);
}
int MailboxSendCoalescedMouse(Mailbox* mailbox, uint32_t msgType, const void* data, uint16_t len) {
int MailboxSendCoalescedMotion(Mailbox* mailbox, uint32_t msgType, const void* data, uint16_t len) {
if (mailbox == nullptr || data == nullptr) return -1;
if (len != sizeof(montauk::abi::WinEvent)) return MailboxSend(mailbox, msgType, data, len);
const montauk::abi::WinEvent* incoming = (const montauk::abi::WinEvent*)data;
if (incoming->type != 1 || incoming->mouse.scroll != 0)
return MailboxSend(mailbox, msgType, data, len);
// Plain pointer motion (mouse moves, drag-over updates) only matters
// in its latest state, so a queued one is overwritten rather than
// stacking up. Clicks, scrolls and every other event queue normally.
auto isMotion = [](const montauk::abi::WinEvent* e) {
if (e->type == montauk::abi::WIN_EVENT_DRAG_OVER) return true;
return e->type == montauk::abi::WIN_EVENT_MOUSE && e->mouse.scroll == 0 &&
e->mouse.buttons == e->mouse.prev_buttons;
};
bool incomingTransition = incoming->mouse.buttons != incoming->mouse.prev_buttons;
if (incomingTransition)
const montauk::abi::WinEvent* incoming = (const montauk::abi::WinEvent*)data;
if (!isMotion(incoming))
return MailboxSend(mailbox, msgType, data, len);
mailbox->lock.Acquire();
@@ -1026,10 +1031,7 @@ namespace Ipc {
MailboxMessage& latest = mailbox->messages[idx];
if (latest.type == msgType && latest.size == len && !latest.hasAttachment) {
const montauk::abi::WinEvent* queued = (const montauk::abi::WinEvent*)latest.data;
bool queuedTransition = queued->type != 1 ||
queued->mouse.scroll != 0 ||
queued->mouse.buttons != queued->mouse.prev_buttons;
if (!queuedTransition) {
if (isMotion(queued) && queued->type == incoming->type) {
memcpy(latest.data, data, len);
mailbox->lock.Release();
NotifyObjectChanged((Object*)mailbox);
+1 -1
View File
@@ -114,7 +114,7 @@ namespace Ipc {
void RetainMailbox(Mailbox* mailbox, bool sender, bool receiver);
void ReleaseMailbox(Mailbox* mailbox, bool sender, bool receiver);
int MailboxSend(Mailbox* mailbox, uint32_t msgType, const void* data, uint16_t len);
int MailboxSendCoalescedMouse(Mailbox* mailbox, uint32_t msgType, const void* data, uint16_t len);
int MailboxSendCoalescedMotion(Mailbox* mailbox, uint32_t msgType, const void* data, uint16_t len);
int MailboxRecv(Mailbox* mailbox, uint32_t* msgType, void* data, uint16_t* inOutLen, bool nonBlocking);
int MailboxSendHandle(int handle, uint32_t msgType, const void* data, uint16_t len, int attachHandle);
int MailboxRecvHandle(int handle, uint32_t* msgType, void* data, uint16_t* inOutLen, int* outAttachHandle);