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:
1 parent
07191b1286
commit
d3e49a925d
66 files changed
+6855
-905
No files matched your search
@@ -12,4 +12,4 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#define MONTAUK_BUILD_NUMBER 7
|
#define MONTAUK_BUILD_NUMBER 16
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Clipboard.hpp
|
* Clipboard.hpp
|
||||||
* SYS_CLIPBOARD_* plain-text clipboard syscalls
|
* SYS_CLIPBOARD_* syscalls: per-user multi-format clipboard
|
||||||
* Copyright (c) 2026 Daniel Hammer
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -11,24 +11,25 @@
|
|||||||
#include <CppLib/Spinlock.hpp>
|
#include <CppLib/Spinlock.hpp>
|
||||||
|
|
||||||
#include "Syscall.hpp"
|
#include "Syscall.hpp"
|
||||||
|
#include "DataTransfer.hpp"
|
||||||
|
|
||||||
namespace montauk::abi {
|
namespace montauk::abi {
|
||||||
|
|
||||||
static constexpr int ClipboardMaxSlots = 32;
|
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 {
|
struct ClipboardSlot {
|
||||||
bool inUse;
|
bool inUse;
|
||||||
char user[32];
|
char user[32];
|
||||||
char* text;
|
DataTransfer::Transfer transfer;
|
||||||
uint32_t len;
|
|
||||||
uint64_t serial;
|
uint64_t serial;
|
||||||
};
|
};
|
||||||
|
|
||||||
static ClipboardSlot g_clipboardSlots[ClipboardMaxSlots];
|
static ClipboardSlot g_clipboardSlots[ClipboardMaxSlots];
|
||||||
static kcp::Mutex g_clipboardLock;
|
static kcp::Mutex g_clipboardLock;
|
||||||
|
|
||||||
static int Sys_ClipboardClear();
|
|
||||||
|
|
||||||
static const char* ClipboardCurrentUser() {
|
static const char* ClipboardCurrentUser() {
|
||||||
auto* proc = Sched::GetCurrentProcessPtr();
|
auto* proc = Sched::GetCurrentProcessPtr();
|
||||||
if (proc == nullptr) return nullptr;
|
if (proc == nullptr) return nullptr;
|
||||||
@@ -81,57 +82,73 @@ namespace montauk::abi {
|
|||||||
|
|
||||||
freeSlot->inUse = true;
|
freeSlot->inUse = true;
|
||||||
ClipboardCopyUser(freeSlot->user, user);
|
ClipboardCopyUser(freeSlot->user, user);
|
||||||
freeSlot->text = nullptr;
|
freeSlot->transfer.count = 0;
|
||||||
freeSlot->len = 0;
|
|
||||||
freeSlot->serial = 0;
|
freeSlot->serial = 0;
|
||||||
return freeSlot;
|
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) {
|
static int Sys_ClipboardSetText(const char* data, uint32_t len) {
|
||||||
if (len == 0)
|
if (len == 0)
|
||||||
return Sys_ClipboardClear();
|
return Sys_ClipboardClear();
|
||||||
if (data == nullptr || len > CLIPBOARD_MAX_TEXT_BYTES)
|
if (data == nullptr || len > CLIPBOARD_MAX_TEXT_BYTES)
|
||||||
return InvalidArgument;
|
return InvalidArgument;
|
||||||
if (Memory::g_heap == nullptr)
|
|
||||||
return OutOfMemory;
|
|
||||||
|
|
||||||
for (uint32_t i = 0; i < len; i++) {
|
for (uint32_t i = 0; i < len; i++) {
|
||||||
if (data[i] == '\0')
|
if (data[i] == '\0')
|
||||||
return InvalidArgument;
|
return InvalidArgument;
|
||||||
}
|
}
|
||||||
|
|
||||||
const char* currentUser = ClipboardCurrentUser();
|
DataTransfer::Transfer transfer;
|
||||||
if (currentUser == nullptr) return GenericFailure;
|
int rc = DataTransfer::BuildText(data, len, transfer);
|
||||||
|
if (rc != Success) return rc;
|
||||||
|
return ClipboardReplace(transfer);
|
||||||
|
}
|
||||||
|
|
||||||
char user[32];
|
static int Sys_ClipboardSetData(const TransferItem* items, uint32_t count) {
|
||||||
ClipboardCopyUser(user, currentUser);
|
if (count == 0) return Sys_ClipboardClear();
|
||||||
|
DataTransfer::Transfer transfer;
|
||||||
char* newText = (char*)Memory::g_heap->Request(len + 1);
|
int rc = DataTransfer::BuildFromUser(items, count, transfer);
|
||||||
if (newText == nullptr) return OutOfMemory;
|
if (rc != Success) return rc;
|
||||||
for (uint32_t i = 0; i < len; i++)
|
return ClipboardReplace(transfer);
|
||||||
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_ClipboardGetInfo(ClipboardInfo* out) {
|
static int Sys_ClipboardGetInfo(ClipboardInfo* out) {
|
||||||
@@ -150,7 +167,8 @@ namespace montauk::abi {
|
|||||||
g_clipboardLock.Acquire();
|
g_clipboardLock.Acquire();
|
||||||
ClipboardSlot* slot = ClipboardFindSlotLocked(user);
|
ClipboardSlot* slot = ClipboardFindSlotLocked(user);
|
||||||
if (slot != nullptr) {
|
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;
|
out->serial = slot->serial;
|
||||||
}
|
}
|
||||||
g_clipboardLock.Release();
|
g_clipboardLock.Release();
|
||||||
@@ -171,52 +189,57 @@ namespace montauk::abi {
|
|||||||
|
|
||||||
g_clipboardLock.Acquire();
|
g_clipboardLock.Acquire();
|
||||||
ClipboardSlot* slot = ClipboardFindSlotLocked(user);
|
ClipboardSlot* slot = ClipboardFindSlotLocked(user);
|
||||||
if (slot == nullptr || slot->len == 0) {
|
if (slot != nullptr && outSerial != nullptr)
|
||||||
if (slot != nullptr && outSerial != nullptr)
|
*outSerial = slot->serial;
|
||||||
*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();
|
g_clipboardLock.Release();
|
||||||
return Success;
|
return Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
*outLen = slot->len;
|
int rc = DataTransfer::CopyOut(*text, buffer, bufferLen, outLen);
|
||||||
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];
|
|
||||||
g_clipboardLock.Release();
|
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();
|
const char* currentUser = ClipboardCurrentUser();
|
||||||
if (currentUser == nullptr) return GenericFailure;
|
if (currentUser == nullptr) return GenericFailure;
|
||||||
|
|
||||||
char user[32];
|
char user[32];
|
||||||
ClipboardCopyUser(user, currentUser);
|
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();
|
g_clipboardLock.Acquire();
|
||||||
ClipboardSlot* slot = ClipboardEnsureSlotLocked(user);
|
ClipboardSlot* slot = ClipboardFindSlotLocked(user);
|
||||||
if (slot == nullptr) {
|
const DataTransfer::Format* format = slot ? DataTransfer::Find(slot->transfer, mime) : nullptr;
|
||||||
g_clipboardLock.Release();
|
int rc = format ? DataTransfer::CopyOut(*format, buffer, bufferLen, outLen) : NotFound;
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
oldText = slot->text;
|
|
||||||
slot->text = nullptr;
|
|
||||||
slot->len = 0;
|
|
||||||
slot->serial = ClipboardNextSerial(slot->serial);
|
|
||||||
g_clipboardLock.Release();
|
g_clipboardLock.Release();
|
||||||
|
return rc;
|
||||||
if (oldText != nullptr)
|
|
||||||
Memory::g_heap->Free(oldText);
|
|
||||||
|
|
||||||
return Success;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
#include "Device.hpp" // SYS_DEVLIST, SYS_DISKINFO
|
#include "Device.hpp" // SYS_DEVLIST, SYS_DISKINFO
|
||||||
#include "Input.hpp" // SYS_INPUT_WAIT
|
#include "Input.hpp" // SYS_INPUT_WAIT
|
||||||
#include "Storage.hpp" // SYS_PARTLIST, SYS_VOLINFO, SYS_DISKREAD, SYS_DISKWRITE
|
#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 "Audio.hpp" // SYS_AUDIOOPEN, SYS_AUDIOCLOSE, SYS_AUDIOWRITE, SYS_AUDIOCTL
|
||||||
#include "BluetoothSyscall.hpp" // SYS_BTSCAN, SYS_BTCONNECT, SYS_BTDISCONNECT, SYS_BTLIST, SYS_BTINFO
|
#include "BluetoothSyscall.hpp" // SYS_BTSCAN, SYS_BTCONNECT, SYS_BTDISCONNECT, SYS_BTLIST, SYS_BTINFO
|
||||||
#include "Usb.hpp" // generic process-owned USB interface access
|
#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 "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 "LibSyscall.hpp" // SYS_LOAD_LIB, SYS_UNLOAD_LIB, SYS_DLSYM
|
||||||
#include "CrashReportSyscall.hpp" // SYS_CRASH_REPORT
|
#include "CrashReportSyscall.hpp" // SYS_CRASH_REPORT
|
||||||
#include "Clipboard.hpp" // SYS_CLIPBOARD_*
|
#include "Clipboard.hpp" // SYS_CLIPBOARD_* (text and multi-format)
|
||||||
#include "UserMemory.hpp"
|
#include "UserMemory.hpp"
|
||||||
|
|
||||||
// Assembly entry point
|
// Assembly entry point
|
||||||
@@ -726,6 +726,51 @@ namespace montauk::abi {
|
|||||||
UserMemory::IsUserPtr(frame->arg4) ? (uint64_t*)frame->arg4 : nullptr);
|
UserMemory::IsUserPtr(frame->arg4) ? (uint64_t*)frame->arg4 : nullptr);
|
||||||
case SYS_CLIPBOARD_CLEAR:
|
case SYS_CLIPBOARD_CLEAR:
|
||||||
return Sys_ClipboardClear();
|
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:
|
case SYS_INPUT_WAIT:
|
||||||
return (int64_t)Sys_InputWait(frame->arg1, frame->arg2);
|
return (int64_t)Sys_InputWait(frame->arg1, frame->arg2);
|
||||||
case SYS_LOG_WRITE: {
|
case SYS_LOG_WRITE: {
|
||||||
|
|||||||
+163
-2
@@ -386,6 +386,19 @@ namespace montauk::abi {
|
|||||||
thread ends the process rather than only that thread. */
|
thread ends the process rather than only that thread. */
|
||||||
static constexpr uint64_t SYS_EXIT_GROUP = 199; // (exitCode) -> does not return
|
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
|
/* Kernel-owned process capabilities. User identities may namespace
|
||||||
per-user resources, but never participate in authorization decisions. */
|
per-user resources, but never participate in authorization decisions. */
|
||||||
static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0;
|
static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0;
|
||||||
@@ -706,14 +719,52 @@ namespace montauk::abi {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Window server shared types
|
// 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 {
|
struct WinEvent {
|
||||||
uint8_t type; // 0=key, 1=mouse, 2=resize, 3=close, 4=scale
|
uint8_t type; // WinEventType
|
||||||
uint8_t _pad[3];
|
uint8_t _pad[3];
|
||||||
union {
|
union {
|
||||||
KeyEvent key; // 16 bytes: the union was already that size (mouse)
|
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 w, h; } resize;
|
||||||
struct { int32_t scale; } scale;
|
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");
|
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
|
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 {
|
struct MemStats {
|
||||||
uint64_t totalBytes;
|
uint64_t totalBytes;
|
||||||
uint64_t freeBytes;
|
uint64_t freeBytes;
|
||||||
|
|||||||
@@ -73,7 +73,14 @@ namespace WinServer {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void OrphanRequestsLocked(int windowId);
|
||||||
|
|
||||||
static void ReleaseSlotResourcesLocked(WindowSlot& slot, bool unmapOwner) {
|
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) {
|
if (unmapOwner && slot.liveSurface != nullptr) {
|
||||||
auto* owner = Sched::GetProcessByPid(slot.ownerPid);
|
auto* owner = Sched::GetProcessByPid(slot.ownerPid);
|
||||||
if (owner != nullptr) {
|
if (owner != nullptr) {
|
||||||
@@ -94,13 +101,57 @@ namespace WinServer {
|
|||||||
ResetSlotLocked(slot);
|
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) {
|
static int SendEventLocked(int windowId, const montauk::abi::WinEvent* event) {
|
||||||
if (windowId < 0 || windowId >= MaxWindows || event == nullptr) return InvalidArgument;
|
if (windowId < 0 || windowId >= MaxWindows || event == nullptr) return InvalidArgument;
|
||||||
WindowSlot& slot = g_slots[windowId];
|
WindowSlot& slot = g_slots[windowId];
|
||||||
if (!slot.used || slot.eventMailbox == nullptr) return InvalidArgument;
|
if (!slot.used || slot.eventMailbox == nullptr) return InvalidArgument;
|
||||||
|
|
||||||
int rc = (event->type == 1)
|
// Delivering a drag event is what lets the receiving app read the
|
||||||
? Ipc::MailboxSendCoalescedMouse(slot.eventMailbox, 0, event, sizeof(*event))
|
// 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));
|
: Ipc::MailboxSend(slot.eventMailbox, 0, event, sizeof(*event));
|
||||||
if (rc < 0) return GenericFailure;
|
if (rc < 0) return GenericFailure;
|
||||||
return rc > 0 ? Success : Busy;
|
return rc > 0 ? Success : Busy;
|
||||||
@@ -376,12 +427,153 @@ namespace WinServer {
|
|||||||
return g_uiScale;
|
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) {
|
void CleanupProcess(int pid) {
|
||||||
WsGuard guard;
|
WsGuard guard;
|
||||||
for (int i = 0; i < MaxWindows; i++) {
|
for (int i = 0; i < MaxWindows; i++) {
|
||||||
if (!g_slots[i].used || g_slots[i].ownerPid != pid) continue;
|
if (!g_slots[i].used || g_slots[i].ownerPid != pid) continue;
|
||||||
ReleaseSlotResourcesLocked(g_slots[i], false);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
#include "Syscall.hpp"
|
#include "Syscall.hpp"
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <Ipc/Ipc.hpp>
|
#include <Ipc/Ipc.hpp>
|
||||||
|
#include "DataTransfer.hpp"
|
||||||
|
|
||||||
namespace WinServer {
|
namespace WinServer {
|
||||||
|
|
||||||
@@ -44,4 +45,26 @@ namespace WinServer {
|
|||||||
int SetScale(int scale);
|
int SetScale(int scale);
|
||||||
int GetScale();
|
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);
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,9 @@
|
|||||||
* Window.hpp
|
* Window.hpp
|
||||||
* SYS_WINCREATE, SYS_WINDESTROY, SYS_WINPRESENT, SYS_WINPOLL,
|
* SYS_WINCREATE, SYS_WINDESTROY, SYS_WINPRESENT, SYS_WINPOLL,
|
||||||
* SYS_WINENUM, SYS_WINMAP, SYS_WINSENDEVENT, SYS_WINRESIZE,
|
* 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
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -92,4 +94,41 @@ namespace montauk::abi {
|
|||||||
static int Sys_WinGetScale() {
|
static int Sys_WinGetScale() {
|
||||||
return WinServer::GetScale();
|
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);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
@@ -25,5 +25,7 @@ enum StatusCodes {
|
|||||||
BufferTooSmall = -13,
|
BufferTooSmall = -13,
|
||||||
OutOfMemory = -14,
|
OutOfMemory = -14,
|
||||||
TimedOut = -15,
|
TimedOut = -15,
|
||||||
WouldBlock = -16
|
WouldBlock = -16,
|
||||||
|
NotFound = -17,
|
||||||
|
AlreadyExists = -18
|
||||||
};
|
};
|
||||||
+12
-10
@@ -1003,16 +1003,21 @@ namespace Ipc {
|
|||||||
return MailboxSendInternal(mailbox, -1, msgType, data, len, -1);
|
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 (mailbox == nullptr || data == nullptr) return -1;
|
||||||
if (len != sizeof(montauk::abi::WinEvent)) return MailboxSend(mailbox, msgType, data, len);
|
if (len != sizeof(montauk::abi::WinEvent)) return MailboxSend(mailbox, msgType, data, len);
|
||||||
|
|
||||||
const montauk::abi::WinEvent* incoming = (const montauk::abi::WinEvent*)data;
|
// Plain pointer motion (mouse moves, drag-over updates) only matters
|
||||||
if (incoming->type != 1 || incoming->mouse.scroll != 0)
|
// in its latest state, so a queued one is overwritten rather than
|
||||||
return MailboxSend(mailbox, msgType, data, len);
|
// 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;
|
const montauk::abi::WinEvent* incoming = (const montauk::abi::WinEvent*)data;
|
||||||
if (incomingTransition)
|
if (!isMotion(incoming))
|
||||||
return MailboxSend(mailbox, msgType, data, len);
|
return MailboxSend(mailbox, msgType, data, len);
|
||||||
|
|
||||||
mailbox->lock.Acquire();
|
mailbox->lock.Acquire();
|
||||||
@@ -1026,10 +1031,7 @@ namespace Ipc {
|
|||||||
MailboxMessage& latest = mailbox->messages[idx];
|
MailboxMessage& latest = mailbox->messages[idx];
|
||||||
if (latest.type == msgType && latest.size == len && !latest.hasAttachment) {
|
if (latest.type == msgType && latest.size == len && !latest.hasAttachment) {
|
||||||
const montauk::abi::WinEvent* queued = (const montauk::abi::WinEvent*)latest.data;
|
const montauk::abi::WinEvent* queued = (const montauk::abi::WinEvent*)latest.data;
|
||||||
bool queuedTransition = queued->type != 1 ||
|
if (isMotion(queued) && queued->type == incoming->type) {
|
||||||
queued->mouse.scroll != 0 ||
|
|
||||||
queued->mouse.buttons != queued->mouse.prev_buttons;
|
|
||||||
if (!queuedTransition) {
|
|
||||||
memcpy(latest.data, data, len);
|
memcpy(latest.data, data, len);
|
||||||
mailbox->lock.Release();
|
mailbox->lock.Release();
|
||||||
NotifyObjectChanged((Object*)mailbox);
|
NotifyObjectChanged((Object*)mailbox);
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ namespace Ipc {
|
|||||||
void RetainMailbox(Mailbox* mailbox, bool sender, bool receiver);
|
void RetainMailbox(Mailbox* mailbox, bool sender, bool receiver);
|
||||||
void ReleaseMailbox(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 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 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 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);
|
int MailboxRecvHandle(int handle, uint32_t* msgType, void* data, uint16_t* inOutLen, int* outAttachHandle);
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ BINDIR := bin
|
|||||||
PROGRAMS := $(notdir $(wildcard src/*))
|
PROGRAMS := $(notdir $(wildcard src/*))
|
||||||
|
|
||||||
# Programs with custom Makefiles (built separately).
|
# Programs with custom Makefiles (built separately).
|
||||||
CUSTOM_BUILDS := 2048 fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network display capabilities keyboard sshserver terminal syslog procmgr powermgr calculator charmap desktop login shell paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs libloader crashpad sshd rtlsdr sdr
|
CUSTOM_BUILDS := 2048 files fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network display capabilities keyboard sshserver terminal syslog procmgr powermgr calculator charmap desktop login shell paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs libloader crashpad sshd rtlsdr sdr
|
||||||
SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS))
|
SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS))
|
||||||
|
|
||||||
# Build targets: system programs go to bin/os/, apps go to bin/apps/<name>/.
|
# Build targets: system programs go to bin/os/, apps go to bin/apps/<name>/.
|
||||||
@@ -96,9 +96,9 @@ WPDIR := data/wallpapers
|
|||||||
WPSRC := $(wildcard $(WPDIR)/*.jpg)
|
WPSRC := $(wildcard $(WPDIR)/*.jpg)
|
||||||
WPDST := $(patsubst $(WPDIR)/%,$(BINDIR)/os/wallpapers/%,$(WPSRC))
|
WPDST := $(patsubst $(WPDIR)/%,$(BINDIR)/os/wallpapers/%,$(WPSRC))
|
||||||
|
|
||||||
.PHONY: all clean 2048 fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network display capabilities keyboard sshserver sshd terminal syslog procmgr powermgr calculator charmap login desktop shell paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs icons fonts configs osdata bearssl libc tls libjpeg libjpegwrite install-apps libloader crashpad rtlsdr sdr check-syscalls gen-syscalls rust
|
.PHONY: all clean 2048 files fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network display capabilities keyboard sshserver sshd terminal syslog procmgr powermgr calculator charmap login desktop shell paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs icons fonts configs osdata bearssl libc tls libjpeg libjpegwrite install-apps libloader crashpad rtlsdr sdr check-syscalls gen-syscalls rust
|
||||||
|
|
||||||
all: bearssl libc libjpeg libjpegwrite tls libloader rtlsdr sdr devkit $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network display capabilities sshserver terminal syslog procmgr powermgr calculator charmap 2048 paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs login desktop shell sshd icons fonts install-apps crashpad rust $(MANDST) $(WWWDST) $(CA_CERTS) $(CONFIGDST) $(OSDATADST) $(FWDST) $(LICDST) $(WPDST)
|
all: bearssl libc libjpeg libjpegwrite tls libloader rtlsdr sdr devkit $(TARGETS) files fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network display capabilities sshserver terminal syslog procmgr powermgr calculator charmap 2048 paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs login desktop shell sshd icons fonts install-apps crashpad rust $(MANDST) $(WWWDST) $(CA_CERTS) $(CONFIGDST) $(OSDATADST) $(FWDST) $(LICDST) $(WPDST)
|
||||||
|
|
||||||
# Build BearSSL static library (cross-compiled for freestanding x86_64).
|
# Build BearSSL static library (cross-compiled for freestanding x86_64).
|
||||||
BEARSSL_INCLUDES := -isystem $(shell cd .. && pwd)/kernel/freestnd-c-hdrs/x86_64/include -isystem $(abspath include/libc)
|
BEARSSL_INCLUDES := -isystem $(shell cd .. && pwd)/kernel/freestnd-c-hdrs/x86_64/include -isystem $(abspath include/libc)
|
||||||
@@ -307,6 +307,10 @@ paint: libc
|
|||||||
texteditor: libc libloader dialogs
|
texteditor: libc libloader dialogs
|
||||||
$(MAKE) -C src/texteditor
|
$(MAKE) -C src/texteditor
|
||||||
|
|
||||||
|
# Build Files (file manager; shares the desktop's app headers).
|
||||||
|
files: libc bearssl libjpeg
|
||||||
|
$(MAKE) -C src/files
|
||||||
|
|
||||||
# Build Mandelbrot standalone GUI app (depends on libc).
|
# Build Mandelbrot standalone GUI app (depends on libc).
|
||||||
mandelbrot: libc
|
mandelbrot: libc
|
||||||
$(MAKE) -C src/mandelbrot
|
$(MAKE) -C src/mandelbrot
|
||||||
@@ -391,7 +395,7 @@ crashpad: libc
|
|||||||
$(MAKE) -C src/crashpad
|
$(MAKE) -C src/crashpad
|
||||||
|
|
||||||
# Install app bundles (manifests, icons, data files) into bin/apps/<name>/.
|
# Install app bundles (manifests, icons, data files) into bin/apps/<name>/.
|
||||||
install-apps: 2048 paint spreadsheet wordprocessor weather wikipedia imageviewer fontpreview pdfviewer disks devexplorer installer audio music video bluetooth network display capabilities keyboard sshserver terminal syslog procmgr powermgr calculator charmap screenshot texteditor mandelbrot printers timezone crashpad
|
install-apps: 2048 files paint spreadsheet wordprocessor weather wikipedia imageviewer fontpreview pdfviewer disks devexplorer installer audio music video bluetooth network display capabilities keyboard sshserver terminal syslog procmgr powermgr calculator charmap screenshot texteditor mandelbrot printers timezone crashpad
|
||||||
../scripts/install_apps.sh
|
../scripts/install_apps.sh
|
||||||
|
|
||||||
# Copy man pages into bin/os/man/ so mkramdisk.sh picks them up.
|
# Copy man pages into bin/os/man/ so mkramdisk.sh picks them up.
|
||||||
@@ -534,6 +538,7 @@ clean:
|
|||||||
$(MAKE) -C src/wordprocessor clean
|
$(MAKE) -C src/wordprocessor clean
|
||||||
$(MAKE) -C src/pdfviewer clean
|
$(MAKE) -C src/pdfviewer clean
|
||||||
$(MAKE) -C src/disks clean
|
$(MAKE) -C src/disks clean
|
||||||
|
$(MAKE) -C src/files clean
|
||||||
$(MAKE) -C src/devexplorer clean
|
$(MAKE) -C src/devexplorer clean
|
||||||
$(MAKE) -C src/installer clean
|
$(MAKE) -C src/installer clean
|
||||||
$(MAKE) -C src/audio clean
|
$(MAKE) -C src/audio clean
|
||||||
|
|||||||
@@ -48,6 +48,13 @@ delegable = [
|
|||||||
"display_admin", "device_admin", "log_read", "brightness",
|
"display_admin", "device_admin", "log_read", "brightness",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Files used to run inside the desktop with its authority; this keeps what
|
||||||
|
# admins could do there (edit 0:/config files). Bounded by what the desktop
|
||||||
|
# may delegate, so a standard session still gets nothing.
|
||||||
|
[grant.files]
|
||||||
|
path = "0:/apps/files/files.elf"
|
||||||
|
effective = ["user_admin", "device_admin"]
|
||||||
|
|
||||||
[grant.terminal]
|
[grant.terminal]
|
||||||
path = "0:/apps/terminal/terminal.elf"
|
path = "0:/apps/terminal/terminal.elf"
|
||||||
delegable = ["all"]
|
delegable = ["all"]
|
||||||
|
|||||||
@@ -299,6 +299,19 @@ namespace montauk::abi {
|
|||||||
thread ends the process rather than only that thread. */
|
thread ends the process rather than only that thread. */
|
||||||
static constexpr uint64_t SYS_EXIT_GROUP = 199; // (exitCode) -> does not return
|
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
|
/* Kernel-owned process capabilities. User identities may namespace
|
||||||
per-user resources, but never participate in authorization decisions. */
|
per-user resources, but never participate in authorization decisions. */
|
||||||
static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0;
|
static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0;
|
||||||
@@ -637,14 +650,52 @@ namespace montauk::abi {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Window server shared types
|
// 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 {
|
struct WinEvent {
|
||||||
uint8_t type; // 0=key, 1=mouse, 2=resize, 3=close, 4=scale
|
uint8_t type; // WinEventType
|
||||||
uint8_t _pad[3];
|
uint8_t _pad[3];
|
||||||
union {
|
union {
|
||||||
KeyEvent key; // 16 bytes: the union was already that size (mouse)
|
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 w, h; } resize;
|
||||||
struct { int32_t scale; } scale;
|
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");
|
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
|
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 {
|
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 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];
|
uint8_t _pad[3];
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -96,6 +96,18 @@ struct DesktopSettings {
|
|||||||
int tz_offset_minutes; // timezone offset from UTC in minutes
|
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 {
|
struct DesktopState {
|
||||||
Framebuffer fb;
|
Framebuffer fb;
|
||||||
Window windows[MAX_WINDOWS];
|
Window windows[MAX_WINDOWS];
|
||||||
@@ -128,6 +140,55 @@ struct DesktopState {
|
|||||||
// releases are delivered too) so mouse handlers can see Ctrl/Shift.
|
// releases are delivered too) so mouse handlers can see Ctrl/Shift.
|
||||||
bool kbd_shift_down;
|
bool kbd_shift_down;
|
||||||
bool kbd_ctrl_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_terminal;
|
||||||
SvgIcon icon_filemanager;
|
SvgIcon icon_filemanager;
|
||||||
@@ -168,7 +229,7 @@ struct DesktopState {
|
|||||||
SvgIcon icon_system_configuration_lg;
|
SvgIcon icon_system_configuration_lg;
|
||||||
|
|
||||||
// Special user folder icons (16x16 and 48x48)
|
// 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[SPECIAL_FOLDER_COUNT];
|
||||||
SvgIcon icon_special_folder_lg[SPECIAL_FOLDER_COUNT];
|
SvgIcon icon_special_folder_lg[SPECIAL_FOLDER_COUNT];
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -223,6 +223,14 @@ extern "C" {
|
|||||||
#define MTK_SYS_FUTEX_WAKE 197
|
#define MTK_SYS_FUTEX_WAKE 197
|
||||||
#define MTK_SYS_ABORT 198
|
#define MTK_SYS_ABORT 198
|
||||||
#define MTK_SYS_EXIT_GROUP 199
|
#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 */
|
/* @SYSCALLS-END */
|
||||||
|
|
||||||
/* Syscall status codes (montauk/statuscodes.h for C++). */
|
/* Syscall status codes (montauk/statuscodes.h for C++). */
|
||||||
@@ -246,6 +254,8 @@ extern "C" {
|
|||||||
#define MTK_STATUS_OUT_OF_MEMORY (-14)
|
#define MTK_STATUS_OUT_OF_MEMORY (-14)
|
||||||
#define MTK_STATUS_TIMED_OUT (-15)
|
#define MTK_STATUS_TIMED_OUT (-15)
|
||||||
#define MTK_STATUS_WOULD_BLOCK (-16)
|
#define MTK_STATUS_WOULD_BLOCK (-16)
|
||||||
|
#define MTK_STATUS_NOT_FOUND (-17)
|
||||||
|
#define MTK_STATUS_ALREADY_EXISTS (-18)
|
||||||
/* @STATUS-END */
|
/* @STATUS-END */
|
||||||
|
|
||||||
#define MTK_SOCK_TCP 1
|
#define MTK_SOCK_TCP 1
|
||||||
|
|||||||
@@ -29,11 +29,13 @@ typedef enum {
|
|||||||
BufferTooSmall = 13,
|
BufferTooSmall = 13,
|
||||||
OutOfMemory = 14,
|
OutOfMemory = 14,
|
||||||
TimedOut = 15,
|
TimedOut = 15,
|
||||||
WouldBlock = 16
|
WouldBlock = 16,
|
||||||
|
NotFound = 17,
|
||||||
|
AlreadyExists = 18
|
||||||
} StatusCode;
|
} StatusCode;
|
||||||
|
|
||||||
// Highest defined StatusCode; update when adding a code above.
|
// Highest defined StatusCode; update when adding a code above.
|
||||||
enum { MaxStatusCode = WouldBlock };
|
enum { MaxStatusCode = AlreadyExists };
|
||||||
|
|
||||||
inline const char* GetStatusText(StatusCode statusCode) {
|
inline const char* GetStatusText(StatusCode statusCode) {
|
||||||
const char* statusText[] = {
|
const char* statusText[] = {
|
||||||
@@ -54,6 +56,8 @@ inline const char* GetStatusText(StatusCode statusCode) {
|
|||||||
"Out of memory", // 14
|
"Out of memory", // 14
|
||||||
"Timed out", // 15
|
"Timed out", // 15
|
||||||
"Operation would block", // 16
|
"Operation would block", // 16
|
||||||
|
"Not found", // 17
|
||||||
|
"Already exists", // 18
|
||||||
};
|
};
|
||||||
|
|
||||||
if (statusCode > 0 && statusCode <= MaxStatusCode) {
|
if (statusCode > 0 && statusCode <= MaxStatusCode) {
|
||||||
@@ -90,6 +94,8 @@ inline const char* GetStatusExplanationText(StatusCode statusCode) {
|
|||||||
0, // 14
|
0, // 14
|
||||||
0, // 15
|
0, // 15
|
||||||
0, // 16
|
0, // 16
|
||||||
|
0, // 17
|
||||||
|
"Something with that name is already there.", // 18
|
||||||
};
|
};
|
||||||
|
|
||||||
if (statusCode > 0 && statusCode <= MaxStatusCode) {
|
if (statusCode > 0 && statusCode <= MaxStatusCode) {
|
||||||
|
|||||||
@@ -795,6 +795,20 @@ namespace montauk {
|
|||||||
inline int clipboard_clear() {
|
inline int clipboard_clear() {
|
||||||
return (int)syscall0(montauk::abi::SYS_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
|
// Window server
|
||||||
inline int win_create(const char* title, int w, int h, montauk::abi::WinCreateResult* result) {
|
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);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -56,7 +56,7 @@ LDFLAGS := \
|
|||||||
|
|
||||||
# ---- C++ source files ----
|
# ---- C++ source files ----
|
||||||
|
|
||||||
CORE_SRCS := main.cpp window.cpp panel.cpp wifi.cpp battery.cpp brightness.cpp compose.cpp input.cpp dialogs.cpp launcher.cpp desktop_builtin.cpp desktop_catalog.cpp consent.cpp font_data.cpp stb_truetype_impl.cpp
|
CORE_SRCS := main.cpp window.cpp panel.cpp messaging.cpp desktop_icons.cpp wifi.cpp battery.cpp brightness.cpp compose.cpp input.cpp dialogs.cpp launcher.cpp desktop_builtin.cpp desktop_catalog.cpp consent.cpp font_data.cpp stb_truetype_impl.cpp
|
||||||
APP_SRCS := $(sort $(shell find apps -name '*.cpp' -print))
|
APP_SRCS := $(sort $(shell find apps -name '*.cpp' -print))
|
||||||
SRCS := $(CORE_SRCS) $(APP_SRCS)
|
SRCS := $(CORE_SRCS) $(APP_SRCS)
|
||||||
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
|
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
|
||||||
|
|||||||
@@ -34,3 +34,36 @@ void open_wordprocessor(DesktopState* ds) {
|
|||||||
(void)ds;
|
(void)ds;
|
||||||
spawn_app(ds, "0:/apps/wordprocessor/wordprocessor.elf");
|
spawn_app(ds, "0:/apps/wordprocessor/wordprocessor.elf");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Files (0:/apps/files/files.elf, its own process)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
static constexpr const char* FILES_APP_PATH = "0:/apps/files/files.elf";
|
||||||
|
|
||||||
|
void open_filemanager(DesktopState* ds) {
|
||||||
|
spawn_app(ds, FILES_APP_PATH);
|
||||||
|
}
|
||||||
|
|
||||||
|
void open_filemanager_path(DesktopState* ds, const char* path) {
|
||||||
|
spawn_app(ds, FILES_APP_PATH, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
void open_system_configuration(DesktopState* ds) {
|
||||||
|
spawn_app(ds, FILES_APP_PATH, "virtual://settings");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Icons the launcher and app menu show for home folders and the Settings
|
||||||
|
// entry (Files loads its own copies).
|
||||||
|
void desktop_ensure_place_icons(DesktopState* ds) {
|
||||||
|
if (!ds || ds->icon_system_configuration_menu.pixels) return;
|
||||||
|
|
||||||
|
Color defColor = colors::ICON_COLOR;
|
||||||
|
ds->icon_system_configuration_menu =
|
||||||
|
svg_load("0:/os/icons/preferences-system.svg", 20, 20, defColor);
|
||||||
|
for (int sf = 0; sf < gui::SPECIAL_FOLDER_COUNT; sf++) {
|
||||||
|
char icon_path[128];
|
||||||
|
snprintf(icon_path, sizeof(icon_path), "0:/os/icons/%s", gui::kSpecialFolderIcons[sf]);
|
||||||
|
ds->icon_special_folder[sf] = svg_load(icon_path, 16, 16, defColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -252,7 +252,7 @@ void open_terminal(DesktopState* ds);
|
|||||||
void open_filemanager(DesktopState* ds);
|
void open_filemanager(DesktopState* ds);
|
||||||
void open_filemanager_path(DesktopState* ds, const char* path);
|
void open_filemanager_path(DesktopState* ds, const char* path);
|
||||||
void open_system_configuration(DesktopState* ds);
|
void open_system_configuration(DesktopState* ds);
|
||||||
void ensure_filemanager_icons_loaded(DesktopState* ds);
|
void desktop_ensure_place_icons(DesktopState* ds);
|
||||||
void open_calculator(DesktopState* ds);
|
void open_calculator(DesktopState* ds);
|
||||||
void open_texteditor(DesktopState* ds);
|
void open_texteditor(DesktopState* ds);
|
||||||
void open_syslog(DesktopState* ds);
|
void open_syslog(DesktopState* ds);
|
||||||
|
|||||||
@@ -1,169 +0,0 @@
|
|||||||
/*
|
|
||||||
* associations.cpp
|
|
||||||
* File type associations for the embedded desktop file manager
|
|
||||||
* Copyright (c) 2026 Daniel Hammer
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "filemanager_internal.hpp"
|
|
||||||
#include <montauk/config.h>
|
|
||||||
|
|
||||||
namespace filemanager {
|
|
||||||
|
|
||||||
// Per-user overrides live in <home>/config/associations.toml as
|
|
||||||
// [open_with]
|
|
||||||
// jpg = "paint"
|
|
||||||
// mapping an extension to the app id of the user's chosen default.
|
|
||||||
static constexpr const char* ASSOC_CONFIG_NAME = "associations";
|
|
||||||
|
|
||||||
static bool assoc_item_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 ".
|
|
||||||
static bool assoc_item_claims(const DesktopItem& item, const char* ext) {
|
|
||||||
if (!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.
|
|
||||||
static bool assoc_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;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void assoc_config_key(char* out, int out_max, const char* ext) {
|
|
||||||
snprintf(out, out_max, "open_with.%s", ext);
|
|
||||||
}
|
|
||||||
|
|
||||||
static int assoc_find_item(const DesktopState* ds, const char* id) {
|
|
||||||
for (int i = 0; i < ds->desktop_item_count; i++) {
|
|
||||||
const DesktopItem& item = ds->desktop_items[i];
|
|
||||||
if (montauk::streq(item.id, id) && assoc_item_launchable(ds, item)) return i;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int filemanager_assoc_default(DesktopState* ds, const char* ext) {
|
|
||||||
if (!ds || !ext || !ext[0]) return -1;
|
|
||||||
|
|
||||||
if (ds->current_user[0] && assoc_ext_storable(ext)) {
|
|
||||||
auto doc = montauk::config::load_user(ds->current_user, ASSOC_CONFIG_NAME);
|
|
||||||
char key[48];
|
|
||||||
assoc_config_key(key, sizeof(key), ext);
|
|
||||||
const char* id = doc.get_string(key, "");
|
|
||||||
int idx = id[0] ? assoc_find_item(ds, id) : -1;
|
|
||||||
doc.destroy();
|
|
||||||
if (idx >= 0) return idx;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 (!assoc_item_launchable(ds, item) || !assoc_item_claims(item, ext)) continue;
|
|
||||||
if (best < 0 || str_compare_ci(item.name, ds->desktop_items[best].name) < 0) best = i;
|
|
||||||
}
|
|
||||||
return best;
|
|
||||||
}
|
|
||||||
|
|
||||||
int filemanager_assoc_candidates(DesktopState* ds, const char* ext,
|
|
||||||
int* out, int max, int* recommended) {
|
|
||||||
if (recommended) *recommended = 0;
|
|
||||||
if (!ds || !out || max <= 0 || ds->desktop_item_count <= 0) return 0;
|
|
||||||
|
|
||||||
int* sorted = (int*)montauk::malloc((uint64_t)ds->desktop_item_count * sizeof(int));
|
|
||||||
if (!sorted) return 0;
|
|
||||||
int n = 0;
|
|
||||||
for (int i = 0; i < ds->desktop_item_count; i++) {
|
|
||||||
if (assoc_item_launchable(ds, ds->desktop_items[i])) sorted[n++] = i;
|
|
||||||
}
|
|
||||||
for (int i = 1; i < n; i++) {
|
|
||||||
int tmp = sorted[i];
|
|
||||||
int j = i - 1;
|
|
||||||
while (j >= 0 && str_compare_ci(ds->desktop_items[tmp].name,
|
|
||||||
ds->desktop_items[sorted[j]].name) < 0) {
|
|
||||||
sorted[j + 1] = sorted[j];
|
|
||||||
j--;
|
|
||||||
}
|
|
||||||
sorted[j + 1] = tmp;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apps that declare the type lead; any other regular app follows (an app
|
|
||||||
// that declares nothing may still open the file). Hidden and settings
|
|
||||||
// items only appear when they claim the type.
|
|
||||||
int count = 0;
|
|
||||||
for (int i = 0; i < n && count < max; i++) {
|
|
||||||
if (assoc_item_claims(ds->desktop_items[sorted[i]], ext)) out[count++] = sorted[i];
|
|
||||||
}
|
|
||||||
if (recommended) *recommended = count;
|
|
||||||
for (int i = 0; i < n && count < max; i++) {
|
|
||||||
const DesktopItem& item = ds->desktop_items[sorted[i]];
|
|
||||||
if (item.section == DESKTOP_ITEM_SECTION_APPS && !assoc_item_claims(item, ext))
|
|
||||||
out[count++] = sorted[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
montauk::mfree(sorted);
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool filemanager_assoc_set_default(DesktopState* ds, const char* ext, int item_index) {
|
|
||||||
if (!ds || !ds->current_user[0] || !assoc_ext_storable(ext)) return false;
|
|
||||||
if (item_index < 0 || item_index >= ds->desktop_item_count) return false;
|
|
||||||
|
|
||||||
auto doc = montauk::config::load_user(ds->current_user, ASSOC_CONFIG_NAME);
|
|
||||||
char key[48];
|
|
||||||
assoc_config_key(key, sizeof(key), ext);
|
|
||||||
montauk::config::set_string(&doc, key, ds->desktop_items[item_index].id);
|
|
||||||
int rc = montauk::config::save_user(ds->current_user, ASSOC_CONFIG_NAME, &doc);
|
|
||||||
doc.destroy();
|
|
||||||
return rc == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool filemanager_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;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool filemanager_launch_with(DesktopState* ds, int item_index, const char* path) {
|
|
||||||
if (!ds || item_index < 0 || item_index >= ds->desktop_item_count) return false;
|
|
||||||
const DesktopItem& item = ds->desktop_items[item_index];
|
|
||||||
if (!assoc_item_launchable(ds, item)) return false;
|
|
||||||
return desktop_spawn_app(ds, item.binary_path, path) >= 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace filemanager
|
|
||||||
@@ -235,6 +235,8 @@ void gui::desktop_compose(DesktopState* ds) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Draw windows from bottom to top
|
// Draw windows from bottom to top
|
||||||
|
desktop_icons_draw(ds);
|
||||||
|
|
||||||
for (int i = 0; i < ds->window_count; i++) {
|
for (int i = 0; i < ds->window_count; i++) {
|
||||||
if (ds->windows[i].state != WIN_MINIMIZED && ds->windows[i].state != WIN_CLOSED) {
|
if (ds->windows[i].state != WIN_MINIMIZED && ds->windows[i].state != WIN_CLOSED) {
|
||||||
desktop_draw_window(ds, i);
|
desktop_draw_window(ds, i);
|
||||||
@@ -350,6 +352,8 @@ void gui::desktop_compose(DesktopState* ds) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
desktop_draw_drag(ds);
|
||||||
|
|
||||||
// Draw cursor last
|
// Draw cursor last
|
||||||
draw_cursor(fb, ds->mouse.x, ds->mouse.y, cur_style);
|
draw_cursor(fb, ds->mouse.x, ds->mouse.y, cur_style);
|
||||||
}
|
}
|
||||||
@@ -6,15 +6,18 @@
|
|||||||
|
|
||||||
#include "desktop_internal.hpp"
|
#include "desktop_internal.hpp"
|
||||||
#include <montauk/user.h>
|
#include <montauk/user.h>
|
||||||
|
#include <gui/app_catalog.hpp>
|
||||||
|
|
||||||
static const DesktopBuiltinEntry builtin_entries[] = {
|
static const DesktopBuiltinEntry builtin_entries[] = {
|
||||||
{ DESKTOP_BUILTIN_FILES, "Files", 0, false, nullptr, "0:/os/icons/system-file-manager.svg" },
|
{ DESKTOP_BUILTIN_FILES, "Files", 0, false, nullptr, "0:/os/icons/system-file-manager.svg" },
|
||||||
{ DESKTOP_BUILTIN_DESKTOP_SETTINGS, "Desktop Settings", -1, false, "desktop-settings",
|
// Settings pages also listed by other processes (Files) through the
|
||||||
"0:/os/icons/preferences-desktop-apps.svg" },
|
// shared catalog definitions in gui/app_catalog.hpp.
|
||||||
|
{ DESKTOP_BUILTIN_DESKTOP_SETTINGS, gui::kCatalogDesktopSettings.label, -1, false,
|
||||||
|
gui::kCatalogDesktopSettings.id, gui::kCatalogDesktopSettings.icon_path },
|
||||||
{ DESKTOP_BUILTIN_SYSTEM_CONFIGURATION, "Settings", -1, true, nullptr,
|
{ DESKTOP_BUILTIN_SYSTEM_CONFIGURATION, "Settings", -1, true, nullptr,
|
||||||
"0:/os/icons/preferences-system.svg" },
|
"0:/os/icons/preferences-system.svg" },
|
||||||
{ DESKTOP_BUILTIN_USER_MANAGEMENT, "User Management", -1, false, "user-management",
|
{ DESKTOP_BUILTIN_USER_MANAGEMENT, gui::kCatalogUserManagement.label, -1, false,
|
||||||
"0:/os/icons/user-home.svg" },
|
gui::kCatalogUserManagement.id, gui::kCatalogUserManagement.icon_path },
|
||||||
{ DESKTOP_BUILTIN_LOCK_SCREEN, "Lock Screen", -1, true, nullptr, nullptr },
|
{ DESKTOP_BUILTIN_LOCK_SCREEN, "Lock Screen", -1, true, nullptr, nullptr },
|
||||||
{ DESKTOP_BUILTIN_LOG_OUT, "Log Out", -1, true, nullptr, nullptr },
|
{ DESKTOP_BUILTIN_LOG_OUT, "Log Out", -1, true, nullptr, nullptr },
|
||||||
{ DESKTOP_BUILTIN_SLEEP, "Sleep", -1, true, nullptr, nullptr },
|
{ DESKTOP_BUILTIN_SLEEP, "Sleep", -1, true, nullptr, nullptr },
|
||||||
@@ -43,7 +46,7 @@ SvgIcon* desktop_builtin_menu_icon(DesktopState* ds, DesktopBuiltinId id) {
|
|||||||
case DESKTOP_BUILTIN_FILES: return &ds->icon_filemanager;
|
case DESKTOP_BUILTIN_FILES: return &ds->icon_filemanager;
|
||||||
case DESKTOP_BUILTIN_DESKTOP_SETTINGS: return &ds->icon_settings;
|
case DESKTOP_BUILTIN_DESKTOP_SETTINGS: return &ds->icon_settings;
|
||||||
case DESKTOP_BUILTIN_SYSTEM_CONFIGURATION:
|
case DESKTOP_BUILTIN_SYSTEM_CONFIGURATION:
|
||||||
ensure_filemanager_icons_loaded(ds);
|
desktop_ensure_place_icons(ds);
|
||||||
return &ds->icon_system_configuration_menu;
|
return &ds->icon_system_configuration_menu;
|
||||||
case DESKTOP_BUILTIN_LOCK_SCREEN: return &ds->icon_lock;
|
case DESKTOP_BUILTIN_LOCK_SCREEN: return &ds->icon_lock;
|
||||||
case DESKTOP_BUILTIN_LOG_OUT: return &ds->icon_logout;
|
case DESKTOP_BUILTIN_LOG_OUT: return &ds->icon_logout;
|
||||||
|
|||||||
@@ -6,24 +6,7 @@
|
|||||||
|
|
||||||
#include "desktop_internal.hpp"
|
#include "desktop_internal.hpp"
|
||||||
#include <montauk/capabilities.h>
|
#include <montauk/capabilities.h>
|
||||||
|
#include <gui/app_catalog.hpp>
|
||||||
namespace {
|
|
||||||
|
|
||||||
static void sort_item_indices(DesktopState* ds, int* indices, int count) {
|
|
||||||
for (int i = 1; i < count; i++) {
|
|
||||||
int tmp = indices[i];
|
|
||||||
int j = i - 1;
|
|
||||||
while (j >= 0
|
|
||||||
&& str_compare_ci(ds->desktop_items[tmp].name,
|
|
||||||
ds->desktop_items[indices[j]].name) < 0) {
|
|
||||||
indices[j + 1] = indices[j];
|
|
||||||
j--;
|
|
||||||
}
|
|
||||||
indices[j + 1] = tmp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
int desktop_spawn_app(DesktopState* ds, const char* path, const char* args) {
|
int desktop_spawn_app(DesktopState* ds, const char* path, const char* args) {
|
||||||
if (!ds || !path || !path[0]) return -1;
|
if (!ds || !path || !path[0]) return -1;
|
||||||
@@ -38,18 +21,7 @@ int desktop_list_item_indices(DesktopState* ds,
|
|||||||
DesktopItemSection section,
|
DesktopItemSection section,
|
||||||
int* out,
|
int* out,
|
||||||
int max_entries) {
|
int max_entries) {
|
||||||
if (!ds || !out || max_entries <= 0) return 0;
|
return gui::app_catalog_list_indices(ds, section, out, max_entries);
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
sort_item_indices(ds, out, written);
|
|
||||||
return written;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool desktop_launch_item(DesktopState* ds, const DesktopItem* item) {
|
bool desktop_launch_item(DesktopState* ds, const DesktopItem* item) {
|
||||||
|
|||||||
@@ -0,0 +1,395 @@
|
|||||||
|
/*
|
||||||
|
* desktop_icons.cpp
|
||||||
|
* The user's Desktop folder as icons on the background: open, drag out,
|
||||||
|
* and drop files onto it
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "desktop_internal.hpp"
|
||||||
|
#include <gui/app_catalog.hpp>
|
||||||
|
#include <gui/transfer.hpp>
|
||||||
|
|
||||||
|
using namespace montauk::abi;
|
||||||
|
|
||||||
|
static constexpr int ICON_CELL_W = 84;
|
||||||
|
static constexpr int ICON_CELL_H = 88;
|
||||||
|
static constexpr int ICON_SIZE = 48;
|
||||||
|
static constexpr int ICON_MARGIN = 12;
|
||||||
|
static constexpr int ICON_DRAG_THRESHOLD = 5;
|
||||||
|
static constexpr uint64_t ICON_RESCAN_MS = 2000;
|
||||||
|
static constexpr const char* FILES_APP_PATH = "0:/apps/files/files.elf";
|
||||||
|
|
||||||
|
static bool str_ends_ci(const char* s, const char* suffix) {
|
||||||
|
int sl = montauk::slen(s), xl = montauk::slen(suffix);
|
||||||
|
if (xl > sl) return false;
|
||||||
|
return gui::app_catalog_compare_ci(s + sl - xl, suffix) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Folder contents
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
static bool desktop_folder(const DesktopState* ds, char* out, int out_max) {
|
||||||
|
if (!ds->home_dir[0]) return false;
|
||||||
|
gui::app_catalog_join(out, out_max, ds->home_dir, "/", "Desktop");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void icon_path(const DesktopState* ds, int idx, char* out, int out_max) {
|
||||||
|
char dir[256];
|
||||||
|
desktop_folder(ds, dir, sizeof(dir));
|
||||||
|
gui::app_catalog_join(out, out_max, dir, "/", ds->desktop_icons[idx].name);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void sort_icons(DesktopState* ds) {
|
||||||
|
// Folders first, then by name.
|
||||||
|
for (int i = 1; i < ds->desktop_icon_count; i++) {
|
||||||
|
DesktopState::DesktopIcon tmp = ds->desktop_icons[i];
|
||||||
|
int j = i - 1;
|
||||||
|
while (j >= 0) {
|
||||||
|
const auto& o = ds->desktop_icons[j];
|
||||||
|
bool before = tmp.is_dir != o.is_dir ? tmp.is_dir
|
||||||
|
: gui::app_catalog_compare_ci(tmp.name, o.name) < 0;
|
||||||
|
if (!before) break;
|
||||||
|
ds->desktop_icons[j + 1] = ds->desktop_icons[j];
|
||||||
|
j--;
|
||||||
|
}
|
||||||
|
ds->desktop_icons[j + 1] = tmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-read the folder every few seconds (or now, with force). Returns true
|
||||||
|
// if what is shown changed.
|
||||||
|
bool desktop_icons_refresh(DesktopState* ds, bool force) {
|
||||||
|
uint64_t now = montauk::get_milliseconds();
|
||||||
|
if (!force && now < ds->desktop_icons_next_scan) return false;
|
||||||
|
ds->desktop_icons_next_scan = now + ICON_RESCAN_MS;
|
||||||
|
|
||||||
|
char dir[256];
|
||||||
|
if (!desktop_folder(ds, dir, sizeof(dir))) return false;
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
uint64_t hash = 1469598103934665603ull;
|
||||||
|
static constexpr int BATCH = 64;
|
||||||
|
const char* names[BATCH];
|
||||||
|
int offset = 0;
|
||||||
|
for (;;) {
|
||||||
|
int got = montauk::readdir_at(dir, names, BATCH, offset);
|
||||||
|
if (got <= 0) break;
|
||||||
|
offset += got;
|
||||||
|
for (int i = 0; i < got; i++) {
|
||||||
|
const char* raw = names[i];
|
||||||
|
int len = montauk::slen(raw);
|
||||||
|
// Hidden files stay hidden here too.
|
||||||
|
if (len == 0 || raw[0] == '.') continue;
|
||||||
|
for (int k = 0; k < len; k++) { hash ^= (uint8_t)raw[k]; hash *= 1099511628211ull; }
|
||||||
|
hash ^= '\n';
|
||||||
|
hash *= 1099511628211ull;
|
||||||
|
|
||||||
|
if (count >= ds->desktop_icon_cap) {
|
||||||
|
int cap = ds->desktop_icon_cap ? ds->desktop_icon_cap * 2 : 32;
|
||||||
|
auto* grown = (DesktopState::DesktopIcon*)montauk::realloc(
|
||||||
|
ds->desktop_icons, (uint64_t)cap * sizeof(DesktopState::DesktopIcon));
|
||||||
|
if (!grown) break;
|
||||||
|
ds->desktop_icons = grown;
|
||||||
|
ds->desktop_icon_cap = cap;
|
||||||
|
}
|
||||||
|
auto& icon = ds->desktop_icons[count++];
|
||||||
|
int n = len;
|
||||||
|
icon.is_dir = raw[len - 1] == '/';
|
||||||
|
if (icon.is_dir) n--;
|
||||||
|
if (n > 127) n = 127;
|
||||||
|
for (int k = 0; k < n; k++) icon.name[k] = raw[k];
|
||||||
|
icon.name[n] = '\0';
|
||||||
|
}
|
||||||
|
if (offset >= 1024) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool changed = hash != ds->desktop_icons_hash || count != ds->desktop_icon_count;
|
||||||
|
ds->desktop_icon_count = count;
|
||||||
|
ds->desktop_icons_hash = hash;
|
||||||
|
if (changed) {
|
||||||
|
sort_icons(ds);
|
||||||
|
ds->desktop_icon_selected = -1;
|
||||||
|
ds->desktop_icon_press = -1;
|
||||||
|
desktop_mark_background_dirty(ds);
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Layout and drawing
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Column-major from the top left, like most desktops.
|
||||||
|
static Rect icon_rect(const DesktopState* ds, int idx) {
|
||||||
|
int rows = (ds->screen_h - PANEL_HEIGHT - ICON_MARGIN) / ICON_CELL_H;
|
||||||
|
if (rows < 1) rows = 1;
|
||||||
|
int col = idx / rows;
|
||||||
|
int row = idx % rows;
|
||||||
|
return {ICON_MARGIN + col * ICON_CELL_W, PANEL_HEIGHT + ICON_MARGIN + row * ICON_CELL_H,
|
||||||
|
ICON_CELL_W, ICON_CELL_H};
|
||||||
|
}
|
||||||
|
|
||||||
|
int desktop_icon_at(DesktopState* ds, int x, int y) {
|
||||||
|
for (int i = 0; i < ds->desktop_icon_count; i++) {
|
||||||
|
if (icon_rect(ds, i).contains(x, y)) return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void draw_label(Canvas& c, int cell_x, int y, const char* text) {
|
||||||
|
char fit[128];
|
||||||
|
montauk::strncpy(fit, text, sizeof(fit));
|
||||||
|
int max_w = ICON_CELL_W - 6;
|
||||||
|
if (text_width(fit) > max_w) {
|
||||||
|
int len = montauk::slen(fit);
|
||||||
|
while (len > 1 && text_width(fit) + text_width("...") > max_w) {
|
||||||
|
len = montauk::utf8::prev_boundary(fit, len);
|
||||||
|
fit[len] = '\0';
|
||||||
|
}
|
||||||
|
str_append(fit, "...", sizeof(fit));
|
||||||
|
}
|
||||||
|
int x = cell_x + (ICON_CELL_W - text_width(fit)) / 2;
|
||||||
|
// A shadow keeps the label readable on any wallpaper.
|
||||||
|
c.text(x + 1, y + 1, fit, Color::from_rgb(0x20, 0x20, 0x20));
|
||||||
|
c.text(x, y, fit, colors::WHITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void desktop_icons_draw(DesktopState* ds) {
|
||||||
|
if (ds->desktop_icon_count == 0) return;
|
||||||
|
Framebuffer& fb = ds->fb;
|
||||||
|
Canvas c(fb.buffer(), fb.pitch() / 4, ds->screen_h);
|
||||||
|
Color accent = ds->settings.accent_color;
|
||||||
|
|
||||||
|
for (int i = 0; i < ds->desktop_icon_count; i++) {
|
||||||
|
Rect r = icon_rect(ds, i);
|
||||||
|
if (r.y + r.h > ds->screen_h) break;
|
||||||
|
const auto& icon = ds->desktop_icons[i];
|
||||||
|
|
||||||
|
if (i == ds->desktop_icon_selected) {
|
||||||
|
c.fill_rounded_rect(r.x + 2, r.y + 2, r.w - 4, r.h - 4, 6,
|
||||||
|
Color::from_rgba(accent.r, accent.g, accent.b, 0x60));
|
||||||
|
}
|
||||||
|
if (i == ds->desktop_drop_icon) {
|
||||||
|
c.rect(r.x + 2, r.y + 2, r.w - 4, r.h - 4, accent);
|
||||||
|
c.rect(r.x + 3, r.y + 3, r.w - 6, r.h - 6, accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
const SvgIcon& svg = icon.is_dir ? ds->icon_folder_lg
|
||||||
|
: str_ends_ci(icon.name, ".elf") ? ds->icon_exec_lg : ds->icon_file_lg;
|
||||||
|
if (svg.pixels) c.icon(r.x + (ICON_CELL_W - ICON_SIZE) / 2, r.y + 6, svg);
|
||||||
|
draw_label(c, r.x, r.y + 6 + ICON_SIZE + 4, icon.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Opening
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
static void open_icon(DesktopState* ds, int idx) {
|
||||||
|
char path[512];
|
||||||
|
icon_path(ds, idx, path, sizeof(path));
|
||||||
|
const auto& icon = ds->desktop_icons[idx];
|
||||||
|
|
||||||
|
if (icon.is_dir) {
|
||||||
|
open_filemanager_path(ds, path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (str_ends_ci(icon.name, ".elf")) {
|
||||||
|
for (int i = 0; i < ds->desktop_item_count; i++) {
|
||||||
|
if (gui::app_catalog_compare_ci(ds->desktop_items[i].binary_path, path) == 0) {
|
||||||
|
desktop_launch_item(ds, &ds->desktop_items[i]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
desktop_spawn_app(ds, path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same rules as Files: the default app, text to the Text Editor, and
|
||||||
|
// anything else asks (Files shows its Open With dialog).
|
||||||
|
char ext[16];
|
||||||
|
gui::app_catalog_extension(icon.name, ext, sizeof(ext));
|
||||||
|
int app = gui::app_catalog_default_for(ds, ext);
|
||||||
|
if (app >= 0) {
|
||||||
|
desktop_spawn_app(ds, ds->desktop_items[app].binary_path, path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (gui::app_catalog_file_looks_textual(path)) {
|
||||||
|
desktop_spawn_app(ds, "0:/apps/texteditor/texteditor.elf", path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
char args[600];
|
||||||
|
gui::app_catalog_join(args, sizeof(args), "--open-with ", path, "");
|
||||||
|
desktop_spawn_app(ds, FILES_APP_PATH, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Mouse
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
void desktop_icons_press(DesktopState* ds, int x, int y) {
|
||||||
|
int idx = desktop_icon_at(ds, x, y);
|
||||||
|
ds->desktop_icon_selected = idx;
|
||||||
|
ds->desktop_icon_press = idx;
|
||||||
|
ds->desktop_icon_press_x = x;
|
||||||
|
ds->desktop_icon_press_y = y;
|
||||||
|
if (idx < 0) return;
|
||||||
|
|
||||||
|
uint64_t now = montauk::get_milliseconds();
|
||||||
|
if (ds->desktop_icon_last_click == idx && now - ds->desktop_icon_last_click_ms < 400) {
|
||||||
|
ds->desktop_icon_last_click = -1;
|
||||||
|
ds->desktop_icon_press = -1;
|
||||||
|
open_icon(ds, idx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ds->desktop_icon_last_click = idx;
|
||||||
|
ds->desktop_icon_last_click_ms = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drag the pressed icon out once the pointer has moved far enough.
|
||||||
|
void desktop_icons_motion(DesktopState* ds) {
|
||||||
|
int idx = ds->desktop_icon_press;
|
||||||
|
if (idx < 0) return;
|
||||||
|
if (!(ds->mouse.buttons & 1)) {
|
||||||
|
ds->desktop_icon_press = -1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ds->drag.active) return;
|
||||||
|
int dx = ds->mouse.x - ds->desktop_icon_press_x;
|
||||||
|
int dy = ds->mouse.y - ds->desktop_icon_press_y;
|
||||||
|
if (dx * dx + dy * dy < ICON_DRAG_THRESHOLD * ICON_DRAG_THRESHOLD) return;
|
||||||
|
|
||||||
|
char path[512];
|
||||||
|
icon_path(ds, idx, path, sizeof(path));
|
||||||
|
gui::TransferBuilder transfer;
|
||||||
|
transfer.add_text(TRANSFER_MIME_FILE_LIST, path);
|
||||||
|
transfer.add_text(TRANSFER_MIME_TEXT, path);
|
||||||
|
// -1: a drag from the desktop surface itself, not from a window.
|
||||||
|
uint32_t serial = gui::drag_begin(-1, transfer, WIN_DRAG_COPY | WIN_DRAG_MOVE, 1,
|
||||||
|
ds->desktop_icons[idx].name);
|
||||||
|
ds->desktop_icon_press = -1;
|
||||||
|
if (serial) desktop_drag_begin_local(ds, serial, WIN_DRAG_COPY | WIN_DRAG_MOVE, 1,
|
||||||
|
ds->desktop_icons[idx].name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Dropping onto the desktop
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
static bool same_volume(const char* a, const char* b) {
|
||||||
|
for (int i = 0; a[i] && b[i]; i++) {
|
||||||
|
if (a[i] != b[i]) return false;
|
||||||
|
if (a[i] == ':') return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool path_within(const char* path, const char* root) {
|
||||||
|
int n = montauk::slen(root);
|
||||||
|
while (n > 0 && root[n - 1] == '/') n--;
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
if (path[i] != root[i]) return false;
|
||||||
|
}
|
||||||
|
return path[n] == '\0' || path[n] == '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool parent_is(const char* path, const char* dir) {
|
||||||
|
int n = montauk::slen(path);
|
||||||
|
while (n > 0 && path[n - 1] != '/') n--;
|
||||||
|
int dn = montauk::slen(dir);
|
||||||
|
while (dn > 3 && dir[dn - 1] == '/') dn--;
|
||||||
|
if (n - 1 != dn) return false;
|
||||||
|
for (int i = 0; i < dn; i++) {
|
||||||
|
if (path[i] != dir[i]) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Where a drop at (x, y) would land: a folder icon, or the Desktop folder.
|
||||||
|
static bool drop_dir_at(DesktopState* ds, int x, int y, char* out, int out_max, int* icon) {
|
||||||
|
*icon = -1;
|
||||||
|
if (y < PANEL_HEIGHT || !desktop_folder(ds, out, out_max)) return false;
|
||||||
|
int idx = desktop_icon_at(ds, x, y);
|
||||||
|
if (idx >= 0 && ds->desktop_icons[idx].is_dir) {
|
||||||
|
icon_path(ds, idx, out, out_max);
|
||||||
|
*icon = idx;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// What dropping the current drag at (x, y) would do; 0 = refuse.
|
||||||
|
uint8_t desktop_icons_drop_action(DesktopState* ds, int x, int y) {
|
||||||
|
ds->desktop_drop_icon = -1;
|
||||||
|
if (!ds->drag.active) return 0;
|
||||||
|
if (ds->desktop_drop_serial != ds->drag.serial) {
|
||||||
|
if (ds->desktop_drop_list) montauk::mfree(ds->desktop_drop_list);
|
||||||
|
ds->desktop_drop_serial = ds->drag.serial;
|
||||||
|
// The compositor may read any drag.
|
||||||
|
ds->desktop_drop_list = gui::drag_get_alloc(ds->drag.serial, TRANSFER_MIME_FILE_LIST);
|
||||||
|
}
|
||||||
|
if (!ds->desktop_drop_list) return 0;
|
||||||
|
|
||||||
|
char dir[256];
|
||||||
|
int icon = -1;
|
||||||
|
if (!drop_dir_at(ds, x, y, dir, sizeof(dir), &icon)) return 0;
|
||||||
|
|
||||||
|
bool ok = true, all_here = true, all_same = true;
|
||||||
|
gui::file_list_each(ds->desktop_drop_list, [&](const char* src) {
|
||||||
|
if (path_within(dir, src)) ok = false;
|
||||||
|
if (!parent_is(src, dir)) all_here = false;
|
||||||
|
if (!same_volume(src, dir)) all_same = false;
|
||||||
|
});
|
||||||
|
if (!ok) return 0;
|
||||||
|
uint8_t action = gui::drag_pick_action(ds->drag.actions, desktop_modifier_bits(ds),
|
||||||
|
all_same ? WIN_DRAG_MOVE : WIN_DRAG_COPY);
|
||||||
|
if (action == WIN_DRAG_LINK || (action == WIN_DRAG_MOVE && all_here)) return 0;
|
||||||
|
ds->desktop_drop_icon = icon;
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform a drop: Files does the copying (progress, conflict prompts) from
|
||||||
|
// a list file in ~/.cache, so the desktop never blocks on file I/O.
|
||||||
|
void desktop_icons_drop(DesktopState* ds, int x, int y, uint8_t action) {
|
||||||
|
char dir[256];
|
||||||
|
int icon = -1;
|
||||||
|
if (!ds->desktop_drop_list || !drop_dir_at(ds, x, y, dir, sizeof(dir), &icon)) return;
|
||||||
|
|
||||||
|
char cache[256];
|
||||||
|
gui::app_catalog_join(cache, sizeof(cache), ds->home_dir, "/.cache", "");
|
||||||
|
montauk::fmkdir(cache);
|
||||||
|
char list_path[320];
|
||||||
|
char name[48];
|
||||||
|
snprintf(name, sizeof(name), "/files-drop-%d.txt", (int)ds->drag.serial);
|
||||||
|
gui::app_catalog_join(list_path, sizeof(list_path), cache, name, "");
|
||||||
|
|
||||||
|
int fd = montauk::fcreate(list_path);
|
||||||
|
if (fd < 0) return;
|
||||||
|
uint64_t off = 0;
|
||||||
|
auto put = [&](const char* text) {
|
||||||
|
int len = montauk::slen(text);
|
||||||
|
montauk::fwrite(fd, (const uint8_t*)text, off, len);
|
||||||
|
off += len;
|
||||||
|
};
|
||||||
|
put(action == WIN_DRAG_MOVE ? "move\n" : "copy\n");
|
||||||
|
put(dir);
|
||||||
|
put("\n");
|
||||||
|
put(ds->desktop_drop_list);
|
||||||
|
put("\n");
|
||||||
|
montauk::close(fd);
|
||||||
|
|
||||||
|
char args[400];
|
||||||
|
gui::app_catalog_join(args, sizeof(args), "--transfer ", list_path, "");
|
||||||
|
desktop_spawn_app(ds, FILES_APP_PATH, args);
|
||||||
|
// Files works asynchronously; look again soon.
|
||||||
|
ds->desktop_icons_next_scan = montauk::get_milliseconds() + 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
void desktop_icons_drag_done(DesktopState* ds) {
|
||||||
|
ds->desktop_drop_icon = -1;
|
||||||
|
if (ds->desktop_drop_list) montauk::mfree(ds->desktop_drop_list);
|
||||||
|
ds->desktop_drop_list = nullptr;
|
||||||
|
ds->desktop_drop_serial = 0;
|
||||||
|
ds->desktop_icons_next_scan = montauk::get_milliseconds() + 400;
|
||||||
|
}
|
||||||
@@ -206,6 +206,26 @@ bool desktop_brightness_handle_mouse(gui::DesktopState* ds, int mx, int my,
|
|||||||
|
|
||||||
// compose.cpp
|
// compose.cpp
|
||||||
void desktop_draw_lock_screen(gui::DesktopState* ds);
|
void desktop_draw_lock_screen(gui::DesktopState* ds);
|
||||||
|
|
||||||
|
// Window server messaging (messaging.cpp): app requests and drag and drop.
|
||||||
|
bool desktop_poll_requests(gui::DesktopState* ds);
|
||||||
|
void desktop_drag_update(gui::DesktopState* ds);
|
||||||
|
void desktop_drag_cancel(gui::DesktopState* ds);
|
||||||
|
void desktop_draw_drag(gui::DesktopState* ds);
|
||||||
|
uint8_t desktop_modifier_bits(const gui::DesktopState* ds);
|
||||||
|
// A drag the desktop itself started (desktop icons; serial from the kernel).
|
||||||
|
void desktop_drag_begin_local(gui::DesktopState* ds, uint32_t serial, uint8_t actions,
|
||||||
|
uint32_t item_count, const char* label);
|
||||||
|
|
||||||
|
// The Desktop folder on the background (desktop_icons.cpp).
|
||||||
|
bool desktop_icons_refresh(gui::DesktopState* ds, bool force);
|
||||||
|
void desktop_icons_draw(gui::DesktopState* ds);
|
||||||
|
int desktop_icon_at(gui::DesktopState* ds, int x, int y);
|
||||||
|
void desktop_icons_press(gui::DesktopState* ds, int x, int y);
|
||||||
|
void desktop_icons_motion(gui::DesktopState* ds);
|
||||||
|
uint8_t desktop_icons_drop_action(gui::DesktopState* ds, int x, int y);
|
||||||
|
void desktop_icons_drop(gui::DesktopState* ds, int x, int y, uint8_t action);
|
||||||
|
void desktop_icons_drag_done(gui::DesktopState* ds);
|
||||||
void desktop_mark_background_dirty(gui::DesktopState* ds);
|
void desktop_mark_background_dirty(gui::DesktopState* ds);
|
||||||
|
|
||||||
// main.cpp
|
// main.cpp
|
||||||
|
|||||||
@@ -106,7 +106,8 @@ static void handle_lock_keyboard(DesktopState* ds, const montauk::abi::KeyEvent&
|
|||||||
montauk::utf8::append_key(ds->lock_password, &ds->lock_password_len, 64, key);
|
montauk::utf8::append_key(ds->lock_password, &ds->lock_password_len, 64, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void forward_external_mouse(gui::Window* win,
|
static void forward_external_mouse(gui::DesktopState* ds,
|
||||||
|
gui::Window* win,
|
||||||
const gui::MouseEvent& ev,
|
const gui::MouseEvent& ev,
|
||||||
int mx,
|
int mx,
|
||||||
int my,
|
int my,
|
||||||
@@ -121,6 +122,7 @@ static void forward_external_mouse(gui::Window* win,
|
|||||||
wev.mouse.scroll = ev.scroll;
|
wev.mouse.scroll = ev.scroll;
|
||||||
wev.mouse.buttons = buttons;
|
wev.mouse.buttons = buttons;
|
||||||
wev.mouse.prev_buttons = prev;
|
wev.mouse.prev_buttons = prev;
|
||||||
|
wev.mouse.modifiers = desktop_modifier_bits(ds);
|
||||||
montauk::win_sendevent(win->ext_win_id, &wev);
|
montauk::win_sendevent(win->ext_win_id, &wev);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,7 +143,7 @@ static bool route_fullscreen_external_mouse(gui::DesktopState* ds,
|
|||||||
gui::desktop_raise_window(ds, i);
|
gui::desktop_raise_window(ds, i);
|
||||||
win = &ds->windows[ds->window_count - 1];
|
win = &ds->windows[ds->window_count - 1];
|
||||||
}
|
}
|
||||||
forward_external_mouse(win, ev, mx, my, buttons, prev);
|
forward_external_mouse(ds, win, ev, mx, my, buttons, prev);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -173,6 +175,11 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
|||||||
ev.prev_buttons = prev;
|
ev.prev_buttons = prev;
|
||||||
ev.scroll = ds->mouse.scrollDelta;
|
ev.scroll = ds->mouse.scrollDelta;
|
||||||
|
|
||||||
|
// An app-started drag follows the pointer across windows; the source
|
||||||
|
// window keeps receiving its own (held/released) mouse events below.
|
||||||
|
desktop_drag_update(ds);
|
||||||
|
desktop_icons_motion(ds);
|
||||||
|
|
||||||
if (route_fullscreen_external_mouse(ds, ev, mx, my, buttons, prev)) {
|
if (route_fullscreen_external_mouse(ds, ev, mx, my, buttons, prev)) {
|
||||||
ds->app_menu_open = false;
|
ds->app_menu_open = false;
|
||||||
gui::mtk::context_menu_close(ds->ctx_menu);
|
gui::mtk::context_menu_close(ds->ctx_menu);
|
||||||
@@ -780,6 +787,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
|||||||
wev.mouse.scroll = ev.scroll;
|
wev.mouse.scroll = ev.scroll;
|
||||||
wev.mouse.buttons = buttons;
|
wev.mouse.buttons = buttons;
|
||||||
wev.mouse.prev_buttons = prev;
|
wev.mouse.prev_buttons = prev;
|
||||||
|
wev.mouse.modifiers = desktop_modifier_bits(ds);
|
||||||
montauk::win_sendevent(raised->ext_win_id, &wev);
|
montauk::win_sendevent(raised->ext_win_id, &wev);
|
||||||
} else if (raised->on_mouse) {
|
} else if (raised->on_mouse) {
|
||||||
ev.x = mx;
|
ev.x = mx;
|
||||||
@@ -796,6 +804,9 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The background: select / open / start dragging a desktop icon.
|
||||||
|
desktop_icons_press(ds, mx, my);
|
||||||
|
|
||||||
ds->app_menu_open = false;
|
ds->app_menu_open = false;
|
||||||
gui::mtk::context_menu_close(ds->ctx_menu);
|
gui::mtk::context_menu_close(ds->ctx_menu);
|
||||||
ds->vol_popup_open = false;
|
ds->vol_popup_open = false;
|
||||||
@@ -820,6 +831,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
|||||||
wev.mouse.scroll = 0;
|
wev.mouse.scroll = 0;
|
||||||
wev.mouse.buttons = buttons;
|
wev.mouse.buttons = buttons;
|
||||||
wev.mouse.prev_buttons = prev;
|
wev.mouse.prev_buttons = prev;
|
||||||
|
wev.mouse.modifiers = desktop_modifier_bits(ds);
|
||||||
montauk::win_sendevent(win->ext_win_id, &wev);
|
montauk::win_sendevent(win->ext_win_id, &wev);
|
||||||
} else if (win->on_mouse) {
|
} else if (win->on_mouse) {
|
||||||
MouseEvent hover = ev;
|
MouseEvent hover = ev;
|
||||||
@@ -846,6 +858,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
|||||||
wev.mouse.scroll = ev.scroll;
|
wev.mouse.scroll = ev.scroll;
|
||||||
wev.mouse.buttons = buttons;
|
wev.mouse.buttons = buttons;
|
||||||
wev.mouse.prev_buttons = prev;
|
wev.mouse.prev_buttons = prev;
|
||||||
|
wev.mouse.modifiers = desktop_modifier_bits(ds);
|
||||||
montauk::win_sendevent(win->ext_win_id, &wev);
|
montauk::win_sendevent(win->ext_win_id, &wev);
|
||||||
} else if (win->on_mouse) {
|
} else if (win->on_mouse) {
|
||||||
win->on_mouse(win, ev);
|
win->on_mouse(win, ev);
|
||||||
@@ -889,6 +902,11 @@ void gui::desktop_handle_keyboard(DesktopState* ds, const montauk::abi::KeyEvent
|
|||||||
const montauk::abi::KeyEvent& routed_key = translated_key;
|
const montauk::abi::KeyEvent& routed_key = translated_key;
|
||||||
ds->kbd_shift_down = routed_key.shift;
|
ds->kbd_shift_down = routed_key.shift;
|
||||||
ds->kbd_ctrl_down = routed_key.ctrl;
|
ds->kbd_ctrl_down = routed_key.ctrl;
|
||||||
|
ds->kbd_alt_down = routed_key.alt;
|
||||||
|
|
||||||
|
// Escape abandons a drag in progress (the key still reaches the app).
|
||||||
|
if (ds->drag.active && routed_key.pressed && routed_key.scancode == 0x01)
|
||||||
|
desktop_drag_cancel(ds);
|
||||||
|
|
||||||
if (ds->screen_locked) {
|
if (ds->screen_locked) {
|
||||||
handle_lock_keyboard(ds, routed_key);
|
handle_lock_keyboard(ds, routed_key);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ static constexpr int LAUNCHER_EMPTY_H = 84;
|
|||||||
static constexpr int LAUNCHER_VISIBLE_ROWS = 7;
|
static constexpr int LAUNCHER_VISIBLE_ROWS = 7;
|
||||||
static constexpr int LAUNCHER_TEXT_PAD = 18;
|
static constexpr int LAUNCHER_TEXT_PAD = 18;
|
||||||
static constexpr int LAUNCHER_CURSOR_BLINK_MS = 700;
|
static constexpr int LAUNCHER_CURSOR_BLINK_MS = 700;
|
||||||
static constexpr int LAUNCHER_SPECIAL_FOLDER_COUNT = 6;
|
static constexpr int LAUNCHER_SPECIAL_FOLDER_COUNT = gui::SPECIAL_FOLDER_COUNT;
|
||||||
|
|
||||||
static constexpr Color LAUNCHER_BORDER = Color::from_rgb(0xD4, 0xDB, 0xE4);
|
static constexpr Color LAUNCHER_BORDER = Color::from_rgb(0xD4, 0xDB, 0xE4);
|
||||||
static constexpr Color LAUNCHER_FIELD_BG = Color::from_rgb(0xFF, 0xFF, 0xFF);
|
static constexpr Color LAUNCHER_FIELD_BG = Color::from_rgb(0xFF, 0xFF, 0xFF);
|
||||||
@@ -37,9 +37,7 @@ static Color launcher_hover_bg(Color accent) {
|
|||||||
static constexpr Color LAUNCHER_MUTED = Color::from_rgb(0x6F, 0x7B, 0x89);
|
static constexpr Color LAUNCHER_MUTED = Color::from_rgb(0x6F, 0x7B, 0x89);
|
||||||
static constexpr Color LAUNCHER_PLACEHOLDER = Color::from_rgb(0x98, 0xA1, 0xAD);
|
static constexpr Color LAUNCHER_PLACEHOLDER = Color::from_rgb(0x98, 0xA1, 0xAD);
|
||||||
|
|
||||||
static const char* launcher_special_folder_names[LAUNCHER_SPECIAL_FOLDER_COUNT] = {
|
static constexpr const char* const* launcher_special_folder_names = gui::kSpecialFolderNames;
|
||||||
"Documents", "Desktop", "Music", "Videos", "Pictures", "Downloads"
|
|
||||||
};
|
|
||||||
|
|
||||||
static char fold_ascii(char c) {
|
static char fold_ascii(char c) {
|
||||||
return (c >= 'A' && c <= 'Z') ? (char)(c - 'A' + 'a') : c;
|
return (c >= 'A' && c <= 'Z') ? (char)(c - 'A' + 'a') : c;
|
||||||
@@ -202,7 +200,7 @@ static void launcher_add_item(DesktopState* ds, LauncherItemKind kind,
|
|||||||
static void desktop_build_launcher_items(DesktopState* ds) {
|
static void desktop_build_launcher_items(DesktopState* ds) {
|
||||||
ds->launcher_item_count = 0;
|
ds->launcher_item_count = 0;
|
||||||
|
|
||||||
ensure_filemanager_icons_loaded(ds);
|
desktop_ensure_place_icons(ds);
|
||||||
|
|
||||||
launcher_add_item(ds, LAUNCHER_ITEM_BUILTIN_FILES,
|
launcher_add_item(ds, LAUNCHER_ITEM_BUILTIN_FILES,
|
||||||
"Files", "Built-in App",
|
"Files", "Built-in App",
|
||||||
|
|||||||
+14
-178
@@ -9,52 +9,19 @@
|
|||||||
#include <montauk/user.h>
|
#include <montauk/user.h>
|
||||||
#include <gui/clipboard.hpp>
|
#include <gui/clipboard.hpp>
|
||||||
#include <gui/dialogs.hpp>
|
#include <gui/dialogs.hpp>
|
||||||
|
#include <gui/app_catalog.hpp>
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// App Manifest Scanning
|
// App Manifest Scanning (shared catalog code: gui/app_catalog.hpp)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
// Extract the basename from a readdir entry, tolerating both current
|
|
||||||
// direct-child names and paths returned by older ramdisk kernels.
|
|
||||||
static void extract_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.
|
|
||||||
static DesktopItem* next_desktop_item_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];
|
|
||||||
}
|
|
||||||
|
|
||||||
static void desktop_add_builtin_catalog_items(DesktopState* ds) {
|
static void desktop_add_builtin_catalog_items(DesktopState* ds) {
|
||||||
int builtin_count = 0;
|
int builtin_count = 0;
|
||||||
const DesktopBuiltinEntry* builtins = desktop_builtin_registry(&builtin_count);
|
const DesktopBuiltinEntry* builtins = desktop_builtin_registry(&builtin_count);
|
||||||
for (int i = 0; i < builtin_count; i++) {
|
for (int i = 0; i < builtin_count; i++) {
|
||||||
if (!builtins[i].system_configuration_id) continue;
|
if (!builtins[i].system_configuration_id) continue;
|
||||||
|
|
||||||
DesktopItem* item = next_desktop_item_slot(ds);
|
DesktopItem* item = gui::app_catalog_next_slot(ds);
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
montauk::memset(item, 0, sizeof(*item));
|
montauk::memset(item, 0, sizeof(*item));
|
||||||
montauk::strncpy(item->id, builtins[i].system_configuration_id, sizeof(item->id));
|
montauk::strncpy(item->id, builtins[i].system_configuration_id, sizeof(item->id));
|
||||||
@@ -74,148 +41,7 @@ static void desktop_add_builtin_catalog_items(DesktopState* ds) {
|
|||||||
void desktop_scan_apps(DesktopState* ds) {
|
void desktop_scan_apps(DesktopState* ds) {
|
||||||
ds->desktop_item_count = 0;
|
ds->desktop_item_count = 0;
|
||||||
desktop_add_builtin_catalog_items(ds);
|
desktop_add_builtin_catalog_items(ds);
|
||||||
|
gui::app_catalog_add_manifest_apps(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];
|
|
||||||
extract_basename(dirname, sizeof(dirname), entries[i]);
|
|
||||||
if (dirname[0] == '\0') continue;
|
|
||||||
|
|
||||||
// Try to open the manifest in this app directory
|
|
||||||
char manifest_path[128];
|
|
||||||
snprintf(manifest_path, sizeof(manifest_path), "0:/apps/%s/manifest.toml", dirname);
|
|
||||||
|
|
||||||
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("desktop: 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("desktop: 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("desktop: skipping app manifest with invalid desktop.section\n");
|
|
||||||
doc.destroy();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
DesktopItem* item = next_desktop_item_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>
|
|
||||||
snprintf(item->binary_path, sizeof(item->binary_path),
|
|
||||||
"0:/apps/%s/%s", 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]) {
|
|
||||||
snprintf(item->icon_path, sizeof(item->icon_path),
|
|
||||||
"0:/apps/%s/%s", dirname, icon_file);
|
|
||||||
Color defColor = colors::ICON_COLOR;
|
|
||||||
item->icon = svg_load(item->icon_path, 20, 20, defColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
doc.destroy();
|
|
||||||
ds->desktop_item_count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -390,6 +216,11 @@ void gui::desktop_init(DesktopState* ds) {
|
|||||||
|
|
||||||
// Scan 0:/apps/ for external app manifests and build the menu
|
// Scan 0:/apps/ for external app manifests and build the menu
|
||||||
desktop_scan_apps(ds);
|
desktop_scan_apps(ds);
|
||||||
|
ds->desktop_icon_selected = -1;
|
||||||
|
ds->desktop_icon_press = -1;
|
||||||
|
ds->desktop_icon_last_click = -1;
|
||||||
|
ds->desktop_drop_icon = -1;
|
||||||
|
desktop_icons_refresh(ds, true);
|
||||||
desktop_build_menu(ds);
|
desktop_build_menu(ds);
|
||||||
|
|
||||||
// Settings defaults
|
// Settings defaults
|
||||||
@@ -537,6 +368,7 @@ static bool desktop_has_visible_dirty_window(const DesktopState* ds) {
|
|||||||
|
|
||||||
static bool desktop_has_active_interaction(const DesktopState* ds) {
|
static bool desktop_has_active_interaction(const DesktopState* ds) {
|
||||||
if (ds->vol_dragging) return true;
|
if (ds->vol_dragging) return true;
|
||||||
|
if (ds->drag.active) return true;
|
||||||
if (desktop_brightness_dragging(ds)) return true;
|
if (desktop_brightness_dragging(ds)) return true;
|
||||||
|
|
||||||
for (int i = 0; i < ds->window_count; i++) {
|
for (int i = 0; i < ds->window_count; i++) {
|
||||||
@@ -891,6 +723,10 @@ void gui::desktop_run(DesktopState* ds) {
|
|||||||
// Poll external windows (discover new, remove dead, update dirty)
|
// Poll external windows (discover new, remove dead, update dirty)
|
||||||
sceneChanged |= desktop_poll_external_windows(ds);
|
sceneChanged |= desktop_poll_external_windows(ds);
|
||||||
|
|
||||||
|
// App requests: catalog launches, dialog placement, drag and drop.
|
||||||
|
sceneChanged |= desktop_poll_requests(ds);
|
||||||
|
if (!ds->screen_locked) sceneChanged |= desktop_icons_refresh(ds, false);
|
||||||
|
|
||||||
// Answer or prompt for pending first-use capability requests.
|
// Answer or prompt for pending first-use capability requests.
|
||||||
if (!ds->screen_locked) sceneChanged |= desktop_consent_poll(ds);
|
if (!ds->screen_locked) sceneChanged |= desktop_consent_poll(ds);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,398 @@
|
|||||||
|
/*
|
||||||
|
* messaging.cpp
|
||||||
|
* Compositor side of window server messaging: app requests (launch a
|
||||||
|
* catalog item, dialog placement) and drag and drop between windows
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "desktop_internal.hpp"
|
||||||
|
#include <gui/transfer.hpp>
|
||||||
|
|
||||||
|
using namespace montauk::abi;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Helpers
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
static int find_external_window(DesktopState* ds, int ext_id) {
|
||||||
|
if (ext_id < 0) return -1;
|
||||||
|
for (int i = 0; i < ds->window_count; i++) {
|
||||||
|
if (ds->windows[i].external && ds->windows[i].ext_win_id == ext_id &&
|
||||||
|
ds->windows[i].state != WIN_CLOSED)
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int window_owner_pid(int ext_id) {
|
||||||
|
WinInfo infos[MAX_WINDOWS];
|
||||||
|
int n = montauk::win_enumerate(infos, MAX_WINDOWS);
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
if (infos[i].id == ext_id) return infos[i].ownerPid;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t desktop_modifier_bits(const DesktopState* ds) {
|
||||||
|
uint8_t mods = 0;
|
||||||
|
if (ds->kbd_shift_down) mods |= WIN_MOD_SHIFT;
|
||||||
|
if (ds->kbd_ctrl_down) mods |= WIN_MOD_CTRL;
|
||||||
|
if (ds->kbd_alt_down) mods |= WIN_MOD_ALT;
|
||||||
|
return mods;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void send_drag_event(DesktopState* ds, int ext_id, uint8_t type, int x, int y,
|
||||||
|
uint8_t actions) {
|
||||||
|
WinEvent ev;
|
||||||
|
montauk::memset(&ev, 0, sizeof(ev));
|
||||||
|
ev.type = type;
|
||||||
|
ev.drag.x = x;
|
||||||
|
ev.drag.y = y;
|
||||||
|
ev.drag.serial = ds->drag.serial;
|
||||||
|
ev.drag.actions = actions;
|
||||||
|
ev.drag.modifiers = desktop_modifier_bits(ds);
|
||||||
|
montauk::win_sendevent(ext_id, &ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Dialog placement
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
static bool apply_parent(DesktopState* ds, int child_ext_id, int parent_ext_id) {
|
||||||
|
int ci = find_external_window(ds, child_ext_id);
|
||||||
|
int pi = find_external_window(ds, parent_ext_id);
|
||||||
|
if (ci < 0 || pi < 0) return false;
|
||||||
|
|
||||||
|
Window* child = &ds->windows[ci];
|
||||||
|
const Window* parent = &ds->windows[pi];
|
||||||
|
if (child->state == WIN_MAXIMIZED || desktop_window_fullscreen(child)) return true;
|
||||||
|
|
||||||
|
int x = parent->frame.x + (parent->frame.w - child->frame.w) / 2;
|
||||||
|
int y = parent->frame.y + (parent->frame.h - child->frame.h) / 2;
|
||||||
|
if (x + child->frame.w > ds->screen_w) x = ds->screen_w - child->frame.w;
|
||||||
|
if (y + child->frame.h > ds->screen_h) y = ds->screen_h - child->frame.h;
|
||||||
|
if (x < 0) x = 0;
|
||||||
|
if (y < PANEL_HEIGHT) y = PANEL_HEIGHT;
|
||||||
|
child->frame.x = x;
|
||||||
|
child->frame.y = y;
|
||||||
|
child->saved_frame = child->frame;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void queue_parent(DesktopState* ds, int child_ext_id, int parent_ext_id) {
|
||||||
|
if (ds->pending_parent_count >= (int)(sizeof(ds->pending_parents) / sizeof(ds->pending_parents[0])))
|
||||||
|
return;
|
||||||
|
auto& p = ds->pending_parents[ds->pending_parent_count++];
|
||||||
|
p.child_ext_id = child_ext_id;
|
||||||
|
p.parent_ext_id = parent_ext_id;
|
||||||
|
p.expires_ms = montauk::get_milliseconds() + 2000;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply placements whose dialog has been mapped since the request arrived.
|
||||||
|
static bool flush_pending_parents(DesktopState* ds) {
|
||||||
|
bool changed = false;
|
||||||
|
uint64_t now = montauk::get_milliseconds();
|
||||||
|
for (int i = 0; i < ds->pending_parent_count;) {
|
||||||
|
auto& p = ds->pending_parents[i];
|
||||||
|
bool done = apply_parent(ds, p.child_ext_id, p.parent_ext_id);
|
||||||
|
changed |= done;
|
||||||
|
if (done || now > p.expires_ms) {
|
||||||
|
p = ds->pending_parents[--ds->pending_parent_count];
|
||||||
|
} else {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Requests
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
static void handle_launch_item(DesktopState* ds, const WinRequest& req) {
|
||||||
|
WinReqLaunchItem p;
|
||||||
|
montauk::memset(&p, 0, sizeof(p));
|
||||||
|
montauk::memcpy(&p, req.payload, req.size < sizeof(p) ? req.size : sizeof(p));
|
||||||
|
p.itemId[sizeof(p.itemId) - 1] = '\0';
|
||||||
|
p.args[sizeof(p.args) - 1] = '\0';
|
||||||
|
|
||||||
|
for (int i = 0; i < ds->desktop_item_count; i++) {
|
||||||
|
const DesktopItem& item = ds->desktop_items[i];
|
||||||
|
if (!montauk::streq(item.id, p.itemId)) continue;
|
||||||
|
if (item.admin_only && !ds->is_admin) return;
|
||||||
|
|
||||||
|
// See WinReqLaunchItem: only document-opening apps receive args.
|
||||||
|
if (p.args[0] && item.launch_kind == DESKTOP_ITEM_LAUNCH_EXECUTABLE &&
|
||||||
|
item.file_types[0] && item.binary_path[0]) {
|
||||||
|
desktop_spawn_app(ds, item.binary_path, p.args);
|
||||||
|
} else {
|
||||||
|
desktop_launch_item(ds, &item);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_set_parent(DesktopState* ds, const WinRequest& req) {
|
||||||
|
if (req.size < sizeof(WinReqSetParent) || req.windowId < 0) return;
|
||||||
|
WinReqSetParent p;
|
||||||
|
montauk::memcpy(&p, req.payload, sizeof(p));
|
||||||
|
// A window may only attach itself to another window of the same app.
|
||||||
|
if (p.parentWindowId == req.windowId || window_owner_pid(p.parentWindowId) != req.senderPid)
|
||||||
|
return;
|
||||||
|
if (!apply_parent(ds, req.windowId, p.parentWindowId))
|
||||||
|
queue_parent(ds, req.windowId, p.parentWindowId);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void finish_drag(DesktopState* ds, bool cancelled);
|
||||||
|
|
||||||
|
static void start_drag_state(DesktopState* ds, uint32_t serial, int source_ext_id, uint8_t actions,
|
||||||
|
uint32_t item_count, const char* label) {
|
||||||
|
if (ds->drag.active) finish_drag(ds, true);
|
||||||
|
montauk::memset(&ds->drag, 0, sizeof(ds->drag));
|
||||||
|
ds->drag.active = true;
|
||||||
|
ds->drag.serial = serial;
|
||||||
|
ds->drag.source_ext_id = source_ext_id;
|
||||||
|
ds->drag.actions = actions;
|
||||||
|
ds->drag.item_count = item_count;
|
||||||
|
if (label) montauk::strncpy(ds->drag.label, label, sizeof(ds->drag.label));
|
||||||
|
ds->drag.target_ext_id = -1;
|
||||||
|
ds->drag.last_x = -1;
|
||||||
|
ds->drag.last_y = -1;
|
||||||
|
|
||||||
|
// The button may already be up (a very quick flick): end it right away
|
||||||
|
// rather than leave a drag that nothing will finish.
|
||||||
|
if (!(ds->mouse.buttons & 1)) finish_drag(ds, true);
|
||||||
|
else desktop_drag_update(ds);
|
||||||
|
}
|
||||||
|
|
||||||
|
void desktop_drag_begin_local(DesktopState* ds, uint32_t serial, uint8_t actions,
|
||||||
|
uint32_t item_count, const char* label) {
|
||||||
|
start_drag_state(ds, serial, -1, actions, item_count, label);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_drag_begin(DesktopState* ds, const WinRequest& req) {
|
||||||
|
if (req.size < sizeof(WinReqDragBegin)) return;
|
||||||
|
WinReqDragBegin p;
|
||||||
|
montauk::memcpy(&p, req.payload, sizeof(p));
|
||||||
|
p.info.label[sizeof(p.info.label) - 1] = '\0';
|
||||||
|
start_drag_state(ds, p.serial, req.windowId, p.info.actions, p.info.itemCount, p.info.label);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_drag_status(DesktopState* ds, const WinRequest& req) {
|
||||||
|
if (req.size < sizeof(WinReqDragStatus)) return;
|
||||||
|
WinReqDragStatus p;
|
||||||
|
montauk::memcpy(&p, req.payload, sizeof(p));
|
||||||
|
if (!ds->drag.active || p.serial != ds->drag.serial) return;
|
||||||
|
if (req.windowId < 0 || req.windowId != ds->drag.target_ext_id) return;
|
||||||
|
ds->drag.target_action = p.action & ds->drag.actions;
|
||||||
|
ds->drag.target_answered = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool desktop_poll_requests(DesktopState* ds) {
|
||||||
|
bool changed = flush_pending_parents(ds);
|
||||||
|
|
||||||
|
WinRequest req;
|
||||||
|
// Bounded per frame so a flood of requests cannot stall compositing.
|
||||||
|
for (int n = 0; n < 32 && montauk::win_request_poll(&req) == 1; n++) {
|
||||||
|
switch (req.type) {
|
||||||
|
case WIN_REQ_LAUNCH_ITEM: handle_launch_item(ds, req); break;
|
||||||
|
case WIN_REQ_SET_PARENT: handle_set_parent(ds, req); break;
|
||||||
|
case WIN_REQ_DRAG_BEGIN: handle_drag_begin(ds, req); break;
|
||||||
|
case WIN_REQ_DRAG_STATUS: handle_drag_status(ds, req); break;
|
||||||
|
case WIN_REQ_ACTIVATE: {
|
||||||
|
int idx = find_external_window(ds, req.windowId);
|
||||||
|
if (idx >= 0) {
|
||||||
|
if (ds->windows[idx].state == WIN_MINIMIZED) ds->windows[idx].state = WIN_NORMAL;
|
||||||
|
desktop_raise_window(ds, idx);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: break; // unknown or newer request types are ignored
|
||||||
|
}
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Drag and drop
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
static bool desktop_point_over_window(DesktopState* ds, int mx, int my) {
|
||||||
|
for (int i = ds->window_count - 1; i >= 0; i--) {
|
||||||
|
const Window* win = &ds->windows[i];
|
||||||
|
if (win->state == WIN_MINIMIZED || win->state == WIN_CLOSED) continue;
|
||||||
|
if (win->frame.contains(mx, my)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The external window whose content is under the pointer, if it is the
|
||||||
|
// topmost window there. Internal desktop windows and the background are not
|
||||||
|
// drop targets.
|
||||||
|
static int drag_target_at(DesktopState* ds, int mx, int my, Rect* out_cr) {
|
||||||
|
for (int i = ds->window_count - 1; i >= 0; i--) {
|
||||||
|
Window* win = &ds->windows[i];
|
||||||
|
if (win->state == WIN_MINIMIZED || win->state == WIN_CLOSED) continue;
|
||||||
|
if (!win->frame.contains(mx, my)) continue;
|
||||||
|
Rect cr = desktop_content_rect(win);
|
||||||
|
if (!win->external || !cr.contains(mx, my)) return -1;
|
||||||
|
if (out_cr) *out_cr = cr;
|
||||||
|
return win->ext_win_id;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void finish_drag(DesktopState* ds, bool cancelled) {
|
||||||
|
if (!ds->drag.active) return;
|
||||||
|
int mx = ds->mouse.x;
|
||||||
|
int my = ds->mouse.y;
|
||||||
|
uint8_t result = 0;
|
||||||
|
|
||||||
|
int target = ds->drag.target_ext_id;
|
||||||
|
if (target >= 0 && find_external_window(ds, target) < 0) target = -1;
|
||||||
|
|
||||||
|
// Dropped on the background: the Desktop folder (or a folder icon).
|
||||||
|
if (target < 0 && ds->drag.over_desktop && !cancelled) {
|
||||||
|
uint8_t action = desktop_icons_drop_action(ds, mx, my);
|
||||||
|
if (action) {
|
||||||
|
desktop_icons_drop(ds, mx, my, action);
|
||||||
|
result = action;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target >= 0) {
|
||||||
|
Rect cr = {};
|
||||||
|
bool still_over = drag_target_at(ds, mx, my, &cr) == target;
|
||||||
|
// A target that answered "no" gets a leave, not a drop; one that never
|
||||||
|
// answered (an app without drag support) gets the drop and may ignore it.
|
||||||
|
uint8_t action = ds->drag.target_answered
|
||||||
|
? ds->drag.target_action
|
||||||
|
: gui::drag_pick_action(ds->drag.actions, desktop_modifier_bits(ds), WIN_DRAG_COPY);
|
||||||
|
if (!cancelled && still_over && action != 0) {
|
||||||
|
send_drag_event(ds, target, WIN_EVENT_DROP, mx - cr.x, my - cr.y, action);
|
||||||
|
result = action;
|
||||||
|
} else {
|
||||||
|
send_drag_event(ds, target, WIN_EVENT_DRAG_LEAVE, -1, -1, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (find_external_window(ds, ds->drag.source_ext_id) >= 0)
|
||||||
|
send_drag_event(ds, ds->drag.source_ext_id, WIN_EVENT_DRAG_END, -1, -1, result);
|
||||||
|
ds->drag.active = false;
|
||||||
|
desktop_icons_drag_done(ds);
|
||||||
|
}
|
||||||
|
|
||||||
|
void desktop_drag_cancel(DesktopState* ds) {
|
||||||
|
finish_drag(ds, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void desktop_drag_update(DesktopState* ds) {
|
||||||
|
if (!ds->drag.active) return;
|
||||||
|
if (ds->screen_locked) {
|
||||||
|
finish_drag(ds, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int mx = ds->mouse.x;
|
||||||
|
int my = ds->mouse.y;
|
||||||
|
if (!(ds->mouse.buttons & 1)) {
|
||||||
|
finish_drag(ds, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Rect cr = {};
|
||||||
|
int target = drag_target_at(ds, mx, my, &cr);
|
||||||
|
uint8_t mods = desktop_modifier_bits(ds);
|
||||||
|
|
||||||
|
// Over the background (not a window, not the panel): the desktop itself
|
||||||
|
// is the target and answers its own "drag status".
|
||||||
|
bool over_desktop = target < 0 && my >= PANEL_HEIGHT && !desktop_point_over_window(ds, mx, my);
|
||||||
|
if (over_desktop) {
|
||||||
|
ds->drag.target_action = desktop_icons_drop_action(ds, mx, my);
|
||||||
|
ds->drag.target_answered = true;
|
||||||
|
} else if (ds->drag.over_desktop) {
|
||||||
|
ds->desktop_drop_icon = -1;
|
||||||
|
ds->drag.target_answered = false;
|
||||||
|
ds->drag.target_action = 0;
|
||||||
|
}
|
||||||
|
ds->drag.over_desktop = over_desktop;
|
||||||
|
|
||||||
|
if (target != ds->drag.target_ext_id) {
|
||||||
|
if (ds->drag.target_ext_id >= 0 && find_external_window(ds, ds->drag.target_ext_id) >= 0)
|
||||||
|
send_drag_event(ds, ds->drag.target_ext_id, WIN_EVENT_DRAG_LEAVE, -1, -1, 0);
|
||||||
|
ds->drag.target_ext_id = target;
|
||||||
|
if (!over_desktop) {
|
||||||
|
ds->drag.target_action = 0;
|
||||||
|
ds->drag.target_answered = false;
|
||||||
|
}
|
||||||
|
if (target >= 0)
|
||||||
|
send_drag_event(ds, target, WIN_EVENT_DRAG_ENTER, mx - cr.x, my - cr.y, ds->drag.actions);
|
||||||
|
} else if (target >= 0 && (mx != ds->drag.last_x || my != ds->drag.last_y ||
|
||||||
|
mods != ds->drag.last_modifiers)) {
|
||||||
|
send_drag_event(ds, target, WIN_EVENT_DRAG_OVER, mx - cr.x, my - cr.y, ds->drag.actions);
|
||||||
|
}
|
||||||
|
ds->drag.last_x = mx;
|
||||||
|
ds->drag.last_y = my;
|
||||||
|
ds->drag.last_modifiers = mods;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Badge next to the pointer: what is being dragged, and what a drop here
|
||||||
|
// would do.
|
||||||
|
void desktop_draw_drag(DesktopState* ds) {
|
||||||
|
if (!ds->drag.active) return;
|
||||||
|
|
||||||
|
char text[96];
|
||||||
|
if (ds->drag.item_count > 1)
|
||||||
|
snprintf(text, sizeof(text), "%d items", (int)ds->drag.item_count);
|
||||||
|
else
|
||||||
|
montauk::strncpy(text, ds->drag.label[0] ? ds->drag.label : "1 item", sizeof(text));
|
||||||
|
// Keep the badge compact: cut long names.
|
||||||
|
int max_text_w = 220;
|
||||||
|
if (text_width(text) > max_text_w) {
|
||||||
|
int len = montauk::slen(text);
|
||||||
|
while (len > 0 && text_width(text) + text_width("...") > max_text_w) {
|
||||||
|
len = montauk::utf8::prev_boundary(text, len);
|
||||||
|
text[len] = '\0';
|
||||||
|
}
|
||||||
|
str_append(text, "...", sizeof(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* action_text = nullptr;
|
||||||
|
bool refused = false;
|
||||||
|
if (ds->drag.target_ext_id >= 0 || ds->drag.over_desktop) {
|
||||||
|
uint8_t action = ds->drag.target_answered
|
||||||
|
? ds->drag.target_action
|
||||||
|
: gui::drag_pick_action(ds->drag.actions, desktop_modifier_bits(ds), WIN_DRAG_COPY);
|
||||||
|
if (ds->drag.target_answered && action == 0) refused = true;
|
||||||
|
else if (action == WIN_DRAG_MOVE) action_text = "Move";
|
||||||
|
else if (action == WIN_DRAG_COPY) action_text = "Copy";
|
||||||
|
else if (action == WIN_DRAG_LINK) action_text = "Link";
|
||||||
|
}
|
||||||
|
|
||||||
|
Framebuffer& fb = ds->fb;
|
||||||
|
Canvas c(fb.buffer(), fb.pitch() / 4, ds->screen_h);
|
||||||
|
gui::mtk::Theme theme = gui::mtk::make_theme(ds->settings.accent_color);
|
||||||
|
|
||||||
|
int fh = system_font_height();
|
||||||
|
int pad = 8;
|
||||||
|
int tw = text_width(text);
|
||||||
|
int aw = action_text ? text_width(action_text) + 12 : 0;
|
||||||
|
int w = pad + tw + (aw ? 8 + aw : 0) + pad;
|
||||||
|
int h = fh + 10;
|
||||||
|
int x = ds->mouse.x + 16;
|
||||||
|
int y = ds->mouse.y + 18;
|
||||||
|
if (x + w > ds->screen_w) x = ds->screen_w - w;
|
||||||
|
if (y + h > ds->screen_h) y = ds->mouse.y - h - 4;
|
||||||
|
|
||||||
|
Color bg = refused ? Color::from_rgb(0xF2, 0xF2, 0xF2) : colors::WHITE;
|
||||||
|
c.fill_rounded_rect(x + 2, y + 2, w, h, 5, Color::from_rgb(0x90, 0x90, 0x90));
|
||||||
|
c.fill_rounded_rect(x, y, w, h, 5, bg);
|
||||||
|
c.rect(x, y, w, h, refused ? theme.border : ds->settings.accent_color);
|
||||||
|
c.text(x + pad, y + 5, text, refused ? theme.text_muted : theme.text);
|
||||||
|
if (action_text) {
|
||||||
|
int ax = x + pad + tw + 8;
|
||||||
|
c.fill_rounded_rect(ax, y + 3, aw, h - 6, 4, ds->settings.accent_color);
|
||||||
|
c.text(ax + 6, y + 5, action_text, colors::WHITE);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# Makefile for Files (file manager, its own process) on MontaukOS
|
||||||
|
# Copyright (c) 2026 Daniel Hammer
|
||||||
|
|
||||||
|
MAKEFLAGS += -rR
|
||||||
|
.SUFFIXES:
|
||||||
|
|
||||||
|
# ---- Toolchain ----
|
||||||
|
|
||||||
|
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-montauk-
|
||||||
|
ifeq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
|
||||||
|
CXX = $(error x86_64-montauk toolchain not found. Run ./toolchain/build-montauk-toolchain.sh first)
|
||||||
|
else
|
||||||
|
CXX := $(TOOLCHAIN_PREFIX)g++
|
||||||
|
endif
|
||||||
|
|
||||||
|
# ---- Paths ----
|
||||||
|
|
||||||
|
PROG_INC := ../../include
|
||||||
|
LINK_LD := ../../link.ld
|
||||||
|
BINDIR := ../../bin
|
||||||
|
OBJDIR := obj
|
||||||
|
LIBDIR := ../../lib
|
||||||
|
BEARSSL := ../../lib/bearssl
|
||||||
|
JPEG_LIB := ../../lib/libjpeg
|
||||||
|
|
||||||
|
# ---- Compiler flags ----
|
||||||
|
|
||||||
|
CXXFLAGS := \
|
||||||
|
-std=gnu++20 \
|
||||||
|
-g -O2 -pipe \
|
||||||
|
-Wall \
|
||||||
|
-Wextra \
|
||||||
|
-Wno-unused-parameter \
|
||||||
|
-Wno-unused-function \
|
||||||
|
-ffreestanding \
|
||||||
|
-fno-stack-protector \
|
||||||
|
-fno-stack-check \
|
||||||
|
-fno-rtti \
|
||||||
|
-fno-exceptions \
|
||||||
|
-ffunction-sections \
|
||||||
|
-fdata-sections \
|
||||||
|
-msse \
|
||||||
|
-msse2 \
|
||||||
|
-I $(PROG_INC) \
|
||||||
|
-isystem $(PROG_INC)/libc \
|
||||||
|
-I $(BEARSSL)/inc
|
||||||
|
|
||||||
|
# ---- Linker flags ----
|
||||||
|
|
||||||
|
LDFLAGS := \
|
||||||
|
-nostdlib \
|
||||||
|
-Wl,--gc-sections \
|
||||||
|
-T $(LINK_LD)
|
||||||
|
|
||||||
|
# ---- Source files ----
|
||||||
|
|
||||||
|
SRCS := $(sort $(wildcard *.cpp))
|
||||||
|
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
|
||||||
|
|
||||||
|
# ---- Target ----
|
||||||
|
|
||||||
|
TARGET := $(BINDIR)/apps/files/files.elf
|
||||||
|
|
||||||
|
.PHONY: all clean
|
||||||
|
|
||||||
|
all: $(TARGET)
|
||||||
|
|
||||||
|
LIBS := $(JPEG_LIB)/libjpeg.a $(BEARSSL)/libbearssl.a $(LIBDIR)/libc/liblibc.a
|
||||||
|
|
||||||
|
$(TARGET): $(OBJS) $(LINK_LD) Makefile $(LIBS)
|
||||||
|
mkdir -p $(BINDIR)/apps/files
|
||||||
|
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBS) -o $@
|
||||||
|
|
||||||
|
$(OBJDIR)/%.o: %.cpp Makefile
|
||||||
|
mkdir -p $(OBJDIR)
|
||||||
|
$(CXX) $(CXXFLAGS) -MMD -MP -c $< -o $@
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf $(OBJDIR) $(TARGET)
|
||||||
|
|
||||||
|
# Auto-generated header dependency tracking (-MMD -MP).
|
||||||
|
-include $(OBJS:.o=.d)
|
||||||
+164
-19
@@ -1,13 +1,17 @@
|
|||||||
/*
|
/*
|
||||||
* actions.cpp
|
* actions.cpp
|
||||||
* Clipboard, rename, delete, and context-menu actions for the embedded desktop file manager
|
* Clipboard, rename, delete, and context-menu actions for the Files file manager
|
||||||
* Copyright (c) 2026 Daniel Hammer
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "filemanager_internal.hpp"
|
#include "filemanager_internal.hpp"
|
||||||
|
#include <gui/transfer.hpp>
|
||||||
|
|
||||||
namespace filemanager {
|
namespace filemanager {
|
||||||
|
|
||||||
|
// Publish the selection to the system clipboard: a file list for Files
|
||||||
|
// (and anything else that understands it), whether it is a cut, and the
|
||||||
|
// paths as plain text for everything else.
|
||||||
static void filemanager_load_clipboard(FileManagerState* fm, bool is_cut) {
|
static void filemanager_load_clipboard(FileManagerState* fm, bool is_cut) {
|
||||||
if (filemanager_is_computer_view(fm) || filemanager_is_virtual_view(fm)) return;
|
if (filemanager_is_computer_view(fm) || filemanager_is_virtual_view(fm)) return;
|
||||||
if (fm->entry_count <= 0) return;
|
if (fm->entry_count <= 0) return;
|
||||||
@@ -15,22 +19,77 @@ static void filemanager_load_clipboard(FileManagerState* fm, bool is_cut) {
|
|||||||
int* sel = (int*)montauk::malloc((uint64_t)fm->entry_count * sizeof(int));
|
int* sel = (int*)montauk::malloc((uint64_t)fm->entry_count * sizeof(int));
|
||||||
if (!sel) return;
|
if (!sel) return;
|
||||||
int n = filemanager_collect_selection(fm, sel, fm->entry_count);
|
int n = filemanager_collect_selection(fm, sel, fm->entry_count);
|
||||||
if (n <= 0) { montauk::mfree(sel); return; }
|
|
||||||
if (!filemanager_ensure_clipboard_capacity(fm, n)) { montauk::mfree(sel); return; }
|
|
||||||
|
|
||||||
|
uint64_t cap = (uint64_t)n * 260 + 1;
|
||||||
|
char* list = (char*)montauk::malloc(cap);
|
||||||
|
if (!list) { montauk::mfree(sel); return; }
|
||||||
|
uint64_t len = 0;
|
||||||
int out = 0;
|
int out = 0;
|
||||||
for (int i = 0; i < n && out < fm->clipboard_cap; i++) {
|
for (int i = 0; i < n; i++) {
|
||||||
int idx = sel[i];
|
int idx = sel[i];
|
||||||
if (fm->entry_types[idx] == FM_ENTRY_DRIVE) continue;
|
if (fm->entry_types[idx] == FM_ENTRY_DRIVE) continue;
|
||||||
filemanager_build_fullpath(fm->clipboard_paths[out], 256,
|
char path[256];
|
||||||
fm->current_path, fm->entry_names[idx]);
|
filemanager_build_fullpath(path, sizeof(path), fm->current_path, fm->entry_names[idx]);
|
||||||
|
int plen = montauk::slen(path);
|
||||||
|
if (len + plen + 2 > cap) break;
|
||||||
|
if (out > 0) list[len++] = '\n';
|
||||||
|
montauk::memcpy(list + len, path, plen);
|
||||||
|
len += plen;
|
||||||
out++;
|
out++;
|
||||||
}
|
}
|
||||||
|
list[len] = '\0';
|
||||||
montauk::mfree(sel);
|
montauk::mfree(sel);
|
||||||
if (out == 0) return;
|
|
||||||
|
|
||||||
fm->clipboard_count = out;
|
if (out > 0) {
|
||||||
fm->clipboard_is_cut = is_cut;
|
gui::TransferBuilder transfer;
|
||||||
|
transfer.add(montauk::abi::TRANSFER_MIME_FILE_LIST, list, (uint32_t)len);
|
||||||
|
transfer.add_text(montauk::abi::TRANSFER_MIME_FILE_OP, is_cut ? "cut" : "copy");
|
||||||
|
transfer.add(montauk::abi::TRANSFER_MIME_TEXT, list, (uint32_t)len);
|
||||||
|
if (gui::clipboard_set(transfer)) {
|
||||||
|
fm->clipboard_checked_serial = 0; // re-check on next draw
|
||||||
|
} else {
|
||||||
|
filemanager_show_error(fm->desktop, fm, "Couldn't copy to the clipboard",
|
||||||
|
nullptr, montauk::Success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
montauk::mfree(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool filemanager_clipboard_has_files(FileManagerState* fm) {
|
||||||
|
uint64_t serial = gui::clipboard_serial();
|
||||||
|
if (serial != fm->clipboard_checked_serial) {
|
||||||
|
fm->clipboard_checked_serial = serial;
|
||||||
|
fm->clipboard_has_files = gui::clipboard_has_format(montauk::abi::TRANSFER_MIME_FILE_LIST);
|
||||||
|
}
|
||||||
|
return fm->clipboard_has_files;
|
||||||
|
}
|
||||||
|
|
||||||
|
int filemanager_sync_clipboard(FileManagerState* fm) {
|
||||||
|
fm->clipboard_count = 0;
|
||||||
|
fm->clipboard_is_cut = false;
|
||||||
|
fm->clipboard_serial = gui::clipboard_serial();
|
||||||
|
|
||||||
|
char* list = gui::clipboard_get_alloc(montauk::abi::TRANSFER_MIME_FILE_LIST);
|
||||||
|
if (!list) return 0;
|
||||||
|
char* op = gui::clipboard_get_alloc(montauk::abi::TRANSFER_MIME_FILE_OP);
|
||||||
|
fm->clipboard_is_cut = op && montauk::streq(op, "cut");
|
||||||
|
if (op) montauk::mfree(op);
|
||||||
|
|
||||||
|
gui::file_list_each(list, [fm](const char* path) {
|
||||||
|
if (!filemanager_ensure_clipboard_capacity(fm, fm->clipboard_count + 1)) return;
|
||||||
|
if (fm->clipboard_count >= fm->clipboard_cap) return;
|
||||||
|
montauk::strncpy(fm->clipboard_paths[fm->clipboard_count], path, 256);
|
||||||
|
fm->clipboard_count++;
|
||||||
|
});
|
||||||
|
montauk::mfree(list);
|
||||||
|
return fm->clipboard_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
void filemanager_consume_cut(FileManagerState* fm) {
|
||||||
|
if (!fm->clipboard_is_cut) return;
|
||||||
|
if (gui::clipboard_serial() == fm->clipboard_serial) montauk::clipboard_clear();
|
||||||
|
fm->clipboard_count = 0;
|
||||||
|
fm->clipboard_is_cut = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void filemanager_do_copy(FileManagerState* fm) {
|
void filemanager_do_copy(FileManagerState* fm) {
|
||||||
@@ -50,6 +109,11 @@ void filemanager_do_paste(FileManagerState* fm) {
|
|||||||
filemanager_start_paste_job(fm);
|
filemanager_start_paste_job(fm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// File names cannot hold path separators.
|
||||||
|
bool filemanager_name_char_ok(char ch, void*) {
|
||||||
|
return ch != '/' && ch != '\\';
|
||||||
|
}
|
||||||
|
|
||||||
void filemanager_start_rename(FileManagerState* fm) {
|
void filemanager_start_rename(FileManagerState* fm) {
|
||||||
if (fm->selected < 0 || fm->selected >= fm->entry_count) return;
|
if (fm->selected < 0 || fm->selected >= fm->entry_count) return;
|
||||||
// Rename is single-target; with several entries selected, `selected` is
|
// Rename is single-target; with several entries selected, `selected` is
|
||||||
@@ -58,11 +122,19 @@ void filemanager_start_rename(FileManagerState* fm) {
|
|||||||
if (filemanager_is_computer_view(fm) || filemanager_is_virtual_view(fm) ||
|
if (filemanager_is_computer_view(fm) || filemanager_is_virtual_view(fm) ||
|
||||||
filemanager_in_trash(fm) || fm->entry_types[fm->selected] == FM_ENTRY_DRIVE) return;
|
filemanager_in_trash(fm) || fm->entry_types[fm->selected] == FM_ENTRY_DRIVE) return;
|
||||||
|
|
||||||
|
// Select the name without its extension, so typing replaces just that.
|
||||||
|
const char* name = fm->entry_names[fm->selected];
|
||||||
|
int len = montauk::slen(name);
|
||||||
|
int stem = len;
|
||||||
|
if (!fm->is_dir[fm->selected]) {
|
||||||
|
for (int i = len - 1; i > 0; i--) {
|
||||||
|
if (name[i] == '.') { stem = i; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
fm->rename_active = true;
|
fm->rename_active = true;
|
||||||
fm->rename_idx = fm->selected;
|
fm->rename_idx = fm->selected;
|
||||||
montauk::strcpy(fm->rename_buf, fm->entry_names[fm->selected]);
|
// Entry names hold 127 bytes.
|
||||||
fm->rename_len = montauk::slen(fm->rename_buf);
|
filemanager_field_set(fm->rename, name, 128, 4, 0, stem, filemanager_name_char_ok);
|
||||||
fm->rename_cursor = fm->rename_len;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void filemanager_finish_rename(FileManagerState* fm) {
|
void filemanager_finish_rename(FileManagerState* fm) {
|
||||||
@@ -70,14 +142,15 @@ void filemanager_finish_rename(FileManagerState* fm) {
|
|||||||
fm->rename_active = false;
|
fm->rename_active = false;
|
||||||
|
|
||||||
// If name unchanged, skip
|
// If name unchanged, skip
|
||||||
if (montauk::streq(fm->rename_buf, fm->entry_names[fm->rename_idx])) return;
|
const char* new_name = fm->rename.buf;
|
||||||
if (fm->rename_len == 0) return;
|
if (montauk::streq(new_name, fm->entry_names[fm->rename_idx])) return;
|
||||||
|
if (new_name[0] == '\0') return;
|
||||||
|
|
||||||
char old_path[512], new_path[512];
|
char old_path[512], new_path[512];
|
||||||
filemanager_build_fullpath(old_path, 512,
|
filemanager_build_fullpath(old_path, 512,
|
||||||
fm->current_path, fm->entry_names[fm->rename_idx]);
|
fm->current_path, fm->entry_names[fm->rename_idx]);
|
||||||
filemanager_build_fullpath(new_path, 512,
|
filemanager_build_fullpath(new_path, 512,
|
||||||
fm->current_path, fm->rename_buf);
|
fm->current_path, new_name);
|
||||||
|
|
||||||
montauk::StatusCode status =
|
montauk::StatusCode status =
|
||||||
filemanager_status_from_result(montauk::frename(old_path, new_path));
|
filemanager_status_from_result(montauk::frename(old_path, new_path));
|
||||||
@@ -100,7 +173,11 @@ void filemanager_finish_rename(FileManagerState* fm) {
|
|||||||
char heading[192];
|
char heading[192];
|
||||||
snprintf(heading, sizeof(heading), "Couldn't rename \"%s\"", path_basename(old_path));
|
snprintf(heading, sizeof(heading), "Couldn't rename \"%s\"", path_basename(old_path));
|
||||||
filemanager_show_error(fm->desktop, fm, heading, old_path, status);
|
filemanager_show_error(fm->desktop, fm, heading, old_path, status);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
const char* from = old_path;
|
||||||
|
const char* to = new_path;
|
||||||
|
filemanager_undo_record(fm, FM_UNDO_RENAME, &from, &to, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void filemanager_cancel_rename(FileManagerState* fm) {
|
void filemanager_cancel_rename(FileManagerState* fm) {
|
||||||
@@ -133,6 +210,8 @@ void filemanager_new_folder(FileManagerState* fm) {
|
|||||||
fm->current_path, status);
|
fm->current_path, status);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const char* created = path;
|
||||||
|
filemanager_undo_record(fm, FM_UNDO_NEW_FOLDER, nullptr, &created, 1);
|
||||||
|
|
||||||
// Select the new folder and start rename
|
// Select the new folder and start rename
|
||||||
for (int i = 0; i < fm->entry_count; i++) {
|
for (int i = 0; i < fm->entry_count; i++) {
|
||||||
@@ -193,13 +272,29 @@ static void filemanager_trash_selected(FileManagerState* fm, const int* indices,
|
|||||||
int failed = 0;
|
int failed = 0;
|
||||||
int first_failed = -1;
|
int first_failed = -1;
|
||||||
montauk::StatusCode first_error = montauk::Success;
|
montauk::StatusCode first_error = montauk::Success;
|
||||||
|
char (*trashed)[256] = (char(*)[256])montauk::malloc((uint64_t)count * 256);
|
||||||
|
const char** undo_from = (const char**)montauk::malloc((uint64_t)count * sizeof(char*));
|
||||||
|
const char** undo_to = (const char**)montauk::malloc((uint64_t)count * sizeof(char*));
|
||||||
|
int undo_n = 0;
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
montauk::StatusCode status = filemanager_trash_item(fm->desktop, paths[i]);
|
char where[256] = "";
|
||||||
if (status != montauk::Success && failed++ == 0) {
|
montauk::StatusCode status = filemanager_trash_item(fm->desktop, paths[i], where, sizeof(where));
|
||||||
|
if (status == montauk::Success) {
|
||||||
|
if (trashed && undo_from && undo_to) {
|
||||||
|
montauk::strncpy(trashed[undo_n], where, 256);
|
||||||
|
undo_from[undo_n] = paths[i];
|
||||||
|
undo_to[undo_n] = trashed[undo_n];
|
||||||
|
undo_n++;
|
||||||
|
}
|
||||||
|
} else if (failed++ == 0) {
|
||||||
first_failed = i;
|
first_failed = i;
|
||||||
first_error = status;
|
first_error = status;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (undo_n > 0) filemanager_undo_record(fm, FM_UNDO_TRASH, undo_from, undo_to, undo_n);
|
||||||
|
montauk::mfree(trashed);
|
||||||
|
montauk::mfree(undo_from);
|
||||||
|
montauk::mfree(undo_to);
|
||||||
filemanager_clear_multi_selection(fm);
|
filemanager_clear_multi_selection(fm);
|
||||||
filemanager_read_dir(fm);
|
filemanager_read_dir(fm);
|
||||||
|
|
||||||
@@ -319,6 +414,27 @@ void filemanager_show_empty_trash_confirmation(FileManagerState* fm) {
|
|||||||
montauk::mfree(paths);
|
montauk::mfree(paths);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void filemanager_toggle_hidden(FileManagerState* fm) {
|
||||||
|
fm->show_hidden = !fm->show_hidden;
|
||||||
|
prefs_set_show_hidden(fm->desktop, fm->show_hidden);
|
||||||
|
if (!filemanager_is_computer_view(fm) && !filemanager_is_virtual_view(fm))
|
||||||
|
filemanager_read_dir(fm);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bookmark a folder entry, or the current folder when target_idx < 0.
|
||||||
|
void filemanager_add_bookmark(FileManagerState* fm, int target_idx) {
|
||||||
|
char path[256];
|
||||||
|
if (target_idx >= 0 && target_idx < fm->entry_count) {
|
||||||
|
if (!fm->is_dir[target_idx]) return;
|
||||||
|
filemanager_build_fullpath(path, sizeof(path), fm->current_path, fm->entry_names[target_idx]);
|
||||||
|
} else {
|
||||||
|
if (filemanager_is_computer_view(fm) || filemanager_is_virtual_view(fm)) return;
|
||||||
|
montauk::strncpy(path, fm->current_path, sizeof(path));
|
||||||
|
}
|
||||||
|
prefs_add_bookmark(fm->desktop, path);
|
||||||
|
filemanager_sidebar_rebuild(fm);
|
||||||
|
}
|
||||||
|
|
||||||
void filemanager_open_ctx_menu(FileManagerState* fm, int local_x, int local_y, int target_idx) {
|
void filemanager_open_ctx_menu(FileManagerState* fm, int local_x, int local_y, int target_idx) {
|
||||||
fm->ctx_open = true;
|
fm->ctx_open = true;
|
||||||
fm->ctx_x = local_x;
|
fm->ctx_x = local_x;
|
||||||
@@ -326,6 +442,22 @@ void filemanager_open_ctx_menu(FileManagerState* fm, int local_x, int local_y, i
|
|||||||
fm->ctx_target_idx = target_idx;
|
fm->ctx_target_idx = target_idx;
|
||||||
fm->ctx_hover = -1;
|
fm->ctx_hover = -1;
|
||||||
fm->ctx_item_count = 0;
|
fm->ctx_item_count = 0;
|
||||||
|
fm->ctx_sub_open = false;
|
||||||
|
fm->ctx_sub_hover = -1;
|
||||||
|
fm->ctx_sub_count = 0;
|
||||||
|
|
||||||
|
// Sidebar rows (target -2 - row).
|
||||||
|
if (target_idx <= -2) {
|
||||||
|
int row = -2 - target_idx;
|
||||||
|
if (row >= fm->sidebar_count) { fm->ctx_open = false; return; }
|
||||||
|
const FileManagerSidebarItem& item = fm->sidebar_items[row];
|
||||||
|
fm->ctx_items[fm->ctx_item_count++] = CTX_OPEN;
|
||||||
|
if (item.kind == FM_SIDEBAR_TRASH && filemanager_trash_has_items(fm->desktop))
|
||||||
|
fm->ctx_items[fm->ctx_item_count++] = CTX_EMPTY_TRASH;
|
||||||
|
if (item.bookmark)
|
||||||
|
fm->ctx_items[fm->ctx_item_count++] = CTX_REMOVE_BOOKMARK;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (target_idx >= 0 && target_idx < fm->entry_count) {
|
if (target_idx >= 0 && target_idx < fm->entry_count) {
|
||||||
int tt = fm->entry_types[target_idx];
|
int tt = fm->entry_types[target_idx];
|
||||||
@@ -361,6 +493,13 @@ void filemanager_open_ctx_menu(FileManagerState* fm, int local_x, int local_y, i
|
|||||||
if (filemanager_can_trash(fm))
|
if (filemanager_can_trash(fm))
|
||||||
fm->ctx_items[fm->ctx_item_count++] = CTX_TRASH;
|
fm->ctx_items[fm->ctx_item_count++] = CTX_TRASH;
|
||||||
fm->ctx_items[fm->ctx_item_count++] = CTX_DELETE;
|
fm->ctx_items[fm->ctx_item_count++] = CTX_DELETE;
|
||||||
|
if (!several && fm->is_dir[target_idx]) {
|
||||||
|
char path[256];
|
||||||
|
filemanager_build_fullpath(path, sizeof(path), fm->current_path,
|
||||||
|
fm->entry_names[target_idx]);
|
||||||
|
if (!prefs_has_bookmark(path))
|
||||||
|
fm->ctx_items[fm->ctx_item_count++] = CTX_ADD_BOOKMARK;
|
||||||
|
}
|
||||||
fm->ctx_items[fm->ctx_item_count++] = CTX_PROPERTIES;
|
fm->ctx_items[fm->ctx_item_count++] = CTX_PROPERTIES;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -376,12 +515,16 @@ void filemanager_open_ctx_menu(FileManagerState* fm, int local_x, int local_y, i
|
|||||||
if (fm->entry_count > 0)
|
if (fm->entry_count > 0)
|
||||||
fm->ctx_items[fm->ctx_item_count++] = CTX_EMPTY_TRASH;
|
fm->ctx_items[fm->ctx_item_count++] = CTX_EMPTY_TRASH;
|
||||||
} else if (!filemanager_in_trash(fm)) {
|
} else if (!filemanager_in_trash(fm)) {
|
||||||
if (fm->clipboard_count > 0)
|
if (filemanager_clipboard_has_files(fm))
|
||||||
fm->ctx_items[fm->ctx_item_count++] = CTX_PASTE;
|
fm->ctx_items[fm->ctx_item_count++] = CTX_PASTE;
|
||||||
fm->ctx_items[fm->ctx_item_count++] = CTX_NEW_FOLDER;
|
fm->ctx_items[fm->ctx_item_count++] = CTX_NEW;
|
||||||
|
filemanager_build_new_submenu(fm);
|
||||||
}
|
}
|
||||||
|
if (filemanager_can_undo(fm))
|
||||||
|
fm->ctx_items[fm->ctx_item_count++] = CTX_UNDO;
|
||||||
if (fm->entry_count > 0)
|
if (fm->entry_count > 0)
|
||||||
fm->ctx_items[fm->ctx_item_count++] = CTX_SELECT_ALL;
|
fm->ctx_items[fm->ctx_item_count++] = CTX_SELECT_ALL;
|
||||||
|
fm->ctx_items[fm->ctx_item_count++] = CTX_TOGGLE_HIDDEN;
|
||||||
fm->ctx_items[fm->ctx_item_count++] = CTX_SORT_NAME;
|
fm->ctx_items[fm->ctx_item_count++] = CTX_SORT_NAME;
|
||||||
fm->ctx_items[fm->ctx_item_count++] = CTX_SORT_SIZE;
|
fm->ctx_items[fm->ctx_item_count++] = CTX_SORT_SIZE;
|
||||||
fm->ctx_items[fm->ctx_item_count++] = CTX_SORT_TYPE;
|
fm->ctx_items[fm->ctx_item_count++] = CTX_SORT_TYPE;
|
||||||
@@ -394,6 +537,8 @@ void filemanager_open_ctx_menu(FileManagerState* fm, int local_x, int local_y, i
|
|||||||
void filemanager_close_ctx_menu(FileManagerState* fm) {
|
void filemanager_close_ctx_menu(FileManagerState* fm) {
|
||||||
fm->ctx_open = false;
|
fm->ctx_open = false;
|
||||||
fm->ctx_hover = -1;
|
fm->ctx_hover = -1;
|
||||||
|
fm->ctx_sub_open = false;
|
||||||
|
fm->ctx_sub_hover = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace filemanager
|
} // namespace filemanager
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* app.cpp
|
* app.cpp
|
||||||
* File manager launcher and desktop integration entry points
|
* Files window creation and icon loading
|
||||||
* Copyright (c) 2026 Daniel Hammer
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ static bool filemanager_open_virtual_path(FileManagerState* fm, const char* path
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void open_filemanager_internal(DesktopState* ds, const char* initial_path) {
|
static void open_filemanager_internal(DesktopState* ds, const char* initial_path) {
|
||||||
int idx = desktop_create_window(ds, "Files", 150, 120, 560, 420);
|
int idx = desktop_create_window(ds, "Files", 150, 120, 780, 500);
|
||||||
if (idx < 0) return;
|
if (idx < 0) return;
|
||||||
|
|
||||||
Window* win = &ds->windows[idx];
|
Window* win = &ds->windows[idx];
|
||||||
@@ -43,6 +43,14 @@ static void open_filemanager_internal(DesktopState* ds, const char* initial_path
|
|||||||
fm->history_count = 0;
|
fm->history_count = 0;
|
||||||
fm->desktop = ds;
|
fm->desktop = ds;
|
||||||
fm->grid_view = true;
|
fm->grid_view = true;
|
||||||
|
fm->press_idx = -1;
|
||||||
|
fm->drop_target_idx = -1;
|
||||||
|
fm->drop_sidebar_idx = -1;
|
||||||
|
fm->sidebar_hover = -1;
|
||||||
|
|
||||||
|
prefs_load(ds);
|
||||||
|
fm->show_hidden = prefs_show_hidden();
|
||||||
|
fm->sidebar_width = prefs_sidebar_width();
|
||||||
|
|
||||||
fm->scrollbar.init(0, 0, FM_SCROLLBAR_W, 100);
|
fm->scrollbar.init(0, 0, FM_SCROLLBAR_W, 100);
|
||||||
|
|
||||||
@@ -65,12 +73,14 @@ static void open_filemanager_internal(DesktopState* ds, const char* initial_path
|
|||||||
filemanager_read_drives(fm);
|
filemanager_read_drives(fm);
|
||||||
}
|
}
|
||||||
filemanager_push_history(fm);
|
filemanager_push_history(fm);
|
||||||
|
filemanager_sidebar_rebuild(fm);
|
||||||
|
|
||||||
win->app_data = fm;
|
win->app_data = fm;
|
||||||
win->on_draw = filemanager_on_draw;
|
win->on_draw = filemanager_on_draw;
|
||||||
win->on_mouse = filemanager_on_mouse;
|
win->on_mouse = filemanager_on_mouse;
|
||||||
win->on_key = filemanager_on_key;
|
win->on_key = filemanager_on_key;
|
||||||
win->on_close = filemanager_on_close;
|
win->on_close = filemanager_on_close;
|
||||||
|
win->on_poll = filemanager_on_poll;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace filemanager
|
} // namespace filemanager
|
||||||
@@ -112,14 +122,6 @@ void ensure_filemanager_icons_loaded(DesktopState* ds) {
|
|||||||
ds->icon_folder_new = svg_load("0:/os/icons/folder-new.svg", 16, 16, defColor);
|
ds->icon_folder_new = svg_load("0:/os/icons/folder-new.svg", 16, 16, defColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
void open_filemanager(DesktopState* ds) {
|
|
||||||
filemanager::open_filemanager_internal(ds, nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
void open_filemanager_path(DesktopState* ds, const char* path) {
|
void open_filemanager_path(DesktopState* ds, const char* path) {
|
||||||
filemanager::open_filemanager_internal(ds, path);
|
filemanager::open_filemanager_internal(ds, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
void open_system_configuration(DesktopState* ds) {
|
|
||||||
filemanager::open_filemanager_internal(ds, filemanager::FM_PATH_SETTINGS);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
/*
|
||||||
|
* associations.cpp
|
||||||
|
* File type associations for the Files file manager
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "filemanager_internal.hpp"
|
||||||
|
#include <montauk/config.h>
|
||||||
|
#include <gui/app_catalog.hpp>
|
||||||
|
|
||||||
|
namespace filemanager {
|
||||||
|
|
||||||
|
// Resolution is shared with the desktop (gui/app_catalog.hpp).
|
||||||
|
static bool assoc_item_launchable(const DesktopState* ds, const DesktopItem& item) {
|
||||||
|
return gui::app_catalog_launchable(ds, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool assoc_item_claims(const DesktopItem& item, const char* ext) {
|
||||||
|
return gui::app_catalog_claims(item, ext);
|
||||||
|
}
|
||||||
|
|
||||||
|
int filemanager_assoc_default(DesktopState* ds, const char* ext) {
|
||||||
|
return gui::app_catalog_default_for(ds, ext);
|
||||||
|
}
|
||||||
|
|
||||||
|
int filemanager_assoc_candidates(DesktopState* ds, const char* ext,
|
||||||
|
int* out, int max, int* recommended) {
|
||||||
|
if (recommended) *recommended = 0;
|
||||||
|
if (!ds || !out || max <= 0 || ds->desktop_item_count <= 0) return 0;
|
||||||
|
|
||||||
|
int* sorted = (int*)montauk::malloc((uint64_t)ds->desktop_item_count * sizeof(int));
|
||||||
|
if (!sorted) return 0;
|
||||||
|
int n = 0;
|
||||||
|
for (int i = 0; i < ds->desktop_item_count; i++) {
|
||||||
|
if (assoc_item_launchable(ds, ds->desktop_items[i])) sorted[n++] = i;
|
||||||
|
}
|
||||||
|
for (int i = 1; i < n; i++) {
|
||||||
|
int tmp = sorted[i];
|
||||||
|
int j = i - 1;
|
||||||
|
while (j >= 0 && str_compare_ci(ds->desktop_items[tmp].name,
|
||||||
|
ds->desktop_items[sorted[j]].name) < 0) {
|
||||||
|
sorted[j + 1] = sorted[j];
|
||||||
|
j--;
|
||||||
|
}
|
||||||
|
sorted[j + 1] = tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apps that declare the type lead; any other regular app follows (an app
|
||||||
|
// that declares nothing may still open the file). Hidden and settings
|
||||||
|
// items only appear when they claim the type.
|
||||||
|
int count = 0;
|
||||||
|
for (int i = 0; i < n && count < max; i++) {
|
||||||
|
if (assoc_item_claims(ds->desktop_items[sorted[i]], ext)) out[count++] = sorted[i];
|
||||||
|
}
|
||||||
|
if (recommended) *recommended = count;
|
||||||
|
for (int i = 0; i < n && count < max; i++) {
|
||||||
|
const DesktopItem& item = ds->desktop_items[sorted[i]];
|
||||||
|
if (item.section == DESKTOP_ITEM_SECTION_APPS && !assoc_item_claims(item, ext))
|
||||||
|
out[count++] = sorted[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
montauk::mfree(sorted);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool filemanager_assoc_set_default(DesktopState* ds, const char* ext, int item_index) {
|
||||||
|
if (!ds || !ds->current_user[0] || !gui::app_catalog_ext_storable(ext)) return false;
|
||||||
|
if (item_index < 0 || item_index >= ds->desktop_item_count) return false;
|
||||||
|
|
||||||
|
auto doc = montauk::config::load_user(ds->current_user, gui::kAssociationsConfig);
|
||||||
|
char key[48];
|
||||||
|
gui::app_catalog_assoc_key(key, sizeof(key), ext);
|
||||||
|
montauk::config::set_string(&doc, key, ds->desktop_items[item_index].id);
|
||||||
|
int rc = montauk::config::save_user(ds->current_user, gui::kAssociationsConfig, &doc);
|
||||||
|
doc.destroy();
|
||||||
|
return rc == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool filemanager_file_looks_textual(const char* path) {
|
||||||
|
return gui::app_catalog_file_looks_textual(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool filemanager_launch_with(DesktopState* ds, int item_index, const char* path) {
|
||||||
|
if (!ds || item_index < 0 || item_index >= ds->desktop_item_count) return false;
|
||||||
|
const DesktopItem& item = ds->desktop_items[item_index];
|
||||||
|
if (!assoc_item_launchable(ds, item)) return false;
|
||||||
|
// Apps that declare file types are started by the desktop, with the
|
||||||
|
// authority they get from the launcher. The desktop forwards a document
|
||||||
|
// path only to those, so anything else is started here.
|
||||||
|
if (item.file_types[0]) return files_host::launch_item(item.id, path);
|
||||||
|
return desktop_spawn_app(ds, item.binary_path, path) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace filemanager
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
/*
|
||||||
|
* breadcrumbs.cpp
|
||||||
|
* Path bar for the Files file manager: clickable path segments and Tab
|
||||||
|
* completion while editing
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "filemanager_internal.hpp"
|
||||||
|
|
||||||
|
namespace filemanager {
|
||||||
|
|
||||||
|
enum CrumbKind : uint8_t {
|
||||||
|
CRUMB_COMPUTER,
|
||||||
|
CRUMB_PATH, // navigate to `path`
|
||||||
|
CRUMB_TRASH,
|
||||||
|
CRUMB_STATIC, // the current virtual view, or the "..." overflow marker
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Crumb {
|
||||||
|
CrumbKind kind;
|
||||||
|
char label[64];
|
||||||
|
char path[256];
|
||||||
|
int x, w;
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr int CRUMB_MAX = 32;
|
||||||
|
static constexpr int CRUMB_PAD = 6;
|
||||||
|
static constexpr int CRUMB_LEFT = 6;
|
||||||
|
|
||||||
|
static int crumb_sep_w() {
|
||||||
|
return text_width(">") + 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void crumb_add(Crumb* out, int* n, CrumbKind kind, const char* label, const char* path, int len) {
|
||||||
|
if (*n >= CRUMB_MAX) return;
|
||||||
|
Crumb& c = out[(*n)++];
|
||||||
|
c.kind = kind;
|
||||||
|
montauk::strncpy(c.label, label, sizeof(c.label));
|
||||||
|
c.path[0] = '\0';
|
||||||
|
if (path) {
|
||||||
|
if (len > (int)sizeof(c.path) - 1) len = sizeof(c.path) - 1;
|
||||||
|
for (int i = 0; i < len; i++) c.path[i] = path[i];
|
||||||
|
c.path[len] = '\0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Segments of path from character `from`, each navigable to its prefix.
|
||||||
|
static void crumb_add_segments(Crumb* out, int* n, const char* path, int from) {
|
||||||
|
int len = montauk::slen(path);
|
||||||
|
int i = from;
|
||||||
|
while (i < len) {
|
||||||
|
while (i < len && path[i] == '/') i++;
|
||||||
|
int start = i;
|
||||||
|
while (i < len && path[i] != '/') i++;
|
||||||
|
if (i == start) break;
|
||||||
|
char label[64];
|
||||||
|
int l = i - start;
|
||||||
|
if (l > 63) l = 63;
|
||||||
|
for (int k = 0; k < l; k++) label[k] = path[start + k];
|
||||||
|
label[l] = '\0';
|
||||||
|
crumb_add(out, n, CRUMB_PATH, label, path, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build and lay out the crumbs for the current location within max_w
|
||||||
|
// pixels. When they do not fit, leading folders collapse into "...".
|
||||||
|
static int crumb_build(FileManagerState* fm, Crumb* out, int max_w) {
|
||||||
|
int n = 0;
|
||||||
|
crumb_add(out, &n, CRUMB_COMPUTER, "Computer", nullptr, 0);
|
||||||
|
|
||||||
|
if (filemanager_is_virtual_view(fm)) {
|
||||||
|
crumb_add(out, &n, CRUMB_STATIC, filemanager_virtual_view_label(fm->virtual_view), nullptr, 0);
|
||||||
|
} else if (!filemanager_is_computer_view(fm)) {
|
||||||
|
char files[256];
|
||||||
|
if (filemanager_in_trash(fm) && filemanager_trash_files_dir(fm->desktop, files, sizeof(files))) {
|
||||||
|
crumb_add(out, &n, CRUMB_TRASH, "Trash", files, montauk::slen(files));
|
||||||
|
crumb_add_segments(out, &n, fm->current_path, montauk::slen(files));
|
||||||
|
} else {
|
||||||
|
// "0:/users/me" -> [0:] [users] [me]
|
||||||
|
const char* p = fm->current_path;
|
||||||
|
int colon = 0;
|
||||||
|
while (p[colon] && p[colon] != ':') colon++;
|
||||||
|
if (p[colon] == ':') {
|
||||||
|
char drive[16];
|
||||||
|
int dl = colon + 1 < 15 ? colon + 1 : 15;
|
||||||
|
for (int k = 0; k < dl; k++) drive[k] = p[k];
|
||||||
|
drive[dl] = '\0';
|
||||||
|
crumb_add(out, &n, CRUMB_PATH, drive, p, colon + 2); // "0:/"
|
||||||
|
crumb_add_segments(out, &n, p, colon + 2);
|
||||||
|
} else {
|
||||||
|
crumb_add_segments(out, &n, p, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int sep = crumb_sep_w();
|
||||||
|
auto width_from = [&](int first) {
|
||||||
|
int w = 0;
|
||||||
|
for (int i = first; i < n; i++) w += text_width(out[i].label) + CRUMB_PAD * 2 + (i > 0 ? sep : 0);
|
||||||
|
return w;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Drop crumbs after "Computer" until the rest fits, then mark the gap.
|
||||||
|
int first_kept = 1;
|
||||||
|
int ellipsis_w = text_width("...") + CRUMB_PAD * 2 + sep;
|
||||||
|
int total = text_width(out[0].label) + CRUMB_PAD * 2;
|
||||||
|
while (first_kept < n - 1 && total + ellipsis_w + width_from(first_kept) > max_w) first_kept++;
|
||||||
|
if (first_kept > 1) {
|
||||||
|
Crumb kept[CRUMB_MAX];
|
||||||
|
int k = 0;
|
||||||
|
kept[k++] = out[0];
|
||||||
|
crumb_add(kept, &k, CRUMB_STATIC, "...", nullptr, 0);
|
||||||
|
for (int i = first_kept; i < n; i++) kept[k++] = out[i];
|
||||||
|
for (int i = 0; i < k; i++) out[i] = kept[i];
|
||||||
|
n = k;
|
||||||
|
}
|
||||||
|
|
||||||
|
int x = CRUMB_LEFT;
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
if (i > 0) x += sep;
|
||||||
|
out[i].x = x;
|
||||||
|
out[i].w = text_width(out[i].label) + CRUMB_PAD * 2;
|
||||||
|
x += out[i].w;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
void filemanager_draw_breadcrumbs(Canvas& c, FileManagerState* fm, int y) {
|
||||||
|
Crumb crumbs[CRUMB_MAX];
|
||||||
|
int n = crumb_build(fm, crumbs, c.w - CRUMB_LEFT * 2);
|
||||||
|
int fh = system_font_height();
|
||||||
|
int ty = y + (FM_PATHBAR_H - fh) / 2;
|
||||||
|
Color muted = Color::from_rgb(0x88, 0x88, 0x88);
|
||||||
|
Color accent = fm->desktop ? fm->desktop->settings.accent_color : colors::ACCENT;
|
||||||
|
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
if (i > 0) c.text(crumbs[i].x - crumb_sep_w() + 4, ty, ">", muted);
|
||||||
|
bool last = i == n - 1;
|
||||||
|
Color color = crumbs[i].kind == CRUMB_STATIC && !last ? muted
|
||||||
|
: last ? colors::TEXT_COLOR : accent;
|
||||||
|
c.text(crumbs[i].x + CRUMB_PAD, ty, crumbs[i].label, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool filemanager_breadcrumb_click(FileManagerState* fm, int x, int content_w) {
|
||||||
|
Crumb crumbs[CRUMB_MAX];
|
||||||
|
int n = crumb_build(fm, crumbs, content_w - CRUMB_LEFT * 2);
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
if (x < crumbs[i].x || x >= crumbs[i].x + crumbs[i].w) continue;
|
||||||
|
// The last crumb is where we are: clicking it edits the path instead.
|
||||||
|
if (i == n - 1) return false;
|
||||||
|
switch (crumbs[i].kind) {
|
||||||
|
case CRUMB_COMPUTER: filemanager_go_home(fm); break;
|
||||||
|
case CRUMB_TRASH: filemanager_open_trash(fm); break;
|
||||||
|
case CRUMB_PATH: {
|
||||||
|
char path[256];
|
||||||
|
montauk::strncpy(path, crumbs[i].path, sizeof(path));
|
||||||
|
filemanager_go_to_path(fm, path);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Tab completion ---------------------------------------------------
|
||||||
|
|
||||||
|
static bool prefix_ci(const char* name, const char* prefix, int len) {
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
char a = name[i], b = prefix[i];
|
||||||
|
if (!a) return false;
|
||||||
|
if (a >= 'A' && a <= 'Z') a += 32;
|
||||||
|
if (b >= 'A' && b <= 'Z') b += 32;
|
||||||
|
if (a != b) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void filemanager_pathbar_complete(FileManagerState* fm) {
|
||||||
|
char* buf = fm->pathbar.buf;
|
||||||
|
int cursor = fm->pathbar.in.cursor;
|
||||||
|
int slash = -1;
|
||||||
|
for (int i = cursor - 1; i >= 0; i--) {
|
||||||
|
if (buf[i] == '/') { slash = i; break; }
|
||||||
|
}
|
||||||
|
if (slash < 0) return;
|
||||||
|
|
||||||
|
char dir[256];
|
||||||
|
int dl = slash + 1 < 255 ? slash + 1 : 255;
|
||||||
|
for (int i = 0; i < dl; i++) dir[i] = buf[i];
|
||||||
|
dir[dl] = '\0';
|
||||||
|
const char* prefix = buf + slash + 1;
|
||||||
|
int plen = cursor - slash - 1;
|
||||||
|
|
||||||
|
// Longest completion shared by every match; remember if the only match
|
||||||
|
// is a folder so a "/" can follow it.
|
||||||
|
char common[128] = "";
|
||||||
|
int common_len = -1;
|
||||||
|
int matches = 0;
|
||||||
|
bool only_dir = false;
|
||||||
|
|
||||||
|
static constexpr int BATCH = 64;
|
||||||
|
const char* names[BATCH];
|
||||||
|
int offset = 0;
|
||||||
|
for (;;) {
|
||||||
|
int got = montauk::readdir_at(dir, names, BATCH, offset);
|
||||||
|
if (got <= 0) break;
|
||||||
|
offset += got;
|
||||||
|
for (int i = 0; i < got; i++) {
|
||||||
|
const char* name = path_basename(names[i]);
|
||||||
|
int len = montauk::slen(name);
|
||||||
|
bool is_dir = len > 0 && name[len - 1] == '/';
|
||||||
|
if (is_dir) len--;
|
||||||
|
if (len < plen || !prefix_ci(name, prefix, plen)) continue;
|
||||||
|
if (!fm->show_hidden && name[0] == '.' && (plen == 0 || prefix[0] != '.')) continue;
|
||||||
|
matches++;
|
||||||
|
only_dir = is_dir;
|
||||||
|
if (common_len < 0) {
|
||||||
|
common_len = len < 127 ? len : 127;
|
||||||
|
for (int k = 0; k < common_len; k++) common[k] = name[k];
|
||||||
|
} else {
|
||||||
|
int k = 0;
|
||||||
|
while (k < common_len && k < len && common[k] == name[k]) k++;
|
||||||
|
common_len = k;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (offset >= FM_ENTRY_HARD_CAP) break;
|
||||||
|
}
|
||||||
|
if (matches == 0 || common_len < plen) return;
|
||||||
|
if (matches == 1 && only_dir && common_len < 127) common[common_len++] = '/';
|
||||||
|
common[common_len] = '\0';
|
||||||
|
|
||||||
|
// Replace the typed prefix with the completion, keeping any text after
|
||||||
|
// the cursor.
|
||||||
|
char tail[256];
|
||||||
|
montauk::strncpy(tail, buf + cursor, sizeof(tail));
|
||||||
|
int head = slash + 1;
|
||||||
|
char out[256];
|
||||||
|
int o = 0;
|
||||||
|
for (int i = 0; i < head && o < 255; i++) out[o++] = buf[i];
|
||||||
|
for (int i = 0; i < common_len && o < 255; i++) out[o++] = common[i];
|
||||||
|
int new_cursor = o;
|
||||||
|
for (int i = 0; tail[i] && o < 255; i++) out[o++] = tail[i];
|
||||||
|
out[o] = '\0';
|
||||||
|
|
||||||
|
montauk::strcpy(buf, out);
|
||||||
|
filemanager_field_select(fm->pathbar, new_cursor, new_cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace filemanager
|
||||||
@@ -0,0 +1,333 @@
|
|||||||
|
/*
|
||||||
|
* dnd.cpp
|
||||||
|
* Drag and drop for the Files file manager: dragging entries out, and
|
||||||
|
* accepting file drops onto folders, sidebar places and the Trash
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "filemanager_internal.hpp"
|
||||||
|
#include <gui/transfer.hpp>
|
||||||
|
|
||||||
|
using namespace montauk::abi;
|
||||||
|
|
||||||
|
namespace filemanager {
|
||||||
|
|
||||||
|
static constexpr int DRAG_THRESHOLD = 5; // pixels before a press becomes a drag
|
||||||
|
|
||||||
|
static FileManagerState* fm_of(gui::Window* win) {
|
||||||
|
return (win && win->on_draw == filemanager_on_draw) ? (FileManagerState*)win->app_data : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Dragging out ---------------------------------------------------------
|
||||||
|
|
||||||
|
static bool start_drag(FileManagerState* fm) {
|
||||||
|
if (filemanager_is_computer_view(fm) || filemanager_is_virtual_view(fm)) return false;
|
||||||
|
|
||||||
|
int* sel = (int*)montauk::malloc((uint64_t)fm->entry_count * sizeof(int));
|
||||||
|
if (!sel) return false;
|
||||||
|
int n = filemanager_collect_selection(fm, sel, fm->entry_count);
|
||||||
|
uint64_t cap = (uint64_t)n * 258 + 1;
|
||||||
|
char* list = (char*)montauk::malloc(cap);
|
||||||
|
if (!list) { montauk::mfree(sel); return false; }
|
||||||
|
|
||||||
|
uint64_t len = 0;
|
||||||
|
int count = 0;
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
char path[256];
|
||||||
|
filemanager_build_fullpath(path, sizeof(path), fm->current_path, fm->entry_names[sel[i]]);
|
||||||
|
int plen = montauk::slen(path);
|
||||||
|
if (count > 0) list[len++] = '\n';
|
||||||
|
montauk::memcpy(list + len, path, plen);
|
||||||
|
len += plen;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
list[len] = '\0';
|
||||||
|
const char* label = count == 1 ? fm->entry_names[sel[0]] : nullptr;
|
||||||
|
montauk::mfree(sel);
|
||||||
|
|
||||||
|
// Items leave the Trash only by Restore (moving them would orphan their
|
||||||
|
// restore info), so from there a drag can only copy.
|
||||||
|
uint8_t actions = filemanager_in_trash(fm) ? WIN_DRAG_COPY : (WIN_DRAG_COPY | WIN_DRAG_MOVE);
|
||||||
|
|
||||||
|
gui::TransferBuilder transfer;
|
||||||
|
transfer.add(TRANSFER_MIME_FILE_LIST, list, (uint32_t)len);
|
||||||
|
transfer.add(TRANSFER_MIME_TEXT, list, (uint32_t)len);
|
||||||
|
fm->drag_serial = count > 0
|
||||||
|
? gui::drag_begin(files_host::main_window_id(), transfer, actions, count, label)
|
||||||
|
: 0;
|
||||||
|
montauk::mfree(list);
|
||||||
|
return fm->drag_serial != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool filemanager_dnd_mouse(Window* win, FileManagerState* fm, const FileManagerLayout& l,
|
||||||
|
int x, int y, MouseEvent& ev) {
|
||||||
|
(void)win;
|
||||||
|
(void)l;
|
||||||
|
if (fm->press_idx < 0) return false;
|
||||||
|
|
||||||
|
// Release: the press was a click (already handled when it went down),
|
||||||
|
// or the end of our drag (the desktop sends DROP / DRAG_END).
|
||||||
|
if (!ev.left_held()) {
|
||||||
|
fm->press_idx = -1;
|
||||||
|
return fm->drag_serial != 0;
|
||||||
|
}
|
||||||
|
if (fm->drag_serial) return true;
|
||||||
|
|
||||||
|
int dx = x - fm->press_x;
|
||||||
|
int dy = y - fm->press_y;
|
||||||
|
if (dx * dx + dy * dy < DRAG_THRESHOLD * DRAG_THRESHOLD) return true;
|
||||||
|
|
||||||
|
if (!start_drag(fm)) fm->press_idx = -1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void filemanager_dnd_poll(FileManagerState* fm) {
|
||||||
|
uint64_t now = 0;
|
||||||
|
for (uint64_t& due : fm->refresh_due) {
|
||||||
|
if (!due) continue;
|
||||||
|
if (!now) now = montauk::get_milliseconds();
|
||||||
|
if (now < due) continue;
|
||||||
|
due = 0;
|
||||||
|
if (!filemanager_is_computer_view(fm) && !filemanager_is_virtual_view(fm) &&
|
||||||
|
!fm->rename_active && !fm->marquee_active && fm->press_idx < 0)
|
||||||
|
filemanager_read_dir(fm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Dropping in ------------------------------------------------------------
|
||||||
|
|
||||||
|
struct DropTarget {
|
||||||
|
char dir[256];
|
||||||
|
bool trash;
|
||||||
|
int entry; // highlighted entry, -1 = the folder itself
|
||||||
|
int sidebar; // highlighted sidebar row, -1 = none
|
||||||
|
};
|
||||||
|
|
||||||
|
static bool resolve_target(gui::Window* win, FileManagerState* fm, int x, int y, DropTarget* t) {
|
||||||
|
t->dir[0] = '\0';
|
||||||
|
t->trash = false;
|
||||||
|
t->entry = -1;
|
||||||
|
t->sidebar = -1;
|
||||||
|
if (x < 0 || y < 0) return false;
|
||||||
|
|
||||||
|
FileManagerLayout l = filemanager_layout(fm, fm->grid_view, win->content_w);
|
||||||
|
DesktopState* ds = fm->desktop;
|
||||||
|
|
||||||
|
if (l.sidebar_w > 0 && x < l.sidebar_w) {
|
||||||
|
int row = filemanager_sidebar_item_at(fm, l, x, y);
|
||||||
|
if (row < 0 || !filemanager_sidebar_target_path(fm, row, t->dir, sizeof(t->dir))) return false;
|
||||||
|
t->sidebar = row;
|
||||||
|
t->trash = fm->sidebar_items[row].kind == FM_SIDEBAR_TRASH;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (y < l.list_y || x < l.view_x || x >= l.view_x + l.view_w) return false;
|
||||||
|
|
||||||
|
int rel_y = y - l.list_y + fm->scrollbar.scroll_offset;
|
||||||
|
int idx = fm->grid_view ? filemanager_grid_index_at(fm, l.grid_cols, x - l.view_x, rel_y)
|
||||||
|
: (rel_y / FM_ITEM_H < fm->entry_count ? rel_y / FM_ITEM_H : -1);
|
||||||
|
|
||||||
|
if (filemanager_is_computer_view(fm)) {
|
||||||
|
if (idx < 0) return false;
|
||||||
|
switch (fm->entry_types[idx]) {
|
||||||
|
case FM_ENTRY_HOME:
|
||||||
|
montauk::strncpy(t->dir, ds->home_dir, sizeof(t->dir));
|
||||||
|
break;
|
||||||
|
case FM_ENTRY_SPECIAL_DIR:
|
||||||
|
filemanager_build_fullpath(t->dir, sizeof(t->dir), ds->home_dir, fm->entry_names[idx]);
|
||||||
|
break;
|
||||||
|
case FM_ENTRY_DRIVE:
|
||||||
|
snprintf(t->dir, sizeof(t->dir), "%d:/", fm->drive_indices[idx]);
|
||||||
|
break;
|
||||||
|
case FM_ENTRY_TRASH:
|
||||||
|
if (!filemanager_trash_files_dir(ds, t->dir, sizeof(t->dir))) return false;
|
||||||
|
t->trash = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
t->entry = idx;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (filemanager_is_virtual_view(fm) || filemanager_in_trash(fm)) return false;
|
||||||
|
|
||||||
|
if (idx >= 0 && fm->is_dir[idx]) {
|
||||||
|
filemanager_build_fullpath(t->dir, sizeof(t->dir), fm->current_path, fm->entry_names[idx]);
|
||||||
|
t->entry = idx;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Background, or a file: the folder being shown.
|
||||||
|
montauk::strncpy(t->dir, fm->current_path, sizeof(t->dir));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool path_within(const char* path, const char* root) {
|
||||||
|
int n = montauk::slen(root);
|
||||||
|
while (n > 0 && root[n - 1] == '/') n--;
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
if (path[i] != root[i]) return false;
|
||||||
|
}
|
||||||
|
return path[n] == '\0' || path[n] == '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool same_volume(const char* a, const char* b) {
|
||||||
|
for (int i = 0; a[i] && b[i]; i++) {
|
||||||
|
if (a[i] != b[i]) return false;
|
||||||
|
if (a[i] == ':') return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool parent_is(const char* path, const char* dir) {
|
||||||
|
char parent[256];
|
||||||
|
montauk::strncpy(parent, path, sizeof(parent));
|
||||||
|
int n = montauk::slen(parent);
|
||||||
|
while (n > 0 && parent[n - 1] != '/') n--;
|
||||||
|
if (n > 0 && !(n >= 2 && parent[n - 2] == ':')) n--; // keep "0:/"
|
||||||
|
parent[n] = '\0';
|
||||||
|
char d[256];
|
||||||
|
montauk::strncpy(d, dir, sizeof(d));
|
||||||
|
int dn = montauk::slen(d);
|
||||||
|
if (dn > 3 && d[dn - 1] == '/') d[dn - 1] = '\0';
|
||||||
|
return montauk::streq(parent, d);
|
||||||
|
}
|
||||||
|
|
||||||
|
// What a drop of fm->drop_list on target would do (0 = refuse).
|
||||||
|
static uint8_t drop_action(FileManagerState* fm, const DropTarget& t, uint8_t offered, uint8_t mods) {
|
||||||
|
if (!fm->drop_list || !t.dir[0]) return 0;
|
||||||
|
|
||||||
|
bool ok = true;
|
||||||
|
bool all_here = true;
|
||||||
|
bool all_same_volume = true;
|
||||||
|
char* list = fm->drop_list;
|
||||||
|
gui::file_list_each(list, [&](const char* src) {
|
||||||
|
if (path_within(t.dir, src)) ok = false; // into itself
|
||||||
|
if (!parent_is(src, t.dir)) all_here = false;
|
||||||
|
if (!same_volume(src, t.dir)) all_same_volume = false;
|
||||||
|
if (t.trash && filemanager_is_trash_path(fm->desktop, src)) ok = false;
|
||||||
|
});
|
||||||
|
if (!ok) return 0;
|
||||||
|
|
||||||
|
if (t.trash) {
|
||||||
|
// Trashing is a move into the Trash, which only exists on the home
|
||||||
|
// volume.
|
||||||
|
if (!(offered & WIN_DRAG_MOVE) || !all_same_volume) return 0;
|
||||||
|
return WIN_DRAG_MOVE;
|
||||||
|
}
|
||||||
|
uint8_t action = gui::drag_pick_action(offered, mods, all_same_volume ? WIN_DRAG_MOVE : WIN_DRAG_COPY);
|
||||||
|
if (action == WIN_DRAG_LINK) return 0; // no links on these filesystems
|
||||||
|
if (action == WIN_DRAG_MOVE && all_here) return 0; // already there
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void clear_drop(FileManagerState* fm) {
|
||||||
|
fm->drop_serial = 0;
|
||||||
|
fm->drop_target_idx = -1;
|
||||||
|
fm->drop_sidebar_idx = -1;
|
||||||
|
fm->drop_action = 0;
|
||||||
|
if (fm->drop_list) montauk::mfree(fm->drop_list);
|
||||||
|
fm->drop_list = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void perform_drop(FileManagerState* fm, const DropTarget& t, uint8_t action) {
|
||||||
|
int n = 0;
|
||||||
|
gui::file_list_each(fm->drop_list, [&](const char*) { n++; });
|
||||||
|
if (n <= 0) return;
|
||||||
|
const char** paths = (const char**)montauk::malloc((uint64_t)n * sizeof(char*));
|
||||||
|
char (*copies)[256] = (char(*)[256])montauk::malloc((uint64_t)n * 256);
|
||||||
|
if (!paths || !copies) {
|
||||||
|
montauk::mfree(paths);
|
||||||
|
montauk::mfree(copies);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int i = 0;
|
||||||
|
gui::file_list_each(fm->drop_list, [&](const char* p) {
|
||||||
|
if (i < n) { montauk::strncpy(copies[i], p, 256); paths[i] = copies[i]; i++; }
|
||||||
|
});
|
||||||
|
|
||||||
|
bool started = t.trash ? filemanager_start_trash_job(fm, paths, i)
|
||||||
|
: filemanager_start_transfer(fm, action == WIN_DRAG_MOVE, paths, i, t.dir);
|
||||||
|
if (!started && fm->active_fileop) {
|
||||||
|
filemanager_show_error(fm->desktop, fm, "Another file operation is still running",
|
||||||
|
nullptr, montauk::Success);
|
||||||
|
}
|
||||||
|
montauk::mfree(paths);
|
||||||
|
montauk::mfree(copies);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace filemanager
|
||||||
|
|
||||||
|
void files_host::on_window_event(gui::Window* win, const montauk::abi::WinEvent& ev) {
|
||||||
|
using namespace filemanager;
|
||||||
|
FileManagerState* fm = fm_of(win);
|
||||||
|
if (!fm) return;
|
||||||
|
const WinDragEvent& d = ev.drag;
|
||||||
|
|
||||||
|
switch (ev.type) {
|
||||||
|
case WIN_EVENT_DRAG_END:
|
||||||
|
// We were the source.
|
||||||
|
if (d.serial == fm->drag_serial) {
|
||||||
|
fm->drag_serial = 0;
|
||||||
|
fm->press_idx = -1;
|
||||||
|
// A move into another window runs in that process; look again
|
||||||
|
// as it progresses.
|
||||||
|
if (d.actions == WIN_DRAG_MOVE) {
|
||||||
|
uint64_t now = montauk::get_milliseconds();
|
||||||
|
fm->refresh_due[0] = now + 300;
|
||||||
|
fm->refresh_due[1] = now + 1500;
|
||||||
|
fm->refresh_due[2] = now + 5000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WIN_EVENT_DRAG_ENTER:
|
||||||
|
case WIN_EVENT_DRAG_OVER: {
|
||||||
|
if (d.serial != fm->drop_serial) {
|
||||||
|
clear_drop(fm);
|
||||||
|
fm->drop_serial = d.serial;
|
||||||
|
fm->drop_list = gui::drag_get_alloc(d.serial, TRANSFER_MIME_FILE_LIST);
|
||||||
|
}
|
||||||
|
DropTarget t;
|
||||||
|
uint8_t action = resolve_target(win, fm, d.x, d.y, &t)
|
||||||
|
? drop_action(fm, t, d.actions ? d.actions : (WIN_DRAG_COPY | WIN_DRAG_MOVE), d.modifiers)
|
||||||
|
: 0;
|
||||||
|
int entry = action ? t.entry : -1;
|
||||||
|
int sidebar = action ? t.sidebar : -1;
|
||||||
|
if (action != fm->drop_action || entry != fm->drop_target_idx || sidebar != fm->drop_sidebar_idx ||
|
||||||
|
ev.type == WIN_EVENT_DRAG_ENTER) {
|
||||||
|
fm->drop_action = action;
|
||||||
|
fm->drop_target_idx = entry;
|
||||||
|
fm->drop_sidebar_idx = sidebar;
|
||||||
|
gui::win_request_drag_status(win->ext_win_id, d.serial, action);
|
||||||
|
win->dirty = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case WIN_EVENT_DRAG_LEAVE:
|
||||||
|
if (d.serial == fm->drop_serial) {
|
||||||
|
clear_drop(fm);
|
||||||
|
win->dirty = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WIN_EVENT_DROP: {
|
||||||
|
if (d.serial != fm->drop_serial) {
|
||||||
|
clear_drop(fm);
|
||||||
|
fm->drop_serial = d.serial;
|
||||||
|
fm->drop_list = gui::drag_get_alloc(d.serial, TRANSFER_MIME_FILE_LIST);
|
||||||
|
}
|
||||||
|
DropTarget t;
|
||||||
|
if (fm->drop_list && resolve_target(win, fm, d.x, d.y, &t)) {
|
||||||
|
// The desktop picked the action from our last status answer.
|
||||||
|
uint8_t action = d.actions ? d.actions : drop_action(fm, t, WIN_DRAG_COPY | WIN_DRAG_MOVE, d.modifiers);
|
||||||
|
if (action) perform_drop(fm, t, action);
|
||||||
|
}
|
||||||
|
clear_drop(fm);
|
||||||
|
win->dirty = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
+195
-146
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* events.cpp
|
* events.cpp
|
||||||
* Mouse and keyboard handling for the embedded desktop file manager
|
* Mouse and keyboard handling for the Files file manager
|
||||||
* Copyright (c) 2026 Daniel Hammer
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -62,6 +62,22 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
|
|||||||
int local_y = ev.y - cr.y;
|
int local_y = ev.y - cr.y;
|
||||||
int cw = cr.w;
|
int cw = cr.w;
|
||||||
|
|
||||||
|
// ---- Text fields: the rename box and the editable path bar ----
|
||||||
|
if (fm->rename_active || fm->pathbar_editing) {
|
||||||
|
bool shift = fm->desktop && fm->desktop->kbd_shift_down;
|
||||||
|
Rect box;
|
||||||
|
if (fm->rename_active &&
|
||||||
|
filemanager_rename_box(fm, filemanager_layout(fm, fm->grid_view, cw),
|
||||||
|
win->content_h, &box) &&
|
||||||
|
filemanager_field_mouse(fm->rename, box, local_x, local_y, ev, cw,
|
||||||
|
win->content_h, shift))
|
||||||
|
return;
|
||||||
|
if (fm->pathbar_editing &&
|
||||||
|
filemanager_field_mouse(fm->pathbar, filemanager_pathbar_box(cw), local_x, local_y,
|
||||||
|
ev, cw, win->content_h, shift))
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Scrollbar interaction
|
// Scrollbar interaction
|
||||||
MouseEvent local_ev = ev;
|
MouseEvent local_ev = ev;
|
||||||
local_ev.x = local_x;
|
local_ev.x = local_x;
|
||||||
@@ -73,6 +89,54 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
|
|||||||
mtk::ContextMenuState state = filemanager_context_state(fm);
|
mtk::ContextMenuState state = filemanager_context_state(fm);
|
||||||
mtk::ContextMenuItem items[FM_CTX_MAX_ITEMS];
|
mtk::ContextMenuItem items[FM_CTX_MAX_ITEMS];
|
||||||
filemanager_build_context_items(fm, items, FM_CTX_MAX_ITEMS);
|
filemanager_build_context_items(fm, items, FM_CTX_MAX_ITEMS);
|
||||||
|
|
||||||
|
// "New" submenu: opens while the pointer is on the "New" row and
|
||||||
|
// stays open while it travels across to the submenu.
|
||||||
|
mtk::ContextMenuState sub;
|
||||||
|
int sub_w = 0;
|
||||||
|
if (filemanager_new_submenu_layout(fm, cw, win->content_h, &sub, &sub_w)) {
|
||||||
|
mtk::ContextMenuItem sub_items[FM_CTX_MAX_ITEMS];
|
||||||
|
int sub_count = filemanager_new_submenu_items(fm, sub_items);
|
||||||
|
int menu_w = filemanager_context_menu_width(items, fm->ctx_item_count);
|
||||||
|
int new_row = -1;
|
||||||
|
for (int i = 0; i < fm->ctx_item_count; i++) {
|
||||||
|
if (fm->ctx_items[i] == CTX_NEW) new_row = i;
|
||||||
|
}
|
||||||
|
Rect sub_rect = mtk::context_menu_rect(sub, sub_count, cw, win->content_h, sub_w);
|
||||||
|
int parent_hit = mtk::context_menu_hit_item(state, fm->ctx_item_count, local_x, local_y,
|
||||||
|
cw, win->content_h, menu_w);
|
||||||
|
bool pressed = ev.left_pressed() || ev.right_pressed();
|
||||||
|
|
||||||
|
if (fm->ctx_sub_open && sub_rect.contains(local_x, local_y)) {
|
||||||
|
int sub_action = mtk::context_menu_handle_mouse(sub, sub_items, sub_count,
|
||||||
|
local_x, local_y, ev.buttons,
|
||||||
|
ev.prev_buttons, cw,
|
||||||
|
win->content_h, sub_w);
|
||||||
|
fm->ctx_sub_hover = sub.hover;
|
||||||
|
fm->ctx_hover = new_row;
|
||||||
|
if (sub_action >= 0 || sub_action == mtk::CONTEXT_MENU_DISMISSED)
|
||||||
|
filemanager_close_ctx_menu(fm);
|
||||||
|
if (sub_action == CTX_NEW_FOLDER) filemanager_new_folder(fm);
|
||||||
|
else if (sub_action >= CTX_NEW_FILE_BASE) filemanager_new_file(fm, sub_action);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (parent_hit == new_row) {
|
||||||
|
// Clicking "New" itself only (re)opens the submenu.
|
||||||
|
fm->ctx_sub_open = true;
|
||||||
|
fm->ctx_sub_hover = -1;
|
||||||
|
fm->ctx_hover = new_row;
|
||||||
|
if (ev.right_pressed()) filemanager_close_ctx_menu(fm);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (parent_hit >= 0) {
|
||||||
|
fm->ctx_sub_open = false;
|
||||||
|
} else if (fm->ctx_sub_open && !pressed) {
|
||||||
|
fm->ctx_hover = new_row;
|
||||||
|
fm->ctx_sub_hover = -1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int action = mtk::context_menu_handle_mouse(state, items,
|
int action = mtk::context_menu_handle_mouse(state, items,
|
||||||
fm->ctx_item_count,
|
fm->ctx_item_count,
|
||||||
local_x, local_y,
|
local_x, local_y,
|
||||||
@@ -85,12 +149,32 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
|
|||||||
if (action >= 0) {
|
if (action >= 0) {
|
||||||
int target = fm->ctx_target_idx;
|
int target = fm->ctx_target_idx;
|
||||||
|
|
||||||
|
// Sidebar rows (target -2 - row) have their own few actions.
|
||||||
|
if (target <= -2) {
|
||||||
|
int row = -2 - target;
|
||||||
|
if (row < fm->sidebar_count) {
|
||||||
|
if (action == CTX_OPEN) filemanager_sidebar_open(fm, row);
|
||||||
|
else if (action == CTX_EMPTY_TRASH) filemanager_show_empty_trash_confirmation(fm);
|
||||||
|
else if (action == CTX_REMOVE_BOOKMARK) {
|
||||||
|
char path[256];
|
||||||
|
montauk::strncpy(path, fm->sidebar_items[row].path, sizeof(path));
|
||||||
|
prefs_remove_bookmark(fm->desktop, path);
|
||||||
|
filemanager_sidebar_rebuild(fm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Select target if needed
|
// Select target if needed
|
||||||
if (target >= 0 && target < fm->entry_count)
|
if (target >= 0 && target < fm->entry_count)
|
||||||
fm->selected = target;
|
fm->selected = target;
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case CTX_OPEN: filemanager_open_entry(fm, target); break;
|
case CTX_OPEN: filemanager_open_entry(fm, target); break;
|
||||||
|
case CTX_TOGGLE_HIDDEN: filemanager_toggle_hidden(fm); break;
|
||||||
|
case CTX_ADD_BOOKMARK: filemanager_add_bookmark(fm, target); break;
|
||||||
|
case CTX_UNDO: filemanager_undo(fm); break;
|
||||||
|
case CTX_REDO: filemanager_redo(fm); break;
|
||||||
case CTX_COPY: filemanager_do_copy(fm); break;
|
case CTX_COPY: filemanager_do_copy(fm); break;
|
||||||
case CTX_CUT: filemanager_do_cut(fm); break;
|
case CTX_CUT: filemanager_do_cut(fm); break;
|
||||||
case CTX_PASTE: filemanager_do_paste(fm); break;
|
case CTX_PASTE: filemanager_do_paste(fm); break;
|
||||||
@@ -115,15 +199,42 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FileManagerLayout l = filemanager_layout(fm, fm->grid_view, cw);
|
||||||
|
|
||||||
|
// ---- Sidebar edge: drag to resize ----
|
||||||
|
if (!fm->marquee_active && fm->press_idx < 0 && !fm->scrollbar.dragging &&
|
||||||
|
filemanager_sidebar_resize_mouse(win, fm, l, local_x, local_y, ev))
|
||||||
|
return;
|
||||||
|
|
||||||
// ---- Cancel rename on click outside rename area ----
|
// ---- Cancel rename on click outside rename area ----
|
||||||
if (fm->rename_active && ev.left_pressed()) {
|
if (fm->rename_active && ev.left_pressed()) {
|
||||||
filemanager_finish_rename(fm);
|
filemanager_finish_rename(fm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Sidebar ----
|
||||||
|
if (l.sidebar_w > 0 && !fm->marquee_active && fm->press_idx < 0 &&
|
||||||
|
filemanager_sidebar_mouse(fm, l, local_x, local_y, ev, win->content_h)) {
|
||||||
|
if (ev.left_pressed() && fm->pathbar_editing) filemanager_cancel_pathbar(fm);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Dragging entries out (dnd.cpp) ----
|
||||||
|
if (filemanager_dnd_mouse(win, fm, l, local_x, local_y, ev)) return;
|
||||||
|
|
||||||
|
// Entry under a point of the file view, -1 for background.
|
||||||
|
auto entry_at = [&](int x, int y) -> int {
|
||||||
|
if (y < l.list_y || x < l.view_x || x >= l.view_x + l.view_w) return -1;
|
||||||
|
int rel_y = y - l.list_y + fm->scrollbar.scroll_offset;
|
||||||
|
if (fm->grid_view) return filemanager_grid_index_at(fm, l.grid_cols, x - l.view_x, rel_y);
|
||||||
|
int idx = rel_y / FM_ITEM_H;
|
||||||
|
return idx < fm->entry_count ? idx : -1;
|
||||||
|
};
|
||||||
|
bool in_view = local_y >= l.list_y && local_x >= l.view_x && local_x < l.view_x + l.view_w;
|
||||||
|
|
||||||
if (ev.left_pressed()) {
|
if (ev.left_pressed()) {
|
||||||
// Click on path bar area
|
// Path bar: a segment navigates, anywhere else starts editing.
|
||||||
if (local_y >= FM_TOOLBAR_H && local_y < FM_TOOLBAR_H + FM_PATHBAR_H) {
|
if (local_y >= FM_TOOLBAR_H && local_y < FM_TOOLBAR_H + FM_PATHBAR_H) {
|
||||||
if (!fm->pathbar_editing)
|
if (!fm->pathbar_editing && !filemanager_breadcrumb_click(fm, local_x, cw))
|
||||||
filemanager_start_pathbar(fm);
|
filemanager_start_pathbar(fm);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -146,6 +257,9 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
|
|||||||
fm->scrollbar.scroll_offset = 0;
|
fm->scrollbar.scroll_offset = 0;
|
||||||
fm->marquee_active = false;
|
fm->marquee_active = false;
|
||||||
filemanager_clear_multi_selection(fm);
|
filemanager_clear_multi_selection(fm);
|
||||||
|
if (filemanager_is_listing(fm))
|
||||||
|
prefs_set_folder_view(fm->desktop, fm->current_path, fm->grid_view,
|
||||||
|
fm->sort_key, fm->sort_desc);
|
||||||
}
|
}
|
||||||
// Action buttons
|
// Action buttons
|
||||||
else if (local_x >= 188 && local_x < 212) filemanager_do_copy(fm);
|
else if (local_x >= 188 && local_x < 212) filemanager_do_copy(fm);
|
||||||
@@ -158,72 +272,50 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// File clicks (grid vs list)
|
// Column header click (list view): sort by that column, again = reverse.
|
||||||
if (fm->grid_view) {
|
if (!fm->grid_view && local_y >= l.body_y && local_y < l.list_y &&
|
||||||
int list_y = FM_TOOLBAR_H + FM_PATHBAR_H;
|
local_x >= l.view_x && filemanager_is_listing(fm)) {
|
||||||
if (local_y >= list_y && local_x < cw - FM_SCROLLBAR_W) {
|
FileManagerListColumns cols = filemanager_list_columns(l.view_x, l.view_w);
|
||||||
int cols = (cw - FM_SCROLLBAR_W) / FM_GRID_CELL_W;
|
if (cols.type_x >= 0 && local_x >= cols.type_x)
|
||||||
if (cols < 1) cols = 1;
|
filemanager_set_sort(fm, FM_SORT_TYPE);
|
||||||
int rel_y = local_y - list_y + fm->scrollbar.scroll_offset;
|
else if (cols.size_x >= 0 && local_x >= cols.size_x)
|
||||||
int clicked_idx = filemanager_grid_index_at(fm, cols, local_x, rel_y);
|
filemanager_set_sort(fm, FM_SORT_SIZE);
|
||||||
|
else if (cols.modified_x >= 0 && local_x >= cols.modified_x - 4)
|
||||||
|
filemanager_set_sort(fm, FM_SORT_MODIFIED);
|
||||||
|
else
|
||||||
|
filemanager_set_sort(fm, FM_SORT_NAME);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (clicked_idx >= 0 && clicked_idx < fm->entry_count) {
|
if (in_view) {
|
||||||
filemanager_click_entry(fm, clicked_idx);
|
int clicked_idx = entry_at(local_x, local_y);
|
||||||
} else {
|
if (clicked_idx >= 0) {
|
||||||
// Background click in grid area: start marquee drag
|
filemanager_click_entry(fm, clicked_idx);
|
||||||
filemanager_select_single(fm, -1);
|
// A press on a selected entry may turn into a drag.
|
||||||
fm->marquee_active = true;
|
if (filemanager_is_entry_selected(fm, clicked_idx)) {
|
||||||
fm->marquee_start_x = local_x;
|
fm->press_idx = clicked_idx;
|
||||||
fm->marquee_start_y = rel_y;
|
fm->press_x = local_x;
|
||||||
fm->marquee_cur_x = local_x;
|
fm->press_y = local_y;
|
||||||
fm->marquee_cur_y = rel_y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// List view clicks
|
|
||||||
int header_y = FM_TOOLBAR_H + FM_PATHBAR_H;
|
|
||||||
int list_y = header_y + FM_HEADER_H;
|
|
||||||
if (local_y >= header_y && local_y < list_y && filemanager_is_listing(fm)) {
|
|
||||||
// Column header click: sort by that column (again = reverse).
|
|
||||||
FileManagerListColumns cols = filemanager_list_columns(cw);
|
|
||||||
if (cols.type_x >= 0 && local_x >= cols.type_x)
|
|
||||||
filemanager_set_sort(fm, FM_SORT_TYPE);
|
|
||||||
else if (cols.size_x >= 0 && local_x >= cols.size_x)
|
|
||||||
filemanager_set_sort(fm, FM_SORT_SIZE);
|
|
||||||
else if (cols.modified_x >= 0 && local_x >= cols.modified_x - 4)
|
|
||||||
filemanager_set_sort(fm, FM_SORT_MODIFIED);
|
|
||||||
else
|
|
||||||
filemanager_set_sort(fm, FM_SORT_NAME);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (local_y >= list_y && local_x < cw - FM_SCROLLBAR_W) {
|
|
||||||
int rel_y = local_y - list_y + fm->scrollbar.scroll_offset;
|
|
||||||
int clicked_idx = rel_y / FM_ITEM_H;
|
|
||||||
|
|
||||||
if (clicked_idx >= 0 && clicked_idx < fm->entry_count) {
|
|
||||||
filemanager_click_entry(fm, clicked_idx);
|
|
||||||
} else {
|
|
||||||
// Background click in list area: start marquee drag
|
|
||||||
filemanager_select_single(fm, -1);
|
|
||||||
fm->marquee_active = true;
|
|
||||||
fm->marquee_start_x = local_x;
|
|
||||||
fm->marquee_start_y = rel_y;
|
|
||||||
fm->marquee_cur_x = local_x;
|
|
||||||
fm->marquee_cur_y = rel_y;
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Background click: start marquee drag
|
||||||
|
int rel_y = local_y - l.list_y + fm->scrollbar.scroll_offset;
|
||||||
|
filemanager_select_single(fm, -1);
|
||||||
|
fm->marquee_active = true;
|
||||||
|
fm->marquee_start_x = local_x;
|
||||||
|
fm->marquee_start_y = rel_y;
|
||||||
|
fm->marquee_cur_x = local_x;
|
||||||
|
fm->marquee_cur_y = rel_y;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Marquee drag (grid or list view) ----
|
// ---- Marquee drag (grid or list view) ----
|
||||||
if (fm->marquee_active) {
|
if (fm->marquee_active) {
|
||||||
int list_y = fm->grid_view
|
int rel_y = local_y - l.list_y + fm->scrollbar.scroll_offset;
|
||||||
? FM_TOOLBAR_H + FM_PATHBAR_H
|
|
||||||
: FM_TOOLBAR_H + FM_PATHBAR_H + FM_HEADER_H;
|
|
||||||
int rel_y = local_y - list_y + fm->scrollbar.scroll_offset;
|
|
||||||
int clamp_x = local_x;
|
int clamp_x = local_x;
|
||||||
if (clamp_x < 0) clamp_x = 0;
|
if (clamp_x < l.view_x) clamp_x = l.view_x;
|
||||||
int max_x = cw - FM_SCROLLBAR_W - 1;
|
int max_x = l.view_x + l.view_w - 1;
|
||||||
if (clamp_x > max_x) clamp_x = max_x;
|
if (clamp_x > max_x) clamp_x = max_x;
|
||||||
if (rel_y < 0) rel_y = 0;
|
if (rel_y < 0) rel_y = 0;
|
||||||
|
|
||||||
@@ -241,12 +333,11 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
|
|||||||
filemanager_clear_multi_selection(fm);
|
filemanager_clear_multi_selection(fm);
|
||||||
int limit = fm->entry_count;
|
int limit = fm->entry_count;
|
||||||
if (fm->grid_view) {
|
if (fm->grid_view) {
|
||||||
int cols = (cw - FM_SCROLLBAR_W) / FM_GRID_CELL_W;
|
int cols = l.grid_cols;
|
||||||
if (cols < 1) cols = 1;
|
|
||||||
for (int i = 0; i < limit; i++) {
|
for (int i = 0; i < limit; i++) {
|
||||||
int col = i % cols;
|
int col = i % cols;
|
||||||
int row = i / cols;
|
int row = i / cols;
|
||||||
int cell_x = col * FM_GRID_CELL_W;
|
int cell_x = l.view_x + col * FM_GRID_CELL_W;
|
||||||
int cell_y = filemanager_grid_row_y(fm, row, cols);
|
int cell_y = filemanager_grid_row_y(fm, row, cols);
|
||||||
int cell_h = filemanager_grid_row_height(fm, row, cols);
|
int cell_h = filemanager_grid_row_height(fm, row, cols);
|
||||||
int cx1 = cell_x + FM_GRID_CELL_W;
|
int cx1 = cell_x + FM_GRID_CELL_W;
|
||||||
@@ -288,42 +379,17 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
|
|||||||
if (ev.right_pressed()) {
|
if (ev.right_pressed()) {
|
||||||
filemanager_cancel_rename(fm);
|
filemanager_cancel_rename(fm);
|
||||||
|
|
||||||
// Determine what was right-clicked
|
|
||||||
int target_idx = -1;
|
int target_idx = -1;
|
||||||
|
if (in_view) {
|
||||||
if (local_y >= FM_TOOLBAR_H + FM_PATHBAR_H) {
|
int idx = entry_at(local_x, local_y);
|
||||||
if (fm->grid_view) {
|
if (idx >= 0) {
|
||||||
int list_y = FM_TOOLBAR_H + FM_PATHBAR_H;
|
target_idx = idx;
|
||||||
if (local_x < cw - FM_SCROLLBAR_W) {
|
// Preserve multi-selection if right-clicking inside it,
|
||||||
int cols = (cw - FM_SCROLLBAR_W) / FM_GRID_CELL_W;
|
// otherwise replace it with a single selection.
|
||||||
if (cols < 1) cols = 1;
|
if (!filemanager_is_entry_selected(fm, idx))
|
||||||
int rel_y = local_y - list_y + fm->scrollbar.scroll_offset;
|
filemanager_select_single(fm, idx);
|
||||||
int idx = filemanager_grid_index_at(fm, cols, local_x, rel_y);
|
|
||||||
if (idx >= 0 && idx < fm->entry_count) {
|
|
||||||
target_idx = idx;
|
|
||||||
// Preserve multi-selection if right-clicking inside it,
|
|
||||||
// otherwise replace it with a single selection.
|
|
||||||
if (!filemanager_is_entry_selected(fm, idx))
|
|
||||||
filemanager_select_single(fm, idx);
|
|
||||||
} else {
|
|
||||||
filemanager_select_single(fm, -1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
int list_y = FM_TOOLBAR_H + FM_PATHBAR_H + FM_HEADER_H;
|
filemanager_select_single(fm, -1);
|
||||||
if (local_y >= list_y && local_x < cw - FM_SCROLLBAR_W) {
|
|
||||||
int rel_y = local_y - list_y + fm->scrollbar.scroll_offset;
|
|
||||||
int idx = rel_y / FM_ITEM_H;
|
|
||||||
if (idx >= 0 && idx < fm->entry_count) {
|
|
||||||
target_idx = idx;
|
|
||||||
// Preserve multi-selection if right-clicking inside it,
|
|
||||||
// otherwise replace it with a single selection.
|
|
||||||
if (!filemanager_is_entry_selected(fm, idx))
|
|
||||||
filemanager_select_single(fm, idx);
|
|
||||||
} else {
|
|
||||||
filemanager_select_single(fm, -1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,11 +398,8 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
|
|||||||
|
|
||||||
// Scroll handling
|
// Scroll handling
|
||||||
if (ev.scroll != 0) {
|
if (ev.scroll != 0) {
|
||||||
int list_y_start = fm->grid_view
|
|
||||||
? FM_TOOLBAR_H + FM_PATHBAR_H
|
|
||||||
: FM_TOOLBAR_H + FM_PATHBAR_H + FM_HEADER_H;
|
|
||||||
int scroll_step = fm->grid_view ? FM_GRID_CELL_H : FM_ITEM_H;
|
int scroll_step = fm->grid_view ? FM_GRID_CELL_H : FM_ITEM_H;
|
||||||
if (local_y >= list_y_start) {
|
if (local_y >= l.list_y && local_x >= l.view_x) {
|
||||||
fm->scrollbar.scroll_offset -= ev.scroll * scroll_step;
|
fm->scrollbar.scroll_offset -= ev.scroll * scroll_step;
|
||||||
int ms = fm->scrollbar.max_scroll();
|
int ms = fm->scrollbar.max_scroll();
|
||||||
if (fm->scrollbar.scroll_offset < 0) fm->scrollbar.scroll_offset = 0;
|
if (fm->scrollbar.scroll_offset < 0) fm->scrollbar.scroll_offset = 0;
|
||||||
@@ -346,8 +409,8 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static int filemanager_grid_cols(Window* win) {
|
static int filemanager_grid_cols(Window* win) {
|
||||||
int cols = (win->content_rect().w - FM_SCROLLBAR_W) / FM_GRID_CELL_W;
|
FileManagerState* fm = (FileManagerState*)win->app_data;
|
||||||
return cols < 1 ? 1 : cols;
|
return filemanager_layout(fm, true, win->content_rect().w).grid_cols;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep the focused entry inside the visible area so keyboard navigation
|
// Keep the focused entry inside the visible area so keyboard navigation
|
||||||
@@ -355,8 +418,7 @@ static int filemanager_grid_cols(Window* win) {
|
|||||||
static void filemanager_scroll_to_selected(Window* win, FileManagerState* fm) {
|
static void filemanager_scroll_to_selected(Window* win, FileManagerState* fm) {
|
||||||
if (fm->selected < 0 || fm->selected >= fm->entry_count) return;
|
if (fm->selected < 0 || fm->selected >= fm->entry_count) return;
|
||||||
|
|
||||||
int list_y = fm->grid_view ? FM_TOOLBAR_H + FM_PATHBAR_H
|
int list_y = filemanager_layout(fm, fm->grid_view, win->content_w).list_y;
|
||||||
: FM_TOOLBAR_H + FM_PATHBAR_H + FM_HEADER_H;
|
|
||||||
int list_h = win->content_h - list_y;
|
int list_h = win->content_h - list_y;
|
||||||
if (list_h <= 0) return;
|
if (list_h <= 0) return;
|
||||||
|
|
||||||
@@ -448,60 +510,30 @@ void filemanager_on_key(Window* win, const montauk::abi::KeyEvent& key) {
|
|||||||
|
|
||||||
// ---- Path bar editing key handling ----
|
// ---- Path bar editing key handling ----
|
||||||
if (fm->pathbar_editing) {
|
if (fm->pathbar_editing) {
|
||||||
if (key.ascii == '\n' || key.ascii == '\r') {
|
if (fm->pathbar.in.context.open) {
|
||||||
|
filemanager_field_key(fm->pathbar, key); // closes its menu
|
||||||
|
} else if (key.ascii == '\t' || key.scancode == 0x0F) {
|
||||||
|
filemanager_pathbar_complete(fm);
|
||||||
|
} else if (key.ascii == '\n' || key.ascii == '\r') {
|
||||||
filemanager_commit_pathbar(fm);
|
filemanager_commit_pathbar(fm);
|
||||||
} else if (key.scancode == 0x01) {
|
} else if (key.scancode == 0x01) {
|
||||||
filemanager_cancel_pathbar(fm);
|
filemanager_cancel_pathbar(fm);
|
||||||
} else if (key.ascii == '\b' || key.scancode == 0x0E) {
|
|
||||||
montauk::utf8::erase_before(fm->pathbar_buf, &fm->pathbar_len, &fm->pathbar_cursor);
|
|
||||||
} else if (montauk::nav_scancode(key) == 0x53) {
|
|
||||||
montauk::utf8::erase_at(fm->pathbar_buf, &fm->pathbar_len, fm->pathbar_cursor);
|
|
||||||
} else if (montauk::nav_scancode(key) == 0x4B) {
|
|
||||||
fm->pathbar_cursor = montauk::utf8::prev_boundary(fm->pathbar_buf, fm->pathbar_cursor);
|
|
||||||
} else if (montauk::nav_scancode(key) == 0x4D) {
|
|
||||||
fm->pathbar_cursor = montauk::utf8::next_boundary(fm->pathbar_buf, fm->pathbar_len,
|
|
||||||
fm->pathbar_cursor);
|
|
||||||
} else if (montauk::nav_scancode(key) == 0x47) {
|
|
||||||
fm->pathbar_cursor = 0;
|
|
||||||
} else if (montauk::nav_scancode(key) == 0x4F) {
|
|
||||||
fm->pathbar_cursor = fm->pathbar_len;
|
|
||||||
} else {
|
} else {
|
||||||
montauk::utf8::insert_key(fm->pathbar_buf, &fm->pathbar_len, 255,
|
filemanager_field_key(fm->pathbar, key);
|
||||||
&fm->pathbar_cursor, key);
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Rename mode key handling ----
|
// ---- Rename mode key handling ----
|
||||||
if (fm->rename_active) {
|
if (fm->rename_active) {
|
||||||
if (key.ascii == '\n' || key.ascii == '\r') {
|
if (fm->rename.in.context.open) {
|
||||||
|
filemanager_field_key(fm->rename, key); // closes its menu
|
||||||
|
} else if (key.ascii == '\n' || key.ascii == '\r') {
|
||||||
filemanager_finish_rename(fm);
|
filemanager_finish_rename(fm);
|
||||||
} else if (key.scancode == 0x01) {
|
} else if (key.scancode == 0x01) {
|
||||||
// Escape
|
|
||||||
filemanager_cancel_rename(fm);
|
filemanager_cancel_rename(fm);
|
||||||
} else if (key.ascii == '\b' || key.scancode == 0x0E) {
|
} else {
|
||||||
// Backspace
|
filemanager_field_key(fm->rename, key);
|
||||||
montauk::utf8::erase_before(fm->rename_buf, &fm->rename_len, &fm->rename_cursor);
|
|
||||||
} else if (montauk::nav_scancode(key) == 0x53) {
|
|
||||||
// Delete key
|
|
||||||
montauk::utf8::erase_at(fm->rename_buf, &fm->rename_len, fm->rename_cursor);
|
|
||||||
} else if (montauk::nav_scancode(key) == 0x4B) {
|
|
||||||
// Left arrow
|
|
||||||
fm->rename_cursor = montauk::utf8::prev_boundary(fm->rename_buf, fm->rename_cursor);
|
|
||||||
} else if (montauk::nav_scancode(key) == 0x4D) {
|
|
||||||
// Right arrow
|
|
||||||
fm->rename_cursor = montauk::utf8::next_boundary(fm->rename_buf, fm->rename_len,
|
|
||||||
fm->rename_cursor);
|
|
||||||
} else if (montauk::nav_scancode(key) == 0x47) {
|
|
||||||
// Home
|
|
||||||
fm->rename_cursor = 0;
|
|
||||||
} else if (montauk::nav_scancode(key) == 0x4F) {
|
|
||||||
// End
|
|
||||||
fm->rename_cursor = fm->rename_len;
|
|
||||||
} else if (key.ascii != '/' && key.ascii != '\\') {
|
|
||||||
// Printable character (reject / and other FS-unsafe chars)
|
|
||||||
montauk::utf8::insert_key(fm->rename_buf, &fm->rename_len, 63,
|
|
||||||
&fm->rename_cursor, key);
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -524,6 +556,12 @@ void filemanager_on_key(Window* win, const montauk::abi::KeyEvent& key) {
|
|||||||
if (filemanager_is_listing(fm)) filemanager_select_all(fm);
|
if (filemanager_is_listing(fm)) filemanager_select_all(fm);
|
||||||
return;
|
return;
|
||||||
case 'l': filemanager_start_pathbar(fm); return;
|
case 'l': filemanager_start_pathbar(fm); return;
|
||||||
|
case 'h': filemanager_toggle_hidden(fm); return;
|
||||||
|
case 'z':
|
||||||
|
if (key.shift) filemanager_redo(fm);
|
||||||
|
else filemanager_undo(fm);
|
||||||
|
return;
|
||||||
|
case 'y': filemanager_redo(fm); return;
|
||||||
case 'n':
|
case 'n':
|
||||||
if (key.shift) filemanager_new_folder(fm);
|
if (key.shift) filemanager_new_folder(fm);
|
||||||
return;
|
return;
|
||||||
@@ -591,9 +629,20 @@ void filemanager_on_key(Window* win, const montauk::abi::KeyEvent& key) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void filemanager_on_poll(Window* win) {
|
||||||
|
FileManagerState* fm = (FileManagerState*)win->app_data;
|
||||||
|
if (!fm) return;
|
||||||
|
if (filemanager_thumbs_poll(fm)) win->dirty = true;
|
||||||
|
filemanager_dnd_poll(fm);
|
||||||
|
}
|
||||||
|
|
||||||
void filemanager_on_close(Window* win) {
|
void filemanager_on_close(Window* win) {
|
||||||
if (win->app_data) {
|
if (win->app_data) {
|
||||||
FileManagerState* fm = (FileManagerState*)win->app_data;
|
FileManagerState* fm = (FileManagerState*)win->app_data;
|
||||||
|
filemanager_thumbs_free(fm);
|
||||||
|
filemanager_undo_free(fm);
|
||||||
|
if (fm->sidebar_items) montauk::mfree(fm->sidebar_items);
|
||||||
|
if (fm->drop_list) montauk::mfree(fm->drop_list);
|
||||||
filemanager_free_arrays(fm);
|
filemanager_free_arrays(fm);
|
||||||
montauk::mfree(win->app_data);
|
montauk::mfree(win->app_data);
|
||||||
win->app_data = nullptr;
|
win->app_data = nullptr;
|
||||||
+306
-27
@@ -1,12 +1,15 @@
|
|||||||
/*
|
/*
|
||||||
* filemanager_internal.hpp
|
* filemanager_internal.hpp
|
||||||
* Shared declarations for the embedded desktop file manager
|
* Shared declarations for the Files file manager
|
||||||
* Copyright (c) 2026 Daniel Hammer
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "../apps_common.hpp"
|
// Files grew up inside the desktop and still speaks its API; host.cpp
|
||||||
|
// provides that API in this process (see files_host.hpp).
|
||||||
|
#include "../desktop/apps/apps_common.hpp"
|
||||||
|
#include "files_host.hpp"
|
||||||
#include <gui/mtk/widgets.hpp>
|
#include <gui/mtk/widgets.hpp>
|
||||||
#include <montauk/statuscodes.h>
|
#include <montauk/statuscodes.h>
|
||||||
|
|
||||||
@@ -64,6 +67,37 @@ enum FileManagerSortKey : uint8_t {
|
|||||||
FM_SORT_MODIFIED,
|
FM_SORT_MODIFIED,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// One row of the sidebar (sidebar.cpp).
|
||||||
|
enum FileManagerSidebarKind : uint8_t {
|
||||||
|
FM_SIDEBAR_HEADER = 0, // section title, not clickable
|
||||||
|
FM_SIDEBAR_COMPUTER,
|
||||||
|
FM_SIDEBAR_FOLDER, // home, a standard folder or a bookmark
|
||||||
|
FM_SIDEBAR_TRASH,
|
||||||
|
FM_SIDEBAR_DRIVE,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FileManagerSidebarItem {
|
||||||
|
FileManagerSidebarKind kind;
|
||||||
|
bool bookmark; // removable (user bookmark)
|
||||||
|
int special_index; // standard folder index, -1 otherwise
|
||||||
|
int drive_kind; // FM_SIDEBAR_DRIVE: kernel BlockDeviceKind
|
||||||
|
char label[64];
|
||||||
|
char path[256]; // empty for Computer / Trash / headers
|
||||||
|
};
|
||||||
|
|
||||||
|
// A single-line editable field (textfield.cpp): the rename box and the
|
||||||
|
// path bar. Editing state is mtk's; scroll keeps the caret in view.
|
||||||
|
struct FileManagerTextField {
|
||||||
|
char buf[256];
|
||||||
|
int cap; // bytes available in buf, NUL included
|
||||||
|
int pad; // text inset from the box's left edge
|
||||||
|
int scroll; // px the text is shifted left
|
||||||
|
mtk::TextInputState in;
|
||||||
|
mtk::TextInputCharFilter filter;
|
||||||
|
uint64_t last_click; // double/triple-click detection
|
||||||
|
int clicks;
|
||||||
|
};
|
||||||
|
|
||||||
struct FileManagerState {
|
struct FileManagerState {
|
||||||
char current_path[256];
|
char current_path[256];
|
||||||
FileManagerLocation history[16];
|
FileManagerLocation history[16];
|
||||||
@@ -91,11 +125,18 @@ struct FileManagerState {
|
|||||||
int* drive_indices; // drive number or special-folder index for Computer view entries
|
int* drive_indices; // drive number or special-folder index for Computer view entries
|
||||||
int* drive_kinds; // kernel BlockDeviceKind for FM_ENTRY_DRIVE entries (4 = USB MSC)
|
int* drive_kinds; // kernel BlockDeviceKind for FM_ENTRY_DRIVE entries (4 = USB MSC)
|
||||||
|
|
||||||
// Clipboard (supports multiple paths from a selection), grown to clipboard_cap.
|
// The system clipboard's file list, loaded by filemanager_sync_clipboard()
|
||||||
|
// just before a paste (every Files window is its own process, so the
|
||||||
|
// clipboard is the system one). Grown to clipboard_cap.
|
||||||
char (*clipboard_paths)[256];
|
char (*clipboard_paths)[256];
|
||||||
int clipboard_cap;
|
int clipboard_cap;
|
||||||
int clipboard_count;
|
int clipboard_count;
|
||||||
bool clipboard_is_cut;
|
bool clipboard_is_cut;
|
||||||
|
uint64_t clipboard_serial; // system clipboard serial at that load
|
||||||
|
// Cached answer to "does the clipboard hold files?" for the toolbar and
|
||||||
|
// menus, refreshed when the clipboard serial changes.
|
||||||
|
uint64_t clipboard_checked_serial;
|
||||||
|
bool clipboard_has_files;
|
||||||
|
|
||||||
// Multi-selection (marquee, Ctrl/Shift-click), sized with the entry arrays.
|
// Multi-selection (marquee, Ctrl/Shift-click), sized with the entry arrays.
|
||||||
bool* multi_selected;
|
bool* multi_selected;
|
||||||
@@ -115,19 +156,20 @@ struct FileManagerState {
|
|||||||
int ctx_items[FM_CTX_MAX_ITEMS];
|
int ctx_items[FM_CTX_MAX_ITEMS];
|
||||||
int ctx_item_count;
|
int ctx_item_count;
|
||||||
int ctx_hover;
|
int ctx_hover;
|
||||||
|
// "New" submenu (newfile.cpp), open while the pointer is on its row.
|
||||||
|
bool ctx_sub_open;
|
||||||
|
int ctx_sub_hover;
|
||||||
|
int ctx_sub_items[FM_CTX_MAX_ITEMS];
|
||||||
|
int ctx_sub_count;
|
||||||
|
|
||||||
// Rename
|
// Rename
|
||||||
bool rename_active;
|
bool rename_active;
|
||||||
int rename_idx;
|
int rename_idx;
|
||||||
char rename_buf[128];
|
FileManagerTextField rename;
|
||||||
int rename_cursor;
|
|
||||||
int rename_len;
|
|
||||||
|
|
||||||
// Path bar editing
|
// Path bar editing
|
||||||
bool pathbar_editing;
|
bool pathbar_editing;
|
||||||
char pathbar_buf[256];
|
FileManagerTextField pathbar;
|
||||||
int pathbar_cursor;
|
|
||||||
int pathbar_len;
|
|
||||||
|
|
||||||
// Virtual views (sized with the entry arrays)
|
// Virtual views (sized with the entry arrays)
|
||||||
FileManagerVirtualViewKind virtual_view;
|
FileManagerVirtualViewKind virtual_view;
|
||||||
@@ -150,6 +192,41 @@ struct FileManagerState {
|
|||||||
// File Manager-owned dialogs
|
// File Manager-owned dialogs
|
||||||
void* delete_confirm_dialog;
|
void* delete_confirm_dialog;
|
||||||
void* open_with_dialog;
|
void* open_with_dialog;
|
||||||
|
|
||||||
|
// No window of its own (files.elf --transfer / --open-with).
|
||||||
|
bool headless;
|
||||||
|
|
||||||
|
// Names starting with '.' are listed only when set (prefs; Ctrl+H).
|
||||||
|
bool show_hidden;
|
||||||
|
|
||||||
|
// Sidebar (sidebar.cpp), rebuilt on refresh and bookmark changes.
|
||||||
|
FileManagerSidebarItem* sidebar_items;
|
||||||
|
int sidebar_count;
|
||||||
|
int sidebar_scroll; // first visible row
|
||||||
|
int sidebar_hover; // row under the pointer, -1 = none
|
||||||
|
int sidebar_width; // wanted width (prefs); the layout clamps it to the window
|
||||||
|
bool sidebar_resizing; // dragging the sidebar's right edge
|
||||||
|
int sidebar_drag_dx; // pointer offset from the edge when the drag began
|
||||||
|
int cursor_style; // CursorStyle last sent to the Window Server
|
||||||
|
|
||||||
|
// Undo / redo of the last file operations (undo.cpp).
|
||||||
|
struct FileManagerUndo* undo;
|
||||||
|
|
||||||
|
// Image thumbnails for the grid view (thumbnails.cpp).
|
||||||
|
struct FileManagerThumbs* thumbs;
|
||||||
|
|
||||||
|
// Drag and drop (dnd.cpp).
|
||||||
|
int press_idx; // entry the left button went down on, -1 = none
|
||||||
|
int press_x, press_y;
|
||||||
|
uint32_t drag_serial; // our outgoing drag, 0 = none
|
||||||
|
uint32_t drop_serial; // incoming drag over this window, 0 = none
|
||||||
|
char* drop_list; // its file list (TRANSFER_MIME_FILE_LIST), cached
|
||||||
|
int drop_target_idx; // entry highlighted as the drop target, -1 = none
|
||||||
|
int drop_sidebar_idx; // sidebar row highlighted as the drop target
|
||||||
|
uint8_t drop_action; // what a drop at the current position would do
|
||||||
|
// After our files were moved into another window, re-read the folder a
|
||||||
|
// few times: the move runs asynchronously in the other process.
|
||||||
|
uint64_t refresh_due[3];
|
||||||
};
|
};
|
||||||
|
|
||||||
inline constexpr int FM_TOOLBAR_H = 32;
|
inline constexpr int FM_TOOLBAR_H = 32;
|
||||||
@@ -163,6 +240,51 @@ inline constexpr int FM_GRID_CELL_H_WRAPPED = 92;
|
|||||||
inline constexpr int FM_GRID_ICON = 48;
|
inline constexpr int FM_GRID_ICON = 48;
|
||||||
inline constexpr int FM_GRID_PAD = 4;
|
inline constexpr int FM_GRID_PAD = 4;
|
||||||
inline constexpr int FM_CTX_MENU_MIN_W = 140;
|
inline constexpr int FM_CTX_MENU_MIN_W = 140;
|
||||||
|
inline constexpr int FM_SIDEBAR_W = 160; // default width
|
||||||
|
inline constexpr int FM_SIDEBAR_MIN_W = 110;
|
||||||
|
inline constexpr int FM_SIDEBAR_MAX_W = 400;
|
||||||
|
inline constexpr int FM_SIDEBAR_MIN_VIEW_W = 300; // a wide sidebar leaves the view this much
|
||||||
|
inline constexpr int FM_SIDEBAR_GRIP = 3; // drag zone either side of its right edge
|
||||||
|
inline constexpr int FM_SIDEBAR_MIN_WINDOW_W = 480; // narrower windows hide it
|
||||||
|
inline constexpr int FM_SIDEBAR_ITEM_H = 24;
|
||||||
|
|
||||||
|
// Sidebar width actually used for a wanted width in a window content_w wide
|
||||||
|
// (0 when the window is too narrow to show it).
|
||||||
|
inline int filemanager_sidebar_width(int wanted, int content_w) {
|
||||||
|
if (content_w < FM_SIDEBAR_MIN_WINDOW_W) return 0;
|
||||||
|
int w = wanted > 0 ? wanted : FM_SIDEBAR_W;
|
||||||
|
int max_w = content_w - FM_SIDEBAR_MIN_VIEW_W;
|
||||||
|
if (max_w > FM_SIDEBAR_MAX_W) max_w = FM_SIDEBAR_MAX_W;
|
||||||
|
if (w > max_w) w = max_w;
|
||||||
|
if (w < FM_SIDEBAR_MIN_W) w = FM_SIDEBAR_MIN_W;
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Window layout, shared by drawing and hit-testing: toolbar and path bar
|
||||||
|
// across the top, then the sidebar (if the window is wide enough) and the
|
||||||
|
// file view, whose scrollbar sits at the right edge.
|
||||||
|
struct FileManagerLayout {
|
||||||
|
int body_y; // top of sidebar and file view
|
||||||
|
int sidebar_w; // 0 when hidden
|
||||||
|
int view_x; // left edge of the file view
|
||||||
|
int view_w; // file view width, scrollbar excluded
|
||||||
|
int list_y; // first entry row (below the column header in list view)
|
||||||
|
int grid_cols;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline FileManagerLayout filemanager_layout(const FileManagerState* fm, bool grid_view,
|
||||||
|
int content_w) {
|
||||||
|
FileManagerLayout l;
|
||||||
|
l.body_y = FM_TOOLBAR_H + FM_PATHBAR_H;
|
||||||
|
l.sidebar_w = filemanager_sidebar_width(fm->sidebar_width, content_w);
|
||||||
|
l.view_x = l.sidebar_w;
|
||||||
|
l.view_w = content_w - l.view_x - FM_SCROLLBAR_W;
|
||||||
|
if (l.view_w < 1) l.view_w = 1;
|
||||||
|
l.list_y = grid_view ? l.body_y : l.body_y + FM_HEADER_H;
|
||||||
|
l.grid_cols = l.view_w / FM_GRID_CELL_W;
|
||||||
|
if (l.grid_cols < 1) l.grid_cols = 1;
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
|
||||||
// List view column layout. Columns are dropped right to left as the window
|
// List view column layout. Columns are dropped right to left as the window
|
||||||
// narrows (Modified first, then Type, then Size) so the name keeps room.
|
// narrows (Modified first, then Type, then Size) so the name keeps room.
|
||||||
@@ -174,36 +296,32 @@ struct FileManagerListColumns {
|
|||||||
int type_x; // -1 = hidden
|
int type_x; // -1 = hidden
|
||||||
};
|
};
|
||||||
|
|
||||||
inline FileManagerListColumns filemanager_list_columns(int content_w) {
|
// Columns of a list view occupying [x0, x0 + view_w).
|
||||||
|
inline FileManagerListColumns filemanager_list_columns(int x0, int view_w) {
|
||||||
static constexpr int MIN_NAME_W = 160;
|
static constexpr int MIN_NAME_W = 160;
|
||||||
static constexpr int TYPE_W = 96;
|
static constexpr int TYPE_W = 96;
|
||||||
static constexpr int SIZE_W = 76;
|
static constexpr int SIZE_W = 76;
|
||||||
static constexpr int MODIFIED_W = 132;
|
static constexpr int MODIFIED_W = 132;
|
||||||
|
|
||||||
FileManagerListColumns cols = {8, 0, -1, -1, -1};
|
FileManagerListColumns cols = {x0 + 8, 0, -1, -1, -1};
|
||||||
int right = content_w - FM_SCROLLBAR_W;
|
int min_right = x0 + MIN_NAME_W;
|
||||||
if (right - SIZE_W >= MIN_NAME_W) { right -= SIZE_W; cols.size_x = right; }
|
int right = x0 + view_w;
|
||||||
if (right - TYPE_W >= MIN_NAME_W) {
|
if (right - SIZE_W >= min_right) { right -= SIZE_W; cols.size_x = right; }
|
||||||
|
if (right - TYPE_W >= min_right) {
|
||||||
// Type sits right of Size; shift Size left to make room.
|
// Type sits right of Size; shift Size left to make room.
|
||||||
cols.type_x = content_w - FM_SCROLLBAR_W - TYPE_W;
|
cols.type_x = x0 + view_w - TYPE_W;
|
||||||
if (cols.size_x >= 0) cols.size_x -= TYPE_W;
|
if (cols.size_x >= 0) cols.size_x -= TYPE_W;
|
||||||
right -= TYPE_W;
|
right -= TYPE_W;
|
||||||
}
|
}
|
||||||
if (right - MODIFIED_W >= MIN_NAME_W) { right -= MODIFIED_W; cols.modified_x = right; }
|
if (right - MODIFIED_W >= min_right) { right -= MODIFIED_W; cols.modified_x = right; }
|
||||||
cols.name_right = right - 8;
|
cols.name_right = right - 8;
|
||||||
return cols;
|
return cols;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special user folders: name, icon filename, index into DesktopState arrays
|
// Special user folders: name, icon filename, index into DesktopState arrays
|
||||||
inline constexpr int SF_COUNT = DesktopState::SPECIAL_FOLDER_COUNT;
|
inline constexpr int SF_COUNT = gui::SPECIAL_FOLDER_COUNT;
|
||||||
inline constexpr const char* sf_names[SF_COUNT] = {
|
inline constexpr const char* const* sf_names = gui::kSpecialFolderNames;
|
||||||
"Documents", "Desktop", "Music", "Videos", "Pictures", "Downloads"
|
inline constexpr const char* const* sf_icons = gui::kSpecialFolderIcons;
|
||||||
};
|
|
||||||
inline constexpr const char* sf_icons[SF_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"
|
|
||||||
};
|
|
||||||
|
|
||||||
// Context menu action codes
|
// Context menu action codes
|
||||||
inline constexpr int CTX_OPEN = 0;
|
inline constexpr int CTX_OPEN = 0;
|
||||||
@@ -224,6 +342,13 @@ inline constexpr int CTX_SORT_MODIFIED = 14;
|
|||||||
inline constexpr int CTX_TRASH = 15; // Move to Trash
|
inline constexpr int CTX_TRASH = 15; // Move to Trash
|
||||||
inline constexpr int CTX_RESTORE = 16; // Restore from Trash
|
inline constexpr int CTX_RESTORE = 16; // Restore from Trash
|
||||||
inline constexpr int CTX_EMPTY_TRASH = 17;
|
inline constexpr int CTX_EMPTY_TRASH = 17;
|
||||||
|
inline constexpr int CTX_TOGGLE_HIDDEN = 18;
|
||||||
|
inline constexpr int CTX_ADD_BOOKMARK = 19; // "Add to Sidebar"
|
||||||
|
inline constexpr int CTX_REMOVE_BOOKMARK = 20; // "Remove from Sidebar"
|
||||||
|
inline constexpr int CTX_UNDO = 21;
|
||||||
|
inline constexpr int CTX_REDO = 22;
|
||||||
|
inline constexpr int CTX_NEW = 23; // opens the "New" submenu
|
||||||
|
inline constexpr int CTX_NEW_FILE_BASE = 32; // + template index (newfile.cpp)
|
||||||
|
|
||||||
inline bool filemanager_is_virtual_view(const FileManagerState* fm) {
|
inline bool filemanager_is_virtual_view(const FileManagerState* fm) {
|
||||||
return fm && fm->virtual_view != FM_VIRTUAL_VIEW_NONE;
|
return fm && fm->virtual_view != FM_VIRTUAL_VIEW_NONE;
|
||||||
@@ -243,6 +368,9 @@ inline const char* filemanager_virtual_view_label(FileManagerVirtualViewKind vie
|
|||||||
|
|
||||||
int special_folder_index(const char* name);
|
int special_folder_index(const char* name);
|
||||||
const char* ctx_label(int action);
|
const char* ctx_label(int action);
|
||||||
|
// ctx_label, with the wording that depends on state ("Undo Rename",
|
||||||
|
// "Show Hidden Files").
|
||||||
|
const char* ctx_label_for(const FileManagerState* fm, int action);
|
||||||
const char* ctx_shortcut(const FileManagerState* fm, int action);
|
const char* ctx_shortcut(const FileManagerState* fm, int action);
|
||||||
// Menu items of different groups are split by a separator line.
|
// Menu items of different groups are split by a separator line.
|
||||||
int ctx_group(int action);
|
int ctx_group(int action);
|
||||||
@@ -268,7 +396,7 @@ inline void filemanager_build_context_items(const FileManagerState* fm,
|
|||||||
int count = fm->ctx_item_count < max_items ? fm->ctx_item_count : max_items;
|
int count = fm->ctx_item_count < max_items ? fm->ctx_item_count : max_items;
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
int action = fm->ctx_items[i];
|
int action = fm->ctx_items[i];
|
||||||
items[i] = {ctx_label(action), action, true, ctx_shortcut(fm, action),
|
items[i] = {ctx_label_for(fm, action), action, true, ctx_shortcut(fm, action),
|
||||||
i + 1 < count && ctx_group(action) != ctx_group(fm->ctx_items[i + 1])};
|
i + 1 < count && ctx_group(action) != ctx_group(fm->ctx_items[i + 1])};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -293,6 +421,10 @@ int detect_file_type(const char* name, bool is_dir);
|
|||||||
// Lowercased extension after the last '.', without the dot ("" if none).
|
// Lowercased extension after the last '.', without the dot ("" if none).
|
||||||
// Leading-dot names (".profile") count as having no extension.
|
// Leading-dot names (".profile") count as having no extension.
|
||||||
void file_extension(const char* name, char* out, int out_max);
|
void file_extension(const char* name, char* out, int out_max);
|
||||||
|
// Icon-pack icon for a file's type (audio, video, archive, source code...)
|
||||||
|
// by its name, at 16px (small) or 48px. Null when the type has none of its
|
||||||
|
// own, i.e. use the generic file icon. Loaded on first use, never freed.
|
||||||
|
const SvgIcon* filemanager_file_type_icon(const char* name, bool large);
|
||||||
// Human-readable type for the list view's Type column ("PDF file", "Folder").
|
// Human-readable type for the list view's Type column ("PDF file", "Folder").
|
||||||
void filemanager_type_label(const FileManagerState* fm, int idx, char* out, int out_max);
|
void filemanager_type_label(const FileManagerState* fm, int idx, char* out, int out_max);
|
||||||
|
|
||||||
@@ -349,14 +481,26 @@ montauk::StatusCode filemanager_delete_item(const char* path);
|
|||||||
montauk::StatusCode filemanager_copy_file(const char* src, const char* dst);
|
montauk::StatusCode filemanager_copy_file(const char* src, const char* dst);
|
||||||
montauk::StatusCode filemanager_copy_dir_recursive(const char* src, const char* dst);
|
montauk::StatusCode filemanager_copy_dir_recursive(const char* src, const char* dst);
|
||||||
|
|
||||||
|
// Clipboard (actions.cpp). Copy and cut publish TRANSFER_MIME_FILE_LIST,
|
||||||
|
// TRANSFER_MIME_FILE_OP ("copy"/"cut") and the paths as plain text.
|
||||||
|
bool filemanager_clipboard_has_files(FileManagerState* fm);
|
||||||
|
int filemanager_sync_clipboard(FileManagerState* fm);
|
||||||
|
// After a cut has been pasted: drop it from the clipboard, unless something
|
||||||
|
// else was copied meanwhile.
|
||||||
|
void filemanager_consume_cut(FileManagerState* fm);
|
||||||
|
|
||||||
void filemanager_do_copy(FileManagerState* fm);
|
void filemanager_do_copy(FileManagerState* fm);
|
||||||
void filemanager_do_cut(FileManagerState* fm);
|
void filemanager_do_cut(FileManagerState* fm);
|
||||||
void filemanager_do_paste(FileManagerState* fm);
|
void filemanager_do_paste(FileManagerState* fm);
|
||||||
|
|
||||||
|
bool filemanager_name_char_ok(char ch, void* userdata); // rename filter
|
||||||
void filemanager_start_rename(FileManagerState* fm);
|
void filemanager_start_rename(FileManagerState* fm);
|
||||||
void filemanager_finish_rename(FileManagerState* fm);
|
void filemanager_finish_rename(FileManagerState* fm);
|
||||||
void filemanager_cancel_rename(FileManagerState* fm);
|
void filemanager_cancel_rename(FileManagerState* fm);
|
||||||
void filemanager_new_folder(FileManagerState* fm);
|
void filemanager_new_folder(FileManagerState* fm);
|
||||||
|
// Show or hide names starting with '.' (remembered per user).
|
||||||
|
void filemanager_toggle_hidden(FileManagerState* fm);
|
||||||
|
void filemanager_add_bookmark(FileManagerState* fm, int target_idx);
|
||||||
// Delete key / toolbar: Move to Trash where possible, otherwise (or with
|
// Delete key / toolbar: Move to Trash where possible, otherwise (or with
|
||||||
// permanent set, i.e. Shift+Delete) delete permanently after confirming.
|
// permanent set, i.e. Shift+Delete) delete permanently after confirming.
|
||||||
void filemanager_delete_selected(FileManagerState* fm, bool permanent = false);
|
void filemanager_delete_selected(FileManagerState* fm, bool permanent = false);
|
||||||
@@ -396,6 +540,125 @@ void filemanager_cancel_pathbar(FileManagerState* fm);
|
|||||||
void filemanager_commit_pathbar(FileManagerState* fm);
|
void filemanager_commit_pathbar(FileManagerState* fm);
|
||||||
|
|
||||||
void filemanager_open_entry(FileManagerState* fm, int idx);
|
void filemanager_open_entry(FileManagerState* fm, int idx);
|
||||||
|
// Navigate to an absolute folder path (pushes history). Empty = Computer.
|
||||||
|
void filemanager_go_to_path(FileManagerState* fm, const char* path);
|
||||||
|
|
||||||
|
// ---- Preferences (prefs.cpp): <home>/config/files.toml ----
|
||||||
|
// Every change re-reads the file first, so several Files windows (each its
|
||||||
|
// own process) do not undo one another's edits.
|
||||||
|
void prefs_load(DesktopState* ds);
|
||||||
|
bool prefs_show_hidden();
|
||||||
|
void prefs_set_show_hidden(DesktopState* ds, bool show);
|
||||||
|
int prefs_sidebar_width(); // 0 = default
|
||||||
|
void prefs_set_sidebar_width(DesktopState* ds, int width);
|
||||||
|
int prefs_bookmark_count();
|
||||||
|
const char* prefs_bookmark(int i);
|
||||||
|
bool prefs_has_bookmark(const char* path);
|
||||||
|
void prefs_add_bookmark(DesktopState* ds, const char* path);
|
||||||
|
void prefs_remove_bookmark(DesktopState* ds, const char* path);
|
||||||
|
// The remembered view of a folder, if any.
|
||||||
|
bool prefs_folder_view(const char* path, bool* grid, FileManagerSortKey* key, bool* desc);
|
||||||
|
void prefs_set_folder_view(DesktopState* ds, const char* path, bool grid,
|
||||||
|
FileManagerSortKey key, bool desc);
|
||||||
|
|
||||||
|
// ---- Sidebar (sidebar.cpp) ----
|
||||||
|
void filemanager_sidebar_rebuild(FileManagerState* fm);
|
||||||
|
void filemanager_sidebar_draw(Canvas& c, FileManagerState* fm, const FileManagerLayout& l);
|
||||||
|
// Mouse over the sidebar (window coordinates). Returns true if it was there.
|
||||||
|
bool filemanager_sidebar_mouse(FileManagerState* fm, const FileManagerLayout& l,
|
||||||
|
int x, int y, MouseEvent& ev, int ch);
|
||||||
|
// Dragging the sidebar's right edge to resize it. Returns true if the event
|
||||||
|
// was consumed (a drag is in progress or just began).
|
||||||
|
bool filemanager_sidebar_resize_mouse(Window* win, FileManagerState* fm,
|
||||||
|
const FileManagerLayout& l, int x, int y, MouseEvent& ev);
|
||||||
|
int filemanager_sidebar_item_at(FileManagerState* fm, const FileManagerLayout& l, int x, int y);
|
||||||
|
void filemanager_sidebar_open(FileManagerState* fm, int idx);
|
||||||
|
// Folder a sidebar row stands for, for drops ("" for Computer/headers).
|
||||||
|
bool filemanager_sidebar_target_path(FileManagerState* fm, int idx, char* out, int out_max);
|
||||||
|
|
||||||
|
// ---- New files (newfile.cpp) ----
|
||||||
|
const char* filemanager_new_file_label(int action);
|
||||||
|
// Fill ctx_sub_items: Folder, then the document types some app can open.
|
||||||
|
void filemanager_build_new_submenu(FileManagerState* fm);
|
||||||
|
int filemanager_new_submenu_items(const FileManagerState* fm, mtk::ContextMenuItem* items);
|
||||||
|
// Where the submenu goes (beside the "New" row). False if the menu has no
|
||||||
|
// "New" row. sub->open mirrors ctx_sub_open.
|
||||||
|
bool filemanager_new_submenu_layout(const FileManagerState* fm, int view_w, int view_h,
|
||||||
|
mtk::ContextMenuState* sub, int* sub_w);
|
||||||
|
// Create "New Text Document.txt" etc. for CTX_NEW_FILE_BASE + i, then rename it.
|
||||||
|
void filemanager_new_file(FileManagerState* fm, int action);
|
||||||
|
montauk::StatusCode filemanager_new_file_write(const char* path, const char* key);
|
||||||
|
// Still exactly as created (undo only removes untouched files).
|
||||||
|
bool filemanager_new_file_unchanged(const char* path, const char* key);
|
||||||
|
|
||||||
|
// ---- Text fields (textfield.cpp) ----
|
||||||
|
int filemanager_field_len(const FileManagerTextField& f);
|
||||||
|
// Load text, selecting [sel_start, sel_end) (caret at sel_end).
|
||||||
|
void filemanager_field_set(FileManagerTextField& f, const char* text, int cap, int pad,
|
||||||
|
int sel_start, int sel_end,
|
||||||
|
mtk::TextInputCharFilter filter = nullptr);
|
||||||
|
void filemanager_field_select(FileManagerTextField& f, int anchor, int cursor);
|
||||||
|
// Editing keys (typing, Backspace/Delete, arrows, Home/End, Ctrl+A/C/X/V,
|
||||||
|
// Shift/Ctrl selection). Enter, Escape and Tab are the caller's.
|
||||||
|
bool filemanager_field_key(FileManagerTextField& f, const montauk::abi::KeyEvent& key);
|
||||||
|
// Mouse in window coordinates. True if the field used the event (clicks
|
||||||
|
// inside it, a selection drag, its context menu).
|
||||||
|
bool filemanager_field_mouse(FileManagerTextField& f, const Rect& box, int mx, int my,
|
||||||
|
const MouseEvent& ev, int view_w, int view_h, bool shift);
|
||||||
|
void filemanager_field_draw(Canvas& c, FileManagerTextField& f, const Rect& box, Color accent);
|
||||||
|
// Its Cut/Copy/Paste menu, drawn last so it is on top.
|
||||||
|
void filemanager_field_draw_menu(Canvas& c, const FileManagerTextField& f, Color accent);
|
||||||
|
// Where the rename box of the entry being renamed is (window coordinates).
|
||||||
|
// False if nothing is being renamed or the entry is scrolled out of view.
|
||||||
|
bool filemanager_rename_box(FileManagerState* fm, const FileManagerLayout& l, int ch, Rect* out);
|
||||||
|
inline Rect filemanager_pathbar_box(int content_w) {
|
||||||
|
return {0, FM_TOOLBAR_H, content_w, FM_PATHBAR_H};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Path bar (breadcrumbs.cpp): clickable segments, Tab completion ----
|
||||||
|
void filemanager_draw_breadcrumbs(Canvas& c, FileManagerState* fm, int y);
|
||||||
|
// A click at x in the (not editing) path bar: navigates and returns true if
|
||||||
|
// it hit a segment; false means "start editing".
|
||||||
|
bool filemanager_breadcrumb_click(FileManagerState* fm, int x, int content_w);
|
||||||
|
void filemanager_pathbar_complete(FileManagerState* fm);
|
||||||
|
|
||||||
|
// ---- Undo / redo (undo.cpp) ----
|
||||||
|
enum FileManagerUndoKind : uint8_t {
|
||||||
|
FM_UNDO_RENAME = 1, // pairs[0]: from -> to
|
||||||
|
FM_UNDO_NEW_FOLDER, // pairs[0].to = the folder
|
||||||
|
FM_UNDO_MOVE, // each pair: src -> dst
|
||||||
|
FM_UNDO_COPY, // each pair: src -> created copy
|
||||||
|
FM_UNDO_TRASH, // each pair: original -> item in the Trash
|
||||||
|
FM_UNDO_NEW_FILE, // pairs[0]: template key -> the file
|
||||||
|
};
|
||||||
|
// Record a completed operation. `from`/`to` are parallel path arrays
|
||||||
|
// (`from` may be null for NEW_FOLDER). Clears the redo history.
|
||||||
|
void filemanager_undo_record(FileManagerState* fm, FileManagerUndoKind kind,
|
||||||
|
const char* const* from, const char* const* to, int count);
|
||||||
|
bool filemanager_can_undo(const FileManagerState* fm);
|
||||||
|
bool filemanager_can_redo(const FileManagerState* fm);
|
||||||
|
void filemanager_undo(FileManagerState* fm);
|
||||||
|
void filemanager_redo(FileManagerState* fm);
|
||||||
|
// Short description for menus, e.g. "Undo Rename".
|
||||||
|
const char* filemanager_undo_label(const FileManagerState* fm);
|
||||||
|
const char* filemanager_redo_label(const FileManagerState* fm);
|
||||||
|
void filemanager_undo_free(FileManagerState* fm);
|
||||||
|
|
||||||
|
// ---- Thumbnails (thumbnails.cpp) ----
|
||||||
|
// Queue thumbnails for the image files of the current listing.
|
||||||
|
void filemanager_thumbs_request(FileManagerState* fm);
|
||||||
|
// Take finished thumbnails; true if any arrived (redraw).
|
||||||
|
bool filemanager_thumbs_poll(FileManagerState* fm);
|
||||||
|
// Thumbnail for entry idx, or null (not an image, or not ready yet).
|
||||||
|
const SvgIcon* filemanager_thumb_for(FileManagerState* fm, int idx);
|
||||||
|
void filemanager_thumbs_free(FileManagerState* fm);
|
||||||
|
|
||||||
|
// ---- Drag and drop (dnd.cpp) ----
|
||||||
|
// Mouse hook for starting drags: true if the event was consumed.
|
||||||
|
bool filemanager_dnd_mouse(Window* win, FileManagerState* fm, const FileManagerLayout& l,
|
||||||
|
int x, int y, MouseEvent& ev);
|
||||||
|
// Per-frame housekeeping (scheduled refreshes after a move went elsewhere).
|
||||||
|
void filemanager_dnd_poll(FileManagerState* fm);
|
||||||
|
|
||||||
// ---- Trash (trash.cpp) ----
|
// ---- Trash (trash.cpp) ----
|
||||||
// <home>/.trash/files holds trashed items; the view of that folder is the
|
// <home>/.trash/files holds trashed items; the view of that folder is the
|
||||||
@@ -410,7 +673,9 @@ bool filemanager_trash_has_items(const DesktopState* ds);
|
|||||||
// Delete means Move to Trash here: a real folder on the home volume, outside
|
// Delete means Move to Trash here: a real folder on the home volume, outside
|
||||||
// the Trash itself.
|
// the Trash itself.
|
||||||
bool filemanager_can_trash(const FileManagerState* fm);
|
bool filemanager_can_trash(const FileManagerState* fm);
|
||||||
montauk::StatusCode filemanager_trash_item(const DesktopState* ds, const char* path);
|
// On success *trashed_out (if given) receives the item's path in the Trash.
|
||||||
|
montauk::StatusCode filemanager_trash_item(const DesktopState* ds, const char* path,
|
||||||
|
char* trashed_out = nullptr, int trashed_max = 0);
|
||||||
// Move a top-level Trash item back to where it came from (recreating missing
|
// Move a top-level Trash item back to where it came from (recreating missing
|
||||||
// parent folders, or picking "name (2)" if the original name is taken).
|
// parent folders, or picking "name (2)" if the original name is taken).
|
||||||
montauk::StatusCode filemanager_trash_restore(const char* trashed, char* restored, int restored_max);
|
montauk::StatusCode filemanager_trash_restore(const char* trashed, char* restored, int restored_max);
|
||||||
@@ -454,6 +719,13 @@ enum FileOpKind : int {
|
|||||||
bool filemanager_start_paste_job(FileManagerState* fm);
|
bool filemanager_start_paste_job(FileManagerState* fm);
|
||||||
bool filemanager_start_delete_job(FileManagerState* fm, const char* const* paths, int count);
|
bool filemanager_start_delete_job(FileManagerState* fm, const char* const* paths, int count);
|
||||||
bool filemanager_start_trash_job(FileManagerState* fm, const char* const* paths, int count);
|
bool filemanager_start_trash_job(FileManagerState* fm, const char* const* paths, int count);
|
||||||
|
// Copy or move paths into dest_dir (paste, drop), prompting on conflicts.
|
||||||
|
bool filemanager_start_transfer(FileManagerState* fm, bool move, const char* const* paths,
|
||||||
|
int count, const char* dest_dir);
|
||||||
|
// Copy or move each from[i] to exactly to[i] (undo / redo). No prompts: an
|
||||||
|
// existing destination fails that item.
|
||||||
|
bool filemanager_start_pairs_job(FileManagerState* fm, bool move, const char* const* from,
|
||||||
|
const char* const* to, int count, bool record_undo);
|
||||||
|
|
||||||
int filemanager_grid_row_height(FileManagerState* fm, int row, int cols);
|
int filemanager_grid_row_height(FileManagerState* fm, int row, int cols);
|
||||||
int filemanager_grid_row_y(FileManagerState* fm, int row, int cols);
|
int filemanager_grid_row_y(FileManagerState* fm, int row, int cols);
|
||||||
@@ -480,5 +752,12 @@ void filemanager_on_draw(Window* win, Framebuffer& fb);
|
|||||||
void filemanager_on_mouse(Window* win, MouseEvent& ev);
|
void filemanager_on_mouse(Window* win, MouseEvent& ev);
|
||||||
void filemanager_on_key(Window* win, const montauk::abi::KeyEvent& key);
|
void filemanager_on_key(Window* win, const montauk::abi::KeyEvent& key);
|
||||||
void filemanager_on_close(Window* win);
|
void filemanager_on_close(Window* win);
|
||||||
|
void filemanager_on_poll(Window* win);
|
||||||
|
|
||||||
} // namespace filemanager
|
} // namespace filemanager
|
||||||
|
|
||||||
|
// Loads the icons Files draws beyond the host's base set (app.cpp).
|
||||||
|
void ensure_filemanager_icons_loaded(DesktopState* ds);
|
||||||
|
// Open a Files window on `path` (a folder, virtual://apps or
|
||||||
|
// virtual://settings), or on Computer when path is null.
|
||||||
|
void open_filemanager_path(DesktopState* ds, const char* path);
|
||||||
@@ -26,10 +26,21 @@ enum FileOpChoice : int {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct FileOpJob {
|
struct FileOpJob {
|
||||||
int kind; // FileOpKind: copy / move / delete
|
int kind; // FileOpKind: copy / move / delete / trash
|
||||||
char (*paths)[256]; // owned: source paths (copy/move) or targets (delete)
|
char (*paths)[256]; // owned: source paths (copy/move) or targets (delete/trash)
|
||||||
int count;
|
int count;
|
||||||
char dest_dir[256]; // destination directory (copy/move); empty for delete
|
char dest_dir[256]; // destination directory (copy/move); empty otherwise
|
||||||
|
// Optional exact destination per item (undo / redo). Such items never
|
||||||
|
// prompt: an existing destination fails with AlreadyExists.
|
||||||
|
char (*dests)[256];
|
||||||
|
|
||||||
|
// Per-item results for undo, written by the worker, read after the join:
|
||||||
|
// where the item ended up, whether that worked, and whether the
|
||||||
|
// destination was new (not a replace or a merge into something there).
|
||||||
|
char (*done_paths)[256];
|
||||||
|
bool* done_ok;
|
||||||
|
bool* done_new;
|
||||||
|
bool record_undo;
|
||||||
|
|
||||||
DesktopState* desktop;
|
DesktopState* desktop;
|
||||||
FileManagerState* owner; // for the post-op refresh (liveness-checked)
|
FileManagerState* owner; // for the post-op refresh (liveness-checked)
|
||||||
@@ -173,7 +184,9 @@ static int fileop_worker(void* arg) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (job->kind == FILE_OP_TRASH) {
|
if (job->kind == FILE_OP_TRASH) {
|
||||||
fileop_record_status(job, src, filemanager_trash_item(job->desktop, src));
|
montauk::StatusCode st = filemanager_trash_item(job->desktop, src, job->done_paths[i], 256);
|
||||||
|
job->done_ok[i] = job->done_new[i] = st == montauk::Success;
|
||||||
|
fileop_record_status(job, src, st);
|
||||||
job->files_done = i + 1;
|
job->files_done = i + 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -188,18 +201,30 @@ static int fileop_worker(void* arg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
char dst[512];
|
char dst[512];
|
||||||
filemanager_build_fullpath(dst, 512, job->dest_dir, base);
|
bool replacing = false;
|
||||||
if (montauk::streq(dst, src)) {
|
if (job->dests) {
|
||||||
// Moving into its own folder is a no-op; copying duplicates it.
|
montauk::strncpy(dst, job->dests[i], sizeof(dst));
|
||||||
if (job->kind == FILE_OP_MOVE) { job->files_done = i + 1; continue; }
|
if (filemanager_path_exists(dst)) {
|
||||||
filemanager_unique_path(job->dest_dir, base, is_dir, true, dst, sizeof(dst));
|
fileop_record_status(job, dst, montauk::AlreadyExists);
|
||||||
} else if (filemanager_path_exists(dst)) {
|
job->files_done = i + 1;
|
||||||
int choice = fileop_resolve_conflict(job, base, is_dir, filemanager_path_is_dir(dst));
|
continue;
|
||||||
if (job->cancel || choice == FILEOP_CHOICE_SKIP) { job->files_done = i + 1; continue; }
|
}
|
||||||
if (choice == FILEOP_CHOICE_KEEP_BOTH)
|
} else {
|
||||||
filemanager_unique_path(job->dest_dir, base, is_dir, false, dst, sizeof(dst));
|
filemanager_build_fullpath(dst, 512, job->dest_dir, base);
|
||||||
// Replace: rename and copy_file swap a file in atomically, and a
|
if (montauk::streq(dst, src)) {
|
||||||
// folder copy merges into the existing one.
|
// Moving into its own folder is a no-op; copying duplicates it.
|
||||||
|
if (job->kind == FILE_OP_MOVE) { job->files_done = i + 1; continue; }
|
||||||
|
filemanager_unique_path(job->dest_dir, base, is_dir, true, dst, sizeof(dst));
|
||||||
|
} else if (filemanager_path_exists(dst)) {
|
||||||
|
int choice = fileop_resolve_conflict(job, base, is_dir, filemanager_path_is_dir(dst));
|
||||||
|
if (job->cancel || choice == FILEOP_CHOICE_SKIP) { job->files_done = i + 1; continue; }
|
||||||
|
if (choice == FILEOP_CHOICE_KEEP_BOTH)
|
||||||
|
filemanager_unique_path(job->dest_dir, base, is_dir, false, dst, sizeof(dst));
|
||||||
|
else
|
||||||
|
replacing = true;
|
||||||
|
// Replace: rename and copy_file swap a file in atomically, and
|
||||||
|
// a folder copy merges into the existing one.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
montauk::StatusCode status;
|
montauk::StatusCode status;
|
||||||
@@ -222,6 +247,11 @@ static int fileop_worker(void* arg) {
|
|||||||
: filemanager_copy_file(src, dst);
|
: filemanager_copy_file(src, dst);
|
||||||
}
|
}
|
||||||
fileop_record_status(job, src, status);
|
fileop_record_status(job, src, status);
|
||||||
|
if (status == montauk::Success) {
|
||||||
|
montauk::strncpy(job->done_paths[i], dst, 256);
|
||||||
|
job->done_ok[i] = true;
|
||||||
|
job->done_new[i] = !replacing;
|
||||||
|
}
|
||||||
job->files_done = i + 1;
|
job->files_done = i + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -488,6 +518,62 @@ static void fileop_dialog_on_poll(Window* win) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void fileop_free(FileOpJob* job) {
|
||||||
|
montauk::mfree(job->paths);
|
||||||
|
montauk::mfree(job->dests);
|
||||||
|
montauk::mfree(job->done_paths);
|
||||||
|
montauk::mfree(job->done_ok);
|
||||||
|
montauk::mfree(job->done_new);
|
||||||
|
montauk::mfree(job);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hand what the job did to the undo history. Copies only count where they
|
||||||
|
// created something new: undoing a replace or merge cannot bring the old
|
||||||
|
// contents back, so those items are left out.
|
||||||
|
static void fileop_record_undo(FileOpJob* job, FileManagerState* owner) {
|
||||||
|
if (!job->record_undo) return;
|
||||||
|
FileManagerUndoKind kind;
|
||||||
|
switch (job->kind) {
|
||||||
|
case FILE_OP_MOVE: kind = FM_UNDO_MOVE; break;
|
||||||
|
case FILE_OP_COPY: kind = FM_UNDO_COPY; break;
|
||||||
|
case FILE_OP_TRASH: kind = FM_UNDO_TRASH; break;
|
||||||
|
default: return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char** from = (const char**)montauk::malloc((uint64_t)job->count * sizeof(char*));
|
||||||
|
const char** to = (const char**)montauk::malloc((uint64_t)job->count * sizeof(char*));
|
||||||
|
if (from && to) {
|
||||||
|
int n = 0;
|
||||||
|
for (int i = 0; i < job->count; i++) {
|
||||||
|
if (!job->done_ok[i]) continue;
|
||||||
|
if (kind == FM_UNDO_COPY && !job->done_new[i]) continue;
|
||||||
|
from[n] = job->paths[i];
|
||||||
|
to[n] = job->done_paths[i];
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
if (n > 0) filemanager_undo_record(owner, kind, from, to, n);
|
||||||
|
}
|
||||||
|
montauk::mfree(from);
|
||||||
|
montauk::mfree(to);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The worker has finished (or never ran on a thread): refresh the window
|
||||||
|
// that started the job, record undo, report failures, free the job.
|
||||||
|
static void fileop_complete(FileOpJob* job) {
|
||||||
|
FileManagerState* owner = fileop_live_owner(job);
|
||||||
|
if (owner) {
|
||||||
|
if (owner->active_fileop == job) owner->active_fileop = nullptr;
|
||||||
|
filemanager_clear_multi_selection(owner);
|
||||||
|
filemanager_read_dir(owner);
|
||||||
|
fileop_record_undo(job, owner);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Opening the popup here is safe: desktop_close_window() only shifts the
|
||||||
|
// window array after on_close returns, and the new window moves with it.
|
||||||
|
fileop_report_errors(job);
|
||||||
|
fileop_free(job);
|
||||||
|
}
|
||||||
|
|
||||||
static void fileop_dialog_on_close(Window* win) {
|
static void fileop_dialog_on_close(Window* win) {
|
||||||
FileOpJob* job = (FileOpJob*)win->app_data;
|
FileOpJob* job = (FileOpJob*)win->app_data;
|
||||||
if (!job) return;
|
if (!job) return;
|
||||||
@@ -499,20 +585,8 @@ static void fileop_dialog_on_close(Window* win) {
|
|||||||
job->tid = 0;
|
job->tid = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
FileManagerState* owner = fileop_live_owner(job);
|
|
||||||
if (owner) {
|
|
||||||
if (owner->active_fileop == job) owner->active_fileop = nullptr;
|
|
||||||
filemanager_clear_multi_selection(owner);
|
|
||||||
filemanager_read_dir(owner);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Opening the popup here is safe: desktop_close_window() only shifts the
|
|
||||||
// window array after on_close returns, and the new window moves with it.
|
|
||||||
fileop_report_errors(job);
|
|
||||||
|
|
||||||
if (job->paths) montauk::mfree(job->paths);
|
|
||||||
montauk::mfree(job);
|
|
||||||
win->app_data = nullptr;
|
win->app_data = nullptr;
|
||||||
|
fileop_complete(job);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool fileop_show_dialog(FileOpJob* job) {
|
static bool fileop_show_dialog(FileOpJob* job) {
|
||||||
@@ -561,15 +635,7 @@ static bool fileop_launch(FileOpJob* job) {
|
|||||||
if (tid <= 0) {
|
if (tid <= 0) {
|
||||||
// Degraded path: do it inline (UI blocks), then refresh.
|
// Degraded path: do it inline (UI blocks), then refresh.
|
||||||
fileop_worker(job);
|
fileop_worker(job);
|
||||||
FileManagerState* owner = fileop_live_owner(job);
|
fileop_complete(job);
|
||||||
if (owner) {
|
|
||||||
owner->active_fileop = nullptr;
|
|
||||||
filemanager_clear_multi_selection(owner);
|
|
||||||
filemanager_read_dir(owner);
|
|
||||||
}
|
|
||||||
fileop_report_errors(job);
|
|
||||||
if (job->paths) montauk::mfree(job->paths);
|
|
||||||
montauk::mfree(job);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
job->tid = tid;
|
job->tid = tid;
|
||||||
@@ -578,30 +644,35 @@ static bool fileop_launch(FileOpJob* job) {
|
|||||||
// Window creation failed: let the worker finish, then reap + refresh
|
// Window creation failed: let the worker finish, then reap + refresh
|
||||||
// without a dialog.
|
// without a dialog.
|
||||||
montauk::thread_join(job->tid, nullptr);
|
montauk::thread_join(job->tid, nullptr);
|
||||||
FileManagerState* owner = fileop_live_owner(job);
|
job->tid = 0;
|
||||||
if (owner) {
|
fileop_complete(job);
|
||||||
owner->active_fileop = nullptr;
|
|
||||||
filemanager_clear_multi_selection(owner);
|
|
||||||
filemanager_read_dir(owner);
|
|
||||||
}
|
|
||||||
fileop_report_errors(job);
|
|
||||||
if (job->paths) montauk::mfree(job->paths);
|
|
||||||
montauk::mfree(job);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static FileOpJob* fileop_alloc_job(int kind, int count) {
|
static FileOpJob* fileop_alloc_job(FileManagerState* fm, int kind, int count) {
|
||||||
if (count <= 0) return nullptr;
|
if (count <= 0) return nullptr;
|
||||||
FileOpJob* job = (FileOpJob*)montauk::malloc(sizeof(FileOpJob));
|
FileOpJob* job = (FileOpJob*)montauk::malloc(sizeof(FileOpJob));
|
||||||
if (!job) return nullptr;
|
if (!job) return nullptr;
|
||||||
montauk::memset(job, 0, sizeof(FileOpJob));
|
montauk::memset(job, 0, sizeof(FileOpJob));
|
||||||
job->paths = (char(*)[256])montauk::malloc((uint64_t)count * 256);
|
job->paths = (char(*)[256])montauk::malloc((uint64_t)count * 256);
|
||||||
if (!job->paths) { montauk::mfree(job); return nullptr; }
|
job->done_paths = (char(*)[256])montauk::malloc((uint64_t)count * 256);
|
||||||
|
job->done_ok = (bool*)montauk::malloc((uint64_t)count);
|
||||||
|
job->done_new = (bool*)montauk::malloc((uint64_t)count);
|
||||||
|
if (!job->paths || !job->done_paths || !job->done_ok || !job->done_new) {
|
||||||
|
fileop_free(job);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
montauk::memset(job->done_ok, 0, (uint64_t)count);
|
||||||
|
montauk::memset(job->done_new, 0, (uint64_t)count);
|
||||||
job->kind = kind;
|
job->kind = kind;
|
||||||
job->count = count;
|
job->count = count;
|
||||||
job->files_total = count;
|
job->files_total = count;
|
||||||
|
job->record_undo = true;
|
||||||
|
job->desktop = fm->desktop;
|
||||||
|
job->owner = fm;
|
||||||
|
job->accent = fm->desktop ? fm->desktop->settings.accent_color : Color{};
|
||||||
return job;
|
return job;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -609,30 +680,28 @@ static FileOpJob* fileop_alloc_job(int kind, int count) {
|
|||||||
// Public entry points
|
// Public entry points
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
bool filemanager_start_paste_job(FileManagerState* fm) {
|
// Copy or move `paths` into `dest_dir` (a paste or a drop). A single small
|
||||||
if (!fm || fm->active_fileop) return false;
|
// file goes inline when it has no name conflict, so quick copies show no
|
||||||
if (fm->clipboard_count <= 0) return false;
|
// progress dialog. Returns false if nothing was started.
|
||||||
if (filemanager_is_computer_view(fm) || filemanager_is_virtual_view(fm)) return false;
|
bool filemanager_start_transfer(FileManagerState* fm, bool move, const char* const* paths,
|
||||||
|
int count, const char* dest_dir) {
|
||||||
|
if (!fm || fm->active_fileop || !paths || count <= 0 || !dest_dir || !dest_dir[0]) return false;
|
||||||
|
|
||||||
bool is_cut = fm->clipboard_is_cut;
|
if (count == 1 && !filemanager_path_is_dir(paths[0])) {
|
||||||
|
const char* src = paths[0];
|
||||||
// Fast path: a single small plain file pastes inline so quick copies stay
|
|
||||||
// snappy (no thread, no dialog flash). A name conflict needs the prompt,
|
|
||||||
// so that case goes through the job instead.
|
|
||||||
if (fm->clipboard_count == 1 && !filemanager_path_is_dir(fm->clipboard_paths[0])) {
|
|
||||||
const char* src = fm->clipboard_paths[0];
|
|
||||||
uint64_t sz = 0;
|
uint64_t sz = 0;
|
||||||
int fd = montauk::open(src);
|
int fd = montauk::open(src);
|
||||||
if (fd >= 0) { sz = montauk::getsize(fd); montauk::close(fd); }
|
if (fd >= 0) { sz = montauk::getsize(fd); montauk::close(fd); }
|
||||||
char dst[512];
|
char dst[512];
|
||||||
filemanager_build_fullpath(dst, 512, fm->current_path, path_basename(src));
|
filemanager_build_fullpath(dst, 512, dest_dir, path_basename(src));
|
||||||
bool same = montauk::streq(dst, src);
|
bool same = montauk::streq(dst, src);
|
||||||
if (same && !is_cut)
|
if (same && !move)
|
||||||
filemanager_unique_path(fm->current_path, path_basename(src), false, true, dst, sizeof(dst));
|
filemanager_unique_path(dest_dir, path_basename(src), false, true, dst, sizeof(dst));
|
||||||
if (sz <= FILEOP_INLINE_MAX_BYTES && (same || !filemanager_path_exists(dst))) {
|
if (sz <= FILEOP_INLINE_MAX_BYTES && (same || !filemanager_path_exists(dst))) {
|
||||||
montauk::StatusCode status = montauk::Success;
|
montauk::StatusCode status = montauk::Success;
|
||||||
if (!(same && is_cut)) {
|
bool did = !(same && move);
|
||||||
if (is_cut) {
|
if (did) {
|
||||||
|
if (move) {
|
||||||
status = filemanager_status_from_result(montauk::frename(src, dst));
|
status = filemanager_status_from_result(montauk::frename(src, dst));
|
||||||
if (status != montauk::Success &&
|
if (status != montauk::Success &&
|
||||||
!filemanager_status_is_access_denied(status)) {
|
!filemanager_status_is_access_denied(status)) {
|
||||||
@@ -644,36 +713,57 @@ bool filemanager_start_paste_job(FileManagerState* fm) {
|
|||||||
status = filemanager_copy_file(src, dst);
|
status = filemanager_copy_file(src, dst);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (is_cut) fm->clipboard_count = 0;
|
|
||||||
filemanager_read_dir(fm);
|
filemanager_read_dir(fm);
|
||||||
|
|
||||||
if (status != montauk::Success) {
|
if (status != montauk::Success) {
|
||||||
char heading[192];
|
char heading[192];
|
||||||
snprintf(heading, sizeof(heading), "Couldn't %s \"%s\"",
|
snprintf(heading, sizeof(heading), "Couldn't %s \"%s\"",
|
||||||
fileop_error_verb(is_cut ? FILE_OP_MOVE : FILE_OP_COPY),
|
fileop_error_verb(move ? FILE_OP_MOVE : FILE_OP_COPY),
|
||||||
path_basename(src));
|
path_basename(src));
|
||||||
filemanager_show_error(fm->desktop, fm, heading, src, status);
|
filemanager_show_error(fm->desktop, fm, heading, src, status);
|
||||||
|
} else if (did) {
|
||||||
|
const char* to = dst;
|
||||||
|
filemanager_undo_record(fm, move ? FM_UNDO_MOVE : FM_UNDO_COPY, &src, &to, 1);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FileOpJob* job = fileop_alloc_job(is_cut ? FILE_OP_MOVE : FILE_OP_COPY,
|
FileOpJob* job = fileop_alloc_job(fm, move ? FILE_OP_MOVE : FILE_OP_COPY, count);
|
||||||
fm->clipboard_count);
|
|
||||||
if (!job) return false;
|
if (!job) return false;
|
||||||
for (int i = 0; i < fm->clipboard_count; i++) {
|
for (int i = 0; i < count; i++) montauk::strncpy(job->paths[i], paths[i], 256);
|
||||||
montauk::strncpy(job->paths[i], fm->clipboard_paths[i], 255);
|
montauk::strncpy(job->dest_dir, dest_dir, sizeof(job->dest_dir));
|
||||||
job->paths[i][255] = '\0';
|
return fileop_launch(job);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool filemanager_start_paste_job(FileManagerState* fm) {
|
||||||
|
if (!fm || fm->active_fileop) return false;
|
||||||
|
if (filemanager_is_computer_view(fm) || filemanager_is_virtual_view(fm)) return false;
|
||||||
|
if (filemanager_sync_clipboard(fm) <= 0) return false;
|
||||||
|
|
||||||
|
const char** paths = (const char**)montauk::malloc((uint64_t)fm->clipboard_count * sizeof(char*));
|
||||||
|
if (!paths) return false;
|
||||||
|
for (int i = 0; i < fm->clipboard_count; i++) paths[i] = fm->clipboard_paths[i];
|
||||||
|
bool move = fm->clipboard_is_cut;
|
||||||
|
bool started = filemanager_start_transfer(fm, move, paths, fm->clipboard_count, fm->current_path);
|
||||||
|
montauk::mfree(paths);
|
||||||
|
// A cut is consumed once its paste has started (the job copied the paths).
|
||||||
|
if (started) filemanager_consume_cut(fm);
|
||||||
|
return started;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool filemanager_start_pairs_job(FileManagerState* fm, bool move, const char* const* from,
|
||||||
|
const char* const* to, int count, bool record_undo) {
|
||||||
|
if (!fm || fm->active_fileop || count <= 0) return false;
|
||||||
|
FileOpJob* job = fileop_alloc_job(fm, move ? FILE_OP_MOVE : FILE_OP_COPY, count);
|
||||||
|
if (!job) return false;
|
||||||
|
job->dests = (char(*)[256])montauk::malloc((uint64_t)count * 256);
|
||||||
|
if (!job->dests) { fileop_free(job); return false; }
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
montauk::strncpy(job->paths[i], from[i], 256);
|
||||||
|
montauk::strncpy(job->dests[i], to[i], 256);
|
||||||
}
|
}
|
||||||
montauk::strncpy(job->dest_dir, fm->current_path, sizeof(job->dest_dir) - 1);
|
job->record_undo = record_undo;
|
||||||
job->dest_dir[sizeof(job->dest_dir) - 1] = '\0';
|
|
||||||
job->desktop = fm->desktop;
|
|
||||||
job->owner = fm;
|
|
||||||
job->accent = fm->desktop ? fm->desktop->settings.accent_color : Color{};
|
|
||||||
|
|
||||||
// A cut is consumed when the paste starts (paths are already copied in).
|
|
||||||
if (is_cut) fm->clipboard_count = 0;
|
|
||||||
|
|
||||||
return fileop_launch(job);
|
return fileop_launch(job);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -682,17 +772,13 @@ static bool fileop_start_path_job(FileManagerState* fm, int kind,
|
|||||||
if (!fm || fm->active_fileop) return false;
|
if (!fm || fm->active_fileop) return false;
|
||||||
if (!paths || count <= 0) return false;
|
if (!paths || count <= 0) return false;
|
||||||
|
|
||||||
FileOpJob* job = fileop_alloc_job(kind, count);
|
FileOpJob* job = fileop_alloc_job(fm, kind, count);
|
||||||
if (!job) return false;
|
if (!job) return false;
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
montauk::strncpy(job->paths[i], paths[i] ? paths[i] : "", 255);
|
montauk::strncpy(job->paths[i], paths[i] ? paths[i] : "", 255);
|
||||||
job->paths[i][255] = '\0';
|
job->paths[i][255] = '\0';
|
||||||
}
|
}
|
||||||
job->dest_dir[0] = '\0';
|
job->dest_dir[0] = '\0';
|
||||||
job->desktop = fm->desktop;
|
|
||||||
job->owner = fm;
|
|
||||||
job->accent = fm->desktop ? fm->desktop->settings.accent_color : Color{};
|
|
||||||
|
|
||||||
return fileop_launch(job);
|
return fileop_launch(job);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* files_host.hpp
|
||||||
|
* Files runs as its own process (0:/apps/files/files.elf). host.cpp gives
|
||||||
|
* the file manager the small slice of the desktop API it was written
|
||||||
|
* against (DesktopState, gui::Window, desktop_create_window, the app
|
||||||
|
* catalog) on top of Window Server windows; this header is the host
|
||||||
|
* services the file manager calls directly.
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <gui/desktop.hpp>
|
||||||
|
#include <Api/Syscall.hpp>
|
||||||
|
|
||||||
|
namespace files_host {
|
||||||
|
|
||||||
|
// Window Server id of the window a request should be attributed to: the
|
||||||
|
// main Files window (dialogs are placed over it).
|
||||||
|
int main_window_id();
|
||||||
|
|
||||||
|
// Ask the desktop to open a catalog item, so the app runs with the
|
||||||
|
// authority the desktop gives it. `args` reach it only if its manifest
|
||||||
|
// declares files.types (see WinReqLaunchItem). False if no window exists.
|
||||||
|
bool launch_item(const char* item_id, const char* args = nullptr);
|
||||||
|
|
||||||
|
// Find a catalog item by its binary path (0:/apps/<dir>/<binary>); -1 if
|
||||||
|
// the path is not an installed app.
|
||||||
|
int find_item_by_binary(gui::DesktopState* ds, const char* path);
|
||||||
|
|
||||||
|
// Called for Window Server events the gui::Window callbacks cannot carry
|
||||||
|
// (drag and drop: WIN_EVENT_DRAG_* / WIN_EVENT_DROP). Implemented by the
|
||||||
|
// file manager.
|
||||||
|
void on_window_event(gui::Window* win, const montauk::abi::WinEvent& ev);
|
||||||
|
|
||||||
|
} // namespace files_host
|
||||||
+29
-5
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* filesystem.cpp
|
* filesystem.cpp
|
||||||
* Filesystem enumeration and copy/delete helpers for the embedded desktop file manager
|
* Filesystem enumeration and copy/delete helpers for the Files file manager
|
||||||
* Copyright (c) 2026 Daniel Hammer
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -194,6 +194,7 @@ static void filemanager_begin_virtual_view(FileManagerState* fm, FileManagerVirt
|
|||||||
fm->current_path[0] = '\0';
|
fm->current_path[0] = '\0';
|
||||||
fm->virtual_view = view;
|
fm->virtual_view = view;
|
||||||
fm->entry_count = 0;
|
fm->entry_count = 0;
|
||||||
|
filemanager_thumbs_request(fm); // drops the previous folder's thumbnails
|
||||||
}
|
}
|
||||||
|
|
||||||
void filemanager_read_drives(FileManagerState* fm) {
|
void filemanager_read_drives(FileManagerState* fm) {
|
||||||
@@ -263,6 +264,7 @@ void filemanager_read_drives(FileManagerState* fm) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
filemanager_reset_list_state(fm);
|
filemanager_reset_list_state(fm);
|
||||||
|
filemanager_thumbs_request(fm);
|
||||||
}
|
}
|
||||||
|
|
||||||
void filemanager_read_dir(FileManagerState* fm) {
|
void filemanager_read_dir(FileManagerState* fm) {
|
||||||
@@ -275,6 +277,19 @@ void filemanager_read_dir(FileManagerState* fm) {
|
|||||||
fm->virtual_view = FM_VIRTUAL_VIEW_NONE;
|
fm->virtual_view = FM_VIRTUAL_VIEW_NONE;
|
||||||
fm->entry_count = 0;
|
fm->entry_count = 0;
|
||||||
|
|
||||||
|
// A folder the user set up a view for opens that way; others keep the
|
||||||
|
// window's current view.
|
||||||
|
bool grid = fm->grid_view;
|
||||||
|
FileManagerSortKey key = fm->sort_key;
|
||||||
|
bool desc = fm->sort_desc;
|
||||||
|
if (prefs_folder_view(fm->current_path, &grid, &key, &desc)) {
|
||||||
|
fm->grid_view = grid;
|
||||||
|
fm->sort_key = key;
|
||||||
|
fm->sort_desc = desc;
|
||||||
|
}
|
||||||
|
// The Trash shows everything it holds, dot-names included.
|
||||||
|
bool hide_dotfiles = !fm->show_hidden && !filemanager_in_trash(fm);
|
||||||
|
|
||||||
// Current drivers return direct-child names. Retain prefix stripping for
|
// Current drivers return direct-child names. Retain prefix stripping for
|
||||||
// compatibility with images built against the older ramdisk behavior.
|
// compatibility with images built against the older ramdisk behavior.
|
||||||
const char* after_drive = fm->current_path;
|
const char* after_drive = fm->current_path;
|
||||||
@@ -300,13 +315,16 @@ void filemanager_read_dir(FileManagerState* fm) {
|
|||||||
// removes the old fixed cap -- directories of any size are fully listed.
|
// removes the old fixed cap -- directories of any size are fully listed.
|
||||||
static constexpr int BATCH = 128;
|
static constexpr int BATCH = 128;
|
||||||
const char* names[BATCH];
|
const char* names[BATCH];
|
||||||
|
int read_offset = 0; // entries read from the directory (some may be hidden)
|
||||||
for (;;) {
|
for (;;) {
|
||||||
int got = montauk::readdir_at(fm->current_path, names, BATCH, fm->entry_count);
|
int got = montauk::readdir_at(fm->current_path, names, BATCH, read_offset);
|
||||||
if (got <= 0) break;
|
if (got <= 0) break;
|
||||||
|
read_offset += got;
|
||||||
if (!filemanager_ensure_entry_capacity(fm, fm->entry_count + got)) break;
|
if (!filemanager_ensure_entry_capacity(fm, fm->entry_count + got)) break;
|
||||||
|
|
||||||
|
int kept = 0;
|
||||||
for (int b = 0; b < got; b++) {
|
for (int b = 0; b < got; b++) {
|
||||||
int i = fm->entry_count + b;
|
int i = fm->entry_count + kept;
|
||||||
const char* raw = names[b];
|
const char* raw = names[b];
|
||||||
// Strip directory prefix if it matches
|
// Strip directory prefix if it matches
|
||||||
if (prefix_len > 0) {
|
if (prefix_len > 0) {
|
||||||
@@ -316,8 +334,10 @@ void filemanager_read_dir(FileManagerState* fm) {
|
|||||||
}
|
}
|
||||||
if (match) raw += prefix_len;
|
if (match) raw += prefix_len;
|
||||||
}
|
}
|
||||||
|
if (hide_dotfiles && raw[0] == '.') continue;
|
||||||
montauk::strncpy(fm->entry_names[i], raw, 128);
|
montauk::strncpy(fm->entry_names[i], raw, 128);
|
||||||
int len = montauk::slen(fm->entry_names[i]);
|
int len = montauk::slen(fm->entry_names[i]);
|
||||||
|
kept++;
|
||||||
|
|
||||||
// Detect directory
|
// Detect directory
|
||||||
if (len > 0 && fm->entry_names[i][len - 1] == '/') {
|
if (len > 0 && fm->entry_names[i][len - 1] == '/') {
|
||||||
@@ -348,12 +368,13 @@ void filemanager_read_dir(FileManagerState* fm) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fm->entry_count += got;
|
fm->entry_count += kept;
|
||||||
if (fm->entry_count >= FM_ENTRY_HARD_CAP) break;
|
if (fm->entry_count >= FM_ENTRY_HARD_CAP || read_offset >= FM_ENTRY_HARD_CAP * 2) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
filemanager_reset_list_state(fm);
|
filemanager_reset_list_state(fm);
|
||||||
filemanager_sort_entries(fm);
|
filemanager_sort_entries(fm);
|
||||||
|
filemanager_thumbs_request(fm);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Folders always lead; within each group the active column decides, with the
|
// Folders always lead; within each group the active column decides, with the
|
||||||
@@ -459,6 +480,9 @@ void filemanager_set_sort(FileManagerState* fm, FileManagerSortKey key) {
|
|||||||
fm->sort_desc = key == FM_SORT_SIZE || key == FM_SORT_MODIFIED;
|
fm->sort_desc = key == FM_SORT_SIZE || key == FM_SORT_MODIFIED;
|
||||||
}
|
}
|
||||||
filemanager_sort_entries(fm);
|
filemanager_sort_entries(fm);
|
||||||
|
filemanager_thumbs_request(fm); // entry indices moved
|
||||||
|
if (!filemanager_is_computer_view(fm) && !filemanager_is_virtual_view(fm))
|
||||||
|
prefs_set_folder_view(fm->desktop, fm->current_path, fm->grid_view, fm->sort_key, fm->sort_desc);
|
||||||
}
|
}
|
||||||
|
|
||||||
void filemanager_free_app_icons(FileManagerState* fm) {
|
void filemanager_free_app_icons(FileManagerState* fm) {
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
/*
|
||||||
|
* filetypes.cpp
|
||||||
|
* File type icons for Files: maps a file name to an icon-pack mimetype
|
||||||
|
* icon (audio, video, archives, source code, ...)
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "filemanager_internal.hpp"
|
||||||
|
|
||||||
|
namespace filemanager {
|
||||||
|
|
||||||
|
// One icon and the lowercased extensions (space separated) it stands for.
|
||||||
|
// The icons are staged into 0:/os/icons by scripts/copy_icons.sh; keep the
|
||||||
|
// two lists in step. A file whose extension is not listed gets the generic
|
||||||
|
// file icon.
|
||||||
|
struct FileTypeIcon {
|
||||||
|
const char* icon;
|
||||||
|
const char* exts;
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr FileTypeIcon kTypeIcons[] = {
|
||||||
|
// Media
|
||||||
|
{"audio-x-generic.svg", "mp3 wav flac ogg oga opus m4a aac wma aif aiff aifc ape mka au snd "
|
||||||
|
"mod xm it s3m voc"},
|
||||||
|
{"audio-midi.svg", "mid midi kar"},
|
||||||
|
{"audio-x-playlist.svg", "m3u m3u8 pls"},
|
||||||
|
{"video-x-generic.svg", "mp4 m4v mkv webm avi mov mpg mpeg wmv flv ogv 3gp vob"},
|
||||||
|
{"image-x-generic.svg", "png jpg jpeg bmp gif tga ico webp tif tiff ppm pgm pbm pnm qoi psd"},
|
||||||
|
{"image-svg+xml.svg", "svg"},
|
||||||
|
|
||||||
|
// Documents
|
||||||
|
{"application-pdf.svg", "pdf"},
|
||||||
|
{"x-office-document.svg", "mwp doc docx odt rtf"},
|
||||||
|
{"x-office-spreadsheet.svg","mss xls xlsx ods csv tsv"},
|
||||||
|
{"x-office-presentation.svg", "ppt pptx odp"},
|
||||||
|
{"text-markdown.svg", "md markdown"},
|
||||||
|
{"text-x-log.svg", "log"},
|
||||||
|
{"text-x-tex.svg", "tex bib"},
|
||||||
|
{"text-calendar.svg", "ics"},
|
||||||
|
{"text-x-vcard.svg", "vcf"},
|
||||||
|
{"font-x-generic.svg", "ttf otf woff woff2 fon pcf bdf psf"},
|
||||||
|
|
||||||
|
// Archives and images of disks
|
||||||
|
{"application-x-archive.svg", "zip tar gz tgz bz2 tbz tbz2 xz txz zst lz lzma 7z rar cpio a"},
|
||||||
|
{"application-x-deb.svg", "deb"},
|
||||||
|
{"application-x-cd-image.svg", "iso"},
|
||||||
|
{"application-x-raw-disk-image.svg", "img hdd raw vhd vmdk qcow2"},
|
||||||
|
|
||||||
|
// Programs and build output
|
||||||
|
{"application-x-object.svg", "o obj bin"},
|
||||||
|
{"application-x-sharedlib.svg", "so dll"},
|
||||||
|
|
||||||
|
// Source code and markup
|
||||||
|
{"text-x-csrc.svg", "c"},
|
||||||
|
{"text-x-chdr.svg", "h hpp hh hxx inc"},
|
||||||
|
{"text-x-c++src.svg", "cpp cc cxx"},
|
||||||
|
{"text-x-script.svg", "sh bash zsh"},
|
||||||
|
{"text-x-python.svg", "py"},
|
||||||
|
{"text-x-rust.svg", "rs"},
|
||||||
|
{"text-x-go.svg", "go"},
|
||||||
|
{"text-x-java.svg", "java"},
|
||||||
|
{"text-x-csharp.svg", "cs"},
|
||||||
|
{"text-x-lua.svg", "lua"},
|
||||||
|
{"text-x-ruby.svg", "rb"},
|
||||||
|
{"text-x-php.svg", "php"},
|
||||||
|
{"text-x-perl.svg", "pl pm"},
|
||||||
|
{"text-x-haskell.svg", "hs"},
|
||||||
|
{"text-x-typescript.svg", "ts tsx"},
|
||||||
|
{"application-javascript.svg", "js mjs cjs"},
|
||||||
|
{"text-html.svg", "html htm xhtml"},
|
||||||
|
{"text-css.svg", "css"},
|
||||||
|
{"application-json.svg", "json"},
|
||||||
|
{"text-xml.svg", "xml xsl"},
|
||||||
|
{"application-x-yaml.svg", "yaml yml"},
|
||||||
|
{"application-x-wine-extension-ini.svg", "ini cfg conf toml"},
|
||||||
|
{"text-x-sql.svg", "sql"},
|
||||||
|
{"application-x-sqlite3.svg", "db sqlite sqlite3"},
|
||||||
|
{"text-x-makefile.svg", "mk cmake"},
|
||||||
|
{"text-x-patch.svg", "patch diff"},
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr int kTypeIconCount = (int)(sizeof(kTypeIcons) / sizeof(kTypeIcons[0]));
|
||||||
|
|
||||||
|
// Whole file names (lowercased) with an icon of their own.
|
||||||
|
struct FileNameIcon {
|
||||||
|
const char* name;
|
||||||
|
const char* icon;
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr FileNameIcon kNameIcons[] = {
|
||||||
|
{"makefile", "text-x-makefile.svg"},
|
||||||
|
{"gnumakefile", "text-x-makefile.svg"},
|
||||||
|
{"cmakelists.txt", "text-x-makefile.svg"},
|
||||||
|
{"readme", "text-x-readme.svg"},
|
||||||
|
{"readme.txt", "text-x-readme.svg"},
|
||||||
|
{"license", "text-x-copying.svg"},
|
||||||
|
{"copying", "text-x-copying.svg"},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Loaded icons, keyed by icon file name: one slot per distinct icon.
|
||||||
|
static constexpr int kMaxLoaded = kTypeIconCount + 4;
|
||||||
|
|
||||||
|
struct LoadedTypeIcon {
|
||||||
|
const char* icon;
|
||||||
|
bool tried; // a failed load is not retried every frame
|
||||||
|
SvgIcon small;
|
||||||
|
SvgIcon large;
|
||||||
|
};
|
||||||
|
|
||||||
|
static LoadedTypeIcon g_loaded[kMaxLoaded];
|
||||||
|
static int g_loaded_count;
|
||||||
|
|
||||||
|
static const SvgIcon* load_type_icon(const char* icon, bool large) {
|
||||||
|
LoadedTypeIcon* slot = nullptr;
|
||||||
|
for (int i = 0; i < g_loaded_count; i++) {
|
||||||
|
if (montauk::streq(g_loaded[i].icon, icon)) { slot = &g_loaded[i]; break; }
|
||||||
|
}
|
||||||
|
if (!slot) {
|
||||||
|
if (g_loaded_count >= kMaxLoaded) return nullptr;
|
||||||
|
slot = &g_loaded[g_loaded_count++];
|
||||||
|
slot->icon = icon;
|
||||||
|
}
|
||||||
|
if (!slot->tried) {
|
||||||
|
slot->tried = true;
|
||||||
|
char path[128];
|
||||||
|
snprintf(path, sizeof(path), "0:/os/icons/%s", icon);
|
||||||
|
slot->small = svg_load(path, 16, 16, colors::ICON_COLOR);
|
||||||
|
slot->large = svg_load(path, FM_GRID_ICON, FM_GRID_ICON, colors::ICON_COLOR);
|
||||||
|
}
|
||||||
|
const SvgIcon* out = large ? &slot->large : &slot->small;
|
||||||
|
return out->pixels ? out : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is word (length n) one of the space-separated entries of list?
|
||||||
|
static bool list_has(const char* list, const char* word, int n) {
|
||||||
|
const char* p = list;
|
||||||
|
while (*p) {
|
||||||
|
while (*p == ' ') p++;
|
||||||
|
const char* start = p;
|
||||||
|
while (*p && *p != ' ') p++;
|
||||||
|
if (p - start != n) continue;
|
||||||
|
bool match = true;
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
if (start[i] != word[i]) { match = false; break; }
|
||||||
|
}
|
||||||
|
if (match) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool name_equals_ci(const char* name, const char* lower) {
|
||||||
|
for (; *name && *lower; name++, lower++) {
|
||||||
|
char ch = *name;
|
||||||
|
if (ch >= 'A' && ch <= 'Z') ch += 32;
|
||||||
|
if (ch != *lower) return false;
|
||||||
|
}
|
||||||
|
return *name == '\0' && *lower == '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
const SvgIcon* filemanager_file_type_icon(const char* name, bool large) {
|
||||||
|
if (!name || !name[0]) return nullptr;
|
||||||
|
|
||||||
|
for (const FileNameIcon& n : kNameIcons) {
|
||||||
|
if (name_equals_ci(name, n.name)) return load_type_icon(n.icon, large);
|
||||||
|
}
|
||||||
|
|
||||||
|
char ext[16];
|
||||||
|
file_extension(name, ext, sizeof(ext));
|
||||||
|
int len = montauk::slen(ext);
|
||||||
|
if (len == 0 || len >= (int)sizeof(ext) - 1) return nullptr; // none, or cut short
|
||||||
|
for (int i = 0; i < kTypeIconCount; i++) {
|
||||||
|
if (list_has(kTypeIcons[i].exts, ext, len)) return load_type_icon(kTypeIcons[i].icon, large);
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace filemanager
|
||||||
+31
-19
@@ -1,10 +1,11 @@
|
|||||||
/*
|
/*
|
||||||
* helpers.cpp
|
* helpers.cpp
|
||||||
* Shared helpers for the embedded desktop file manager
|
* Shared helpers for the Files file manager
|
||||||
* Copyright (c) 2026 Daniel Hammer
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "filemanager_internal.hpp"
|
#include "filemanager_internal.hpp"
|
||||||
|
#include <gui/app_catalog.hpp>
|
||||||
|
|
||||||
namespace filemanager {
|
namespace filemanager {
|
||||||
|
|
||||||
@@ -140,7 +141,23 @@ const char* ctx_label(int action) {
|
|||||||
case CTX_TRASH: return "Move to Trash";
|
case CTX_TRASH: return "Move to Trash";
|
||||||
case CTX_RESTORE: return "Restore";
|
case CTX_RESTORE: return "Restore";
|
||||||
case CTX_EMPTY_TRASH: return "Empty Trash";
|
case CTX_EMPTY_TRASH: return "Empty Trash";
|
||||||
default: return "?";
|
case CTX_TOGGLE_HIDDEN: return "Show Hidden Files";
|
||||||
|
case CTX_ADD_BOOKMARK: return "Add to Sidebar";
|
||||||
|
case CTX_REMOVE_BOOKMARK: return "Remove from Sidebar";
|
||||||
|
case CTX_UNDO: return "Undo";
|
||||||
|
case CTX_REDO: return "Redo";
|
||||||
|
case CTX_NEW: return "New";
|
||||||
|
default:
|
||||||
|
return action >= CTX_NEW_FILE_BASE ? filemanager_new_file_label(action) : "?";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* ctx_label_for(const FileManagerState* fm, int action) {
|
||||||
|
switch (action) {
|
||||||
|
case CTX_TOGGLE_HIDDEN: return fm->show_hidden ? "Hide Hidden Files" : "Show Hidden Files";
|
||||||
|
case CTX_UNDO: return filemanager_undo_label(fm);
|
||||||
|
case CTX_REDO: return filemanager_redo_label(fm);
|
||||||
|
default: return ctx_label(action);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,9 +185,13 @@ const char* ctx_shortcut(const FileManagerState* fm, int action) {
|
|||||||
// trashed (inside the Trash, other volumes).
|
// trashed (inside the Trash, other volumes).
|
||||||
case CTX_DELETE: return filemanager_can_trash(fm) ? "Shift+Del" : "Del";
|
case CTX_DELETE: return filemanager_can_trash(fm) ? "Shift+Del" : "Del";
|
||||||
case CTX_NEW_FOLDER: return "Ctrl+Shift+N";
|
case CTX_NEW_FOLDER: return "Ctrl+Shift+N";
|
||||||
|
case CTX_NEW: return ">";
|
||||||
case CTX_PROPERTIES: return "Alt+Enter";
|
case CTX_PROPERTIES: return "Alt+Enter";
|
||||||
case CTX_REFRESH: return "F5";
|
case CTX_REFRESH: return "F5";
|
||||||
case CTX_SELECT_ALL: return "Ctrl+A";
|
case CTX_SELECT_ALL: return "Ctrl+A";
|
||||||
|
case CTX_TOGGLE_HIDDEN: return "Ctrl+H";
|
||||||
|
case CTX_UNDO: return "Ctrl+Z";
|
||||||
|
case CTX_REDO: return "Ctrl+Y";
|
||||||
default: return nullptr;
|
default: return nullptr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -187,8 +208,14 @@ int ctx_group(int action) {
|
|||||||
case CTX_TRASH:
|
case CTX_TRASH:
|
||||||
case CTX_DELETE:
|
case CTX_DELETE:
|
||||||
case CTX_EMPTY_TRASH:
|
case CTX_EMPTY_TRASH:
|
||||||
|
case CTX_NEW:
|
||||||
case CTX_NEW_FOLDER: return 2;
|
case CTX_NEW_FOLDER: return 2;
|
||||||
case CTX_SELECT_ALL: return 3;
|
case CTX_UNDO:
|
||||||
|
case CTX_REDO: return 7;
|
||||||
|
case CTX_ADD_BOOKMARK:
|
||||||
|
case CTX_REMOVE_BOOKMARK: return 8;
|
||||||
|
case CTX_SELECT_ALL:
|
||||||
|
case CTX_TOGGLE_HIDDEN: return 3;
|
||||||
case CTX_SORT_NAME:
|
case CTX_SORT_NAME:
|
||||||
case CTX_SORT_SIZE:
|
case CTX_SORT_SIZE:
|
||||||
case CTX_SORT_TYPE:
|
case CTX_SORT_TYPE:
|
||||||
@@ -219,22 +246,7 @@ int detect_file_type(const char* name, bool is_dir) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void file_extension(const char* name, char* out, int out_max) {
|
void file_extension(const char* name, char* out, int out_max) {
|
||||||
if (!out || out_max <= 0) return;
|
gui::app_catalog_extension(name, out, out_max);
|
||||||
out[0] = '\0';
|
|
||||||
if (!name) return;
|
|
||||||
|
|
||||||
const char* dot = nullptr;
|
|
||||||
for (const char* p = name; *p; p++) {
|
|
||||||
if (*p == '.') dot = p;
|
|
||||||
}
|
|
||||||
if (!dot || dot == name || !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';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void filemanager_type_label(const FileManagerState* fm, int idx, char* out, int out_max) {
|
void filemanager_type_label(const FileManagerState* fm, int idx, char* out, int out_max) {
|
||||||
@@ -0,0 +1,439 @@
|
|||||||
|
/*
|
||||||
|
* host.cpp
|
||||||
|
* Process host for Files: the desktop window and catalog API the file
|
||||||
|
* manager uses, implemented over Window Server windows, plus the event loop
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "filemanager_internal.hpp"
|
||||||
|
#include "files_host.hpp"
|
||||||
|
#include <gui/app_catalog.hpp>
|
||||||
|
#include <gui/transfer.hpp>
|
||||||
|
#include <gui/mtk/settings.hpp>
|
||||||
|
#include <montauk/capabilities.h>
|
||||||
|
#include <montauk/user.h>
|
||||||
|
|
||||||
|
using namespace gui;
|
||||||
|
using namespace filemanager;
|
||||||
|
|
||||||
|
// Every gui::Window here is backed by one Window Server window: ext_win_id
|
||||||
|
// holds its id and `content` its pixel buffer. The frame is placed so that
|
||||||
|
// content_rect() is (0, 0, w, h) -- the window server reports mouse
|
||||||
|
// positions relative to the content already -- which lets the file manager
|
||||||
|
// keep its desktop-style coordinate handling unchanged.
|
||||||
|
|
||||||
|
static DesktopState* g_ds = nullptr;
|
||||||
|
static int g_main_window_id = -1;
|
||||||
|
|
||||||
|
static void host_set_frame(Window* win, int content_w, int content_h) {
|
||||||
|
win->frame = {-BORDER_WIDTH, -TITLEBAR_HEIGHT,
|
||||||
|
content_w + 2 * BORDER_WIDTH, content_h + TITLEBAR_HEIGHT + BORDER_WIDTH};
|
||||||
|
win->content_w = content_w;
|
||||||
|
win->content_h = content_h;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int host_find_window(DesktopState* ds, int ws_id) {
|
||||||
|
for (int i = 0; i < ds->window_count; i++) {
|
||||||
|
if (ds->windows[i].ext_win_id == ws_id) return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Desktop window API
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// x/y are ignored: the desktop places new windows, and dialogs ask to be
|
||||||
|
// centred over the main window (WIN_REQ_SET_PARENT). w/h are frame sizes,
|
||||||
|
// as on the desktop, so the content is what the desktop would have given.
|
||||||
|
int gui::desktop_create_window(DesktopState* ds, const char* title, int x, int y, int w, int h) {
|
||||||
|
(void)x;
|
||||||
|
(void)y;
|
||||||
|
if (ds->window_count >= MAX_WINDOWS) return -1;
|
||||||
|
|
||||||
|
int content_w = w - 2 * BORDER_WIDTH;
|
||||||
|
int content_h = h - TITLEBAR_HEIGHT - BORDER_WIDTH;
|
||||||
|
if (content_w < 1 || content_h < 1) return -1;
|
||||||
|
|
||||||
|
montauk::abi::WinCreateResult res;
|
||||||
|
if (montauk::win_create(title, content_w, content_h, &res) < 0 || res.id < 0) return -1;
|
||||||
|
|
||||||
|
int idx = ds->window_count;
|
||||||
|
Window* win = &ds->windows[idx];
|
||||||
|
montauk::memset(win, 0, sizeof(Window));
|
||||||
|
montauk::strncpy(win->title, title, MAX_TITLE_LEN);
|
||||||
|
host_set_frame(win, content_w, content_h);
|
||||||
|
win->content = (uint32_t*)(uintptr_t)res.pixelVa;
|
||||||
|
win->ext_win_id = res.id;
|
||||||
|
win->state = WIN_NORMAL;
|
||||||
|
win->focused = true;
|
||||||
|
win->dirty = true;
|
||||||
|
ds->window_count++;
|
||||||
|
ds->focused_window = idx;
|
||||||
|
|
||||||
|
if (g_main_window_id < 0) {
|
||||||
|
g_main_window_id = res.id;
|
||||||
|
} else {
|
||||||
|
gui::win_request_set_parent(res.id, g_main_window_id);
|
||||||
|
}
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
void gui::desktop_close_window(DesktopState* ds, int idx) {
|
||||||
|
if (idx < 0 || idx >= ds->window_count) return;
|
||||||
|
Window* win = &ds->windows[idx];
|
||||||
|
|
||||||
|
// on_close runs first and may open another window (an error popup); that
|
||||||
|
// one is appended after us and moves down with the shift below.
|
||||||
|
if (win->on_close) win->on_close(win);
|
||||||
|
|
||||||
|
int ws_id = win->ext_win_id;
|
||||||
|
montauk::win_destroy(ws_id);
|
||||||
|
if (ws_id == g_main_window_id) g_main_window_id = -1;
|
||||||
|
|
||||||
|
for (int i = idx; i < ds->window_count - 1; i++) ds->windows[i] = ds->windows[i + 1];
|
||||||
|
ds->window_count--;
|
||||||
|
if (ds->focused_window >= ds->window_count) ds->focused_window = ds->window_count - 1;
|
||||||
|
|
||||||
|
// Requests need a window to be attributed to; fall back to any left.
|
||||||
|
if (g_main_window_id < 0 && ds->window_count > 0) g_main_window_id = ds->windows[0].ext_win_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
void gui::desktop_raise_window(DesktopState* ds, int idx) {
|
||||||
|
if (idx < 0 || idx >= ds->window_count) return;
|
||||||
|
gui::win_request_activate(ds->windows[idx].ext_win_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Launching and the app catalog
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Files' own authority: what capabilities.toml grants files.elf, bounded by
|
||||||
|
// the desktop. Used for documents opened in apps that do not declare file
|
||||||
|
// types, and for executables that are not installed apps.
|
||||||
|
int desktop_spawn_app(DesktopState* ds, const char* path, const char* args) {
|
||||||
|
(void)ds;
|
||||||
|
if (!path || !path[0]) return -1;
|
||||||
|
return montauk::caps::launch(path, {.args = args});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool desktop_launch_item(DesktopState* ds, const DesktopItem* item) {
|
||||||
|
if (!ds || !item) return false;
|
||||||
|
if (item->admin_only && !ds->is_admin) return false;
|
||||||
|
return files_host::launch_item(item->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
int desktop_list_item_indices(DesktopState* ds, DesktopItemSection section,
|
||||||
|
int* out, int max_entries) {
|
||||||
|
return gui::app_catalog_list_indices(ds, section, out, max_entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
int files_host::main_window_id() {
|
||||||
|
return g_main_window_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool files_host::launch_item(const char* item_id, const char* args) {
|
||||||
|
if (g_main_window_id < 0 || !item_id || !item_id[0]) return false;
|
||||||
|
return gui::win_request_launch_item(g_main_window_id, item_id, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
int files_host::find_item_by_binary(DesktopState* ds, const char* path) {
|
||||||
|
if (!ds || !path) return -1;
|
||||||
|
for (int i = 0; i < ds->desktop_item_count; i++) {
|
||||||
|
const DesktopItem& item = ds->desktop_items[i];
|
||||||
|
if (item.launch_kind == DESKTOP_ITEM_LAUNCH_EXECUTABLE && item.binary_path[0] &&
|
||||||
|
str_compare_ci(item.binary_path, path) == 0)
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings pages the desktop draws itself, then every installed app.
|
||||||
|
static void host_load_catalog(DesktopState* ds) {
|
||||||
|
Color def = colors::ICON_COLOR;
|
||||||
|
for (const CatalogBuiltinPage* page : kCatalogBuiltinPages) {
|
||||||
|
DesktopItem* item = app_catalog_next_slot(ds);
|
||||||
|
if (!item) return;
|
||||||
|
montauk::memset(item, 0, sizeof(*item));
|
||||||
|
montauk::strncpy(item->id, page->id, sizeof(item->id));
|
||||||
|
montauk::strncpy(item->name, page->label, sizeof(item->name));
|
||||||
|
montauk::strncpy(item->icon_path, page->icon_path, sizeof(item->icon_path));
|
||||||
|
item->icon = svg_load(page->icon_path, 20, 20, def);
|
||||||
|
item->section = DESKTOP_ITEM_SECTION_SETTINGS;
|
||||||
|
item->launch_kind = DESKTOP_ITEM_LAUNCH_BUILTIN;
|
||||||
|
item->builtin_id = -1;
|
||||||
|
ds->desktop_item_count++;
|
||||||
|
}
|
||||||
|
app_catalog_add_manifest_apps(ds);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void host_load_identity(DesktopState* ds) {
|
||||||
|
char user[64] = {};
|
||||||
|
if (montauk::getuser(user, sizeof(user)) <= 0 || !user[0]) montauk::strcpy(user, "default");
|
||||||
|
montauk::strncpy(ds->current_user, user, sizeof(ds->current_user));
|
||||||
|
montauk::user::home_dir(user, ds->home_dir, sizeof(ds->home_dir));
|
||||||
|
montauk::user::config_dir(user, ds->user_config_dir, sizeof(ds->user_config_dir));
|
||||||
|
|
||||||
|
// UI affordance only (admin_only catalog items); the kernel enforces
|
||||||
|
// what this process may actually do.
|
||||||
|
montauk::user::UserInfo users[16];
|
||||||
|
int count = montauk::user::load_users(users, 16);
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
if (montauk::streq(users[i].username, user)) {
|
||||||
|
ds->is_admin = montauk::streq(users[i].role, "admin");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void host_load_icons(DesktopState* ds) {
|
||||||
|
Color def = colors::ICON_COLOR;
|
||||||
|
ds->icon_folder = svg_load("0:/os/icons/folder.svg", 16, 16, def);
|
||||||
|
ds->icon_file = svg_load("0:/os/icons/text-x-generic.svg", 16, 16, def);
|
||||||
|
ds->icon_exec = svg_load("0:/os/icons/utilities-terminal.svg", 16, 16, def);
|
||||||
|
ds->icon_go_up = svg_load("0:/os/icons/go-up-symbolic.svg", 16, 16, def);
|
||||||
|
ds->icon_go_back = svg_load("0:/os/icons/go-previous-symbolic.svg", 16, 16, def);
|
||||||
|
ds->icon_go_forward = svg_load("0:/os/icons/go-next-symbolic.svg", 16, 16, def);
|
||||||
|
ds->icon_home = svg_load("0:/os/icons/user-home.svg", 16, 16, def);
|
||||||
|
ds->icon_refresh = svg_load("0:/os/icons/view-refresh-symbolic.svg", 16, 16, def);
|
||||||
|
ds->icon_folder_lg = svg_load("0:/os/icons/folder.svg", 48, 48, def);
|
||||||
|
ds->icon_file_lg = svg_load("0:/os/icons/text-x-generic.svg", 48, 48, def);
|
||||||
|
ds->icon_exec_lg = svg_load("0:/os/icons/utilities-terminal.svg", 48, 48, def);
|
||||||
|
ensure_filemanager_icons_loaded(ds);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Event loop
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
static void host_track_modifiers(DesktopState* ds, const montauk::abi::KeyEvent& key) {
|
||||||
|
ds->kbd_shift_down = key.shift;
|
||||||
|
ds->kbd_ctrl_down = key.ctrl;
|
||||||
|
ds->kbd_alt_down = key.alt;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void host_track_mouse_modifiers(DesktopState* ds, uint8_t mods) {
|
||||||
|
ds->kbd_shift_down = (mods & montauk::abi::WIN_MOD_SHIFT) != 0;
|
||||||
|
ds->kbd_ctrl_down = (mods & montauk::abi::WIN_MOD_CTRL) != 0;
|
||||||
|
ds->kbd_alt_down = (mods & montauk::abi::WIN_MOD_ALT) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deliver one event to the window with Window Server id `ws_id`. Returns
|
||||||
|
// false once that window is gone (closed by the event).
|
||||||
|
static bool host_dispatch(DesktopState* ds, int ws_id, const montauk::abi::WinEvent& ev) {
|
||||||
|
int idx = host_find_window(ds, ws_id);
|
||||||
|
if (idx < 0) return false;
|
||||||
|
Window* win = &ds->windows[idx];
|
||||||
|
|
||||||
|
switch (ev.type) {
|
||||||
|
case montauk::abi::WIN_EVENT_KEY:
|
||||||
|
host_track_modifiers(ds, ev.key);
|
||||||
|
ds->focused_window = idx;
|
||||||
|
if (win->on_key) win->on_key(win, ev.key);
|
||||||
|
break;
|
||||||
|
case montauk::abi::WIN_EVENT_MOUSE: {
|
||||||
|
host_track_mouse_modifiers(ds, ev.mouse.modifiers);
|
||||||
|
MouseEvent me;
|
||||||
|
me.x = ev.mouse.x;
|
||||||
|
me.y = ev.mouse.y;
|
||||||
|
me.buttons = ev.mouse.buttons;
|
||||||
|
me.prev_buttons = ev.mouse.prev_buttons;
|
||||||
|
me.scroll = ev.mouse.scroll;
|
||||||
|
if (me.left_pressed() || me.right_pressed()) ds->focused_window = idx;
|
||||||
|
if (win->on_mouse) win->on_mouse(win, me);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case montauk::abi::WIN_EVENT_RESIZE: {
|
||||||
|
int w = ev.resize.w;
|
||||||
|
int h = ev.resize.h;
|
||||||
|
if (w < 1 || h < 1) break;
|
||||||
|
uint64_t va = montauk::win_resize(ws_id, w, h);
|
||||||
|
if (va == 0) break;
|
||||||
|
win->content = (uint32_t*)(uintptr_t)va;
|
||||||
|
host_set_frame(win, w, h);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case montauk::abi::WIN_EVENT_CLOSE:
|
||||||
|
desktop_close_window(ds, idx);
|
||||||
|
return false;
|
||||||
|
case montauk::abi::WIN_EVENT_DRAG_ENTER:
|
||||||
|
case montauk::abi::WIN_EVENT_DRAG_OVER:
|
||||||
|
case montauk::abi::WIN_EVENT_DRAG_LEAVE:
|
||||||
|
case montauk::abi::WIN_EVENT_DROP:
|
||||||
|
case montauk::abi::WIN_EVENT_DRAG_END:
|
||||||
|
host_track_mouse_modifiers(ds, ev.drag.modifiers);
|
||||||
|
files_host::on_window_event(win, ev);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return host_find_window(ds, ws_id) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void host_redraw(DesktopState* ds, bool all) {
|
||||||
|
for (int i = 0; i < ds->window_count; i++) {
|
||||||
|
Window* win = &ds->windows[i];
|
||||||
|
if (!all && !win->dirty) continue;
|
||||||
|
if (win->on_draw) win->on_draw(win, ds->fb);
|
||||||
|
win->dirty = false;
|
||||||
|
montauk::win_present(win->ext_win_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void host_run(DesktopState* ds) {
|
||||||
|
while (ds->window_count > 0) {
|
||||||
|
bool activity = false;
|
||||||
|
|
||||||
|
// Snapshot the ids: handlers may open and close windows.
|
||||||
|
int ids[MAX_WINDOWS];
|
||||||
|
int n = ds->window_count;
|
||||||
|
for (int i = 0; i < n; i++) ids[i] = ds->windows[i].ext_win_id;
|
||||||
|
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
montauk::abi::WinEvent ev;
|
||||||
|
// Bounded per window per pass so one busy window cannot starve
|
||||||
|
// the others (or the redraw).
|
||||||
|
for (int k = 0; k < 64; k++) {
|
||||||
|
if (host_find_window(ds, ids[i]) < 0) break;
|
||||||
|
if (montauk::win_poll(ids[i], &ev) <= 0) break;
|
||||||
|
activity = true;
|
||||||
|
if (!host_dispatch(ds, ids[i], ev)) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < ds->window_count; i++) {
|
||||||
|
Window* win = &ds->windows[i];
|
||||||
|
if (win->on_poll) {
|
||||||
|
int id = win->ext_win_id;
|
||||||
|
win->on_poll(win);
|
||||||
|
// on_poll may close its own window.
|
||||||
|
if (host_find_window(ds, id) < 0) { i--; activity = true; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// State changed by one window (a paste, a confirmed delete) often
|
||||||
|
// shows in another, so any input repaints them all.
|
||||||
|
host_redraw(ds, activity);
|
||||||
|
if (!activity) montauk::sleep_ms(10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A file manager state with no window of its own, for the modes where Files
|
||||||
|
// only shows a dialog (it reuses the same jobs and dialogs).
|
||||||
|
static FileManagerState* host_headless_state(DesktopState* ds, const char* dir) {
|
||||||
|
auto* fm = (FileManagerState*)montauk::malloc(sizeof(FileManagerState));
|
||||||
|
if (!fm) return nullptr;
|
||||||
|
montauk::memset(fm, 0, sizeof(FileManagerState));
|
||||||
|
fm->desktop = ds;
|
||||||
|
fm->headless = true;
|
||||||
|
fm->selected = -1;
|
||||||
|
fm->sel_anchor = -1;
|
||||||
|
fm->press_idx = -1;
|
||||||
|
fm->drop_target_idx = -1;
|
||||||
|
fm->drop_sidebar_idx = -1;
|
||||||
|
fm->sidebar_hover = -1;
|
||||||
|
montauk::strncpy(fm->current_path, dir, sizeof(fm->current_path));
|
||||||
|
return fm;
|
||||||
|
}
|
||||||
|
|
||||||
|
// files.elf --transfer <list file>: copy or move files on someone else's
|
||||||
|
// behalf (the desktop, for drops onto it), with the usual progress dialog
|
||||||
|
// and conflict prompts. The list file (deleted once read) holds
|
||||||
|
// "copy"/"move", the destination folder, then one source path per line.
|
||||||
|
static void host_transfer(DesktopState* ds, const char* list_path) {
|
||||||
|
int fd = montauk::open(list_path);
|
||||||
|
if (fd < 0) return;
|
||||||
|
uint64_t size = montauk::getsize(fd);
|
||||||
|
char* text = (size > 0 && size < (1u << 20)) ? (char*)montauk::malloc(size + 1) : nullptr;
|
||||||
|
if (text && montauk::read(fd, (uint8_t*)text, 0, size) != (int)size) {
|
||||||
|
montauk::mfree(text);
|
||||||
|
text = nullptr;
|
||||||
|
}
|
||||||
|
montauk::close(fd);
|
||||||
|
montauk::fdelete(list_path);
|
||||||
|
if (!text) return;
|
||||||
|
text[size] = '\0';
|
||||||
|
|
||||||
|
char* lines[4096];
|
||||||
|
int n = 0;
|
||||||
|
char* p = text;
|
||||||
|
while (*p && n < 4096) {
|
||||||
|
char* line = p;
|
||||||
|
while (*p && *p != '\n') p++;
|
||||||
|
if (*p) *p++ = '\0';
|
||||||
|
if (line[0]) lines[n++] = line;
|
||||||
|
}
|
||||||
|
if (n >= 3 && (montauk::streq(lines[0], "copy") || montauk::streq(lines[0], "move"))) {
|
||||||
|
FileManagerState* fm = host_headless_state(ds, lines[1]);
|
||||||
|
if (fm) {
|
||||||
|
filemanager_start_transfer(fm, montauk::streq(lines[0], "move"),
|
||||||
|
(const char* const*)(lines + 2), n - 2, lines[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
montauk::mfree(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// files.elf --open-with <path>: just the "Open With" chooser.
|
||||||
|
static void host_open_with(DesktopState* ds, const char* path) {
|
||||||
|
char dir[256];
|
||||||
|
montauk::strncpy(dir, path, sizeof(dir));
|
||||||
|
int cut = montauk::slen(dir);
|
||||||
|
while (cut > 0 && dir[cut - 1] != '/') cut--;
|
||||||
|
if (cut <= 0) return;
|
||||||
|
dir[cut > 3 && dir[cut - 2] != ':' ? cut - 1 : cut] = '\0';
|
||||||
|
|
||||||
|
FileManagerState* fm = host_headless_state(ds, dir);
|
||||||
|
if (!fm) return;
|
||||||
|
filemanager_read_dir(fm);
|
||||||
|
const char* name = path_basename(path);
|
||||||
|
for (int i = 0; i < fm->entry_count; i++) {
|
||||||
|
if (montauk::streq(fm->entry_names[i], name)) {
|
||||||
|
filemanager_show_open_with(fm, i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool starts_with(const char* s, const char* prefix) {
|
||||||
|
while (*prefix) {
|
||||||
|
if (*s++ != *prefix++) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" void _start() {
|
||||||
|
// Argument: a folder path, virtual://apps or virtual://settings, or one
|
||||||
|
// of the dialog-only modes (--transfer, --open-with).
|
||||||
|
char path[512] = {};
|
||||||
|
montauk::getargs(path, sizeof(path));
|
||||||
|
|
||||||
|
if (!fonts::init()) montauk::exit(1);
|
||||||
|
|
||||||
|
g_ds = (DesktopState*)montauk::malloc(sizeof(DesktopState));
|
||||||
|
if (!g_ds) montauk::exit(1);
|
||||||
|
montauk::memset(g_ds, 0, sizeof(DesktopState));
|
||||||
|
g_ds->focused_window = -1;
|
||||||
|
// Only used to keep dialogs on screen; the desktop does the placing.
|
||||||
|
g_ds->screen_w = 1 << 14;
|
||||||
|
g_ds->screen_h = 1 << 14;
|
||||||
|
g_ds->settings.accent_color = mtk::load_system_accent();
|
||||||
|
|
||||||
|
host_load_identity(g_ds);
|
||||||
|
host_load_icons(g_ds);
|
||||||
|
host_load_catalog(g_ds);
|
||||||
|
|
||||||
|
if (starts_with(path, "--transfer ")) {
|
||||||
|
host_transfer(g_ds, path + 11);
|
||||||
|
// A small copy may have finished inline, without any window.
|
||||||
|
if (g_ds->window_count == 0) montauk::exit(0);
|
||||||
|
} else if (starts_with(path, "--open-with ")) {
|
||||||
|
host_open_with(g_ds, path + 12);
|
||||||
|
if (g_ds->window_count == 0) montauk::exit(1);
|
||||||
|
} else {
|
||||||
|
open_filemanager_path(g_ds, path[0] ? path : nullptr);
|
||||||
|
if (g_ds->window_count == 0) montauk::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
host_run(g_ds);
|
||||||
|
montauk::exit(0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[app]
|
||||||
|
name = "Files"
|
||||||
|
binary = "files.elf"
|
||||||
|
icon = "system-file-manager.svg"
|
||||||
|
|
||||||
|
# The desktop lists Files itself (a built-in launcher entry that starts this
|
||||||
|
# binary), so the manifest stays out of the menus.
|
||||||
|
[menu]
|
||||||
|
category = "Utilities"
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
[desktop]
|
||||||
|
section = "hidden"
|
||||||
|
|
||||||
|
[launch]
|
||||||
|
pass_home_dir = false
|
||||||
+28
-13
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* navigation.cpp
|
* navigation.cpp
|
||||||
* Navigation and file opening logic for the embedded desktop file manager
|
* Navigation and file opening logic for the Files file manager
|
||||||
* Copyright (c) 2026 Daniel Hammer
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -134,6 +134,17 @@ void filemanager_go_forward(FileManagerState* fm) {
|
|||||||
filemanager_navigate_to_history(fm);
|
filemanager_navigate_to_history(fm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void filemanager_go_to_path(FileManagerState* fm, const char* path) {
|
||||||
|
if (!fm) return;
|
||||||
|
if (!path || !path[0]) {
|
||||||
|
filemanager_go_home(fm);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (path != fm->current_path) montauk::strncpy(fm->current_path, path, sizeof(fm->current_path));
|
||||||
|
filemanager_read_dir(fm);
|
||||||
|
filemanager_push_history(fm);
|
||||||
|
}
|
||||||
|
|
||||||
void filemanager_open_trash(FileManagerState* fm) {
|
void filemanager_open_trash(FileManagerState* fm) {
|
||||||
if (!fm || !filemanager_trash_ensure(fm->desktop)) return;
|
if (!fm || !filemanager_trash_ensure(fm->desktop)) return;
|
||||||
if (!filemanager_trash_files_dir(fm->desktop, fm->current_path, sizeof(fm->current_path)))
|
if (!filemanager_trash_files_dir(fm->desktop, fm->current_path, sizeof(fm->current_path)))
|
||||||
@@ -149,6 +160,8 @@ void filemanager_go_home(FileManagerState* fm) {
|
|||||||
|
|
||||||
void filemanager_refresh(FileManagerState* fm) {
|
void filemanager_refresh(FileManagerState* fm) {
|
||||||
if (!fm) return;
|
if (!fm) return;
|
||||||
|
// Drives and bookmarks may have come and gone.
|
||||||
|
filemanager_sidebar_rebuild(fm);
|
||||||
if (filemanager_is_virtual_view(fm)) {
|
if (filemanager_is_virtual_view(fm)) {
|
||||||
filemanager_read_virtual_view(fm, fm->virtual_view);
|
filemanager_read_virtual_view(fm, fm->virtual_view);
|
||||||
} else if (filemanager_is_computer_view(fm)) {
|
} else if (filemanager_is_computer_view(fm)) {
|
||||||
@@ -160,14 +173,10 @@ void filemanager_refresh(FileManagerState* fm) {
|
|||||||
|
|
||||||
void filemanager_start_pathbar(FileManagerState* fm) {
|
void filemanager_start_pathbar(FileManagerState* fm) {
|
||||||
fm->pathbar_editing = true;
|
fm->pathbar_editing = true;
|
||||||
if (filemanager_is_computer_view(fm) || filemanager_is_virtual_view(fm)) {
|
const char* path = filemanager_is_computer_view(fm) || filemanager_is_virtual_view(fm)
|
||||||
fm->pathbar_buf[0] = '\0';
|
? "" : fm->current_path;
|
||||||
fm->pathbar_len = 0;
|
// All selected, so typing replaces the path and the arrows keep it.
|
||||||
} else {
|
filemanager_field_set(fm->pathbar, path, sizeof(fm->pathbar.buf), 8, 0, montauk::slen(path));
|
||||||
montauk::strcpy(fm->pathbar_buf, fm->current_path);
|
|
||||||
fm->pathbar_len = montauk::slen(fm->pathbar_buf);
|
|
||||||
}
|
|
||||||
fm->pathbar_cursor = fm->pathbar_len;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void filemanager_cancel_pathbar(FileManagerState* fm) {
|
void filemanager_cancel_pathbar(FileManagerState* fm) {
|
||||||
@@ -188,7 +197,7 @@ static bool filemanager_has_volume_prefix(const char* path) {
|
|||||||
// full sub-path. Qualify it here so the prefix stripping matches. The drive of
|
// full sub-path. Qualify it here so the prefix stripping matches. The drive of
|
||||||
// the current location is preferred, falling back to drive 0.
|
// the current location is preferred, falling back to drive 0.
|
||||||
static void filemanager_qualify_path(FileManagerState* fm, char* out, int out_max) {
|
static void filemanager_qualify_path(FileManagerState* fm, char* out, int out_max) {
|
||||||
const char* in = fm->pathbar_buf;
|
const char* in = fm->pathbar.buf;
|
||||||
if (filemanager_has_volume_prefix(in)) {
|
if (filemanager_has_volume_prefix(in)) {
|
||||||
montauk::strcpy(out, in);
|
montauk::strcpy(out, in);
|
||||||
return;
|
return;
|
||||||
@@ -212,7 +221,7 @@ static void filemanager_qualify_path(FileManagerState* fm, char* out, int out_ma
|
|||||||
|
|
||||||
void filemanager_commit_pathbar(FileManagerState* fm) {
|
void filemanager_commit_pathbar(FileManagerState* fm) {
|
||||||
fm->pathbar_editing = false;
|
fm->pathbar_editing = false;
|
||||||
if (fm->pathbar_len == 0) {
|
if (filemanager_field_len(fm->pathbar) == 0) {
|
||||||
filemanager_read_drives(fm);
|
filemanager_read_drives(fm);
|
||||||
filemanager_push_history(fm);
|
filemanager_push_history(fm);
|
||||||
return;
|
return;
|
||||||
@@ -302,7 +311,11 @@ void filemanager_open_entry(FileManagerState* fm, int idx) {
|
|||||||
char fullpath[512];
|
char fullpath[512];
|
||||||
filemanager_build_fullpath(fullpath, 512, fm->current_path, name);
|
filemanager_build_fullpath(fullpath, 512, fm->current_path, name);
|
||||||
if (str_ends_with(name, ".elf")) {
|
if (str_ends_with(name, ".elf")) {
|
||||||
desktop_spawn_app(ds, fullpath);
|
// An installed app's binary starts the way the launcher would start
|
||||||
|
// it; any other program runs with Files' own (narrower) authority.
|
||||||
|
int item = files_host::find_item_by_binary(ds, fullpath);
|
||||||
|
if (item >= 0) desktop_launch_item(ds, &ds->desktop_items[item]);
|
||||||
|
else desktop_spawn_app(ds, fullpath);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,7 +327,9 @@ void filemanager_open_entry(FileManagerState* fm, int idx) {
|
|||||||
// Nothing claims this type: text still goes straight to the Text Editor
|
// Nothing claims this type: text still goes straight to the Text Editor
|
||||||
// (READMEs, Makefiles, odd extensions); anything else asks.
|
// (READMEs, Makefiles, odd extensions); anything else asks.
|
||||||
if (filemanager_file_looks_textual(fullpath)) {
|
if (filemanager_file_looks_textual(fullpath)) {
|
||||||
desktop_spawn_app(ds, "0:/apps/texteditor/texteditor.elf", fullpath);
|
int editor = files_host::find_item_by_binary(ds, "0:/apps/texteditor/texteditor.elf");
|
||||||
|
if (editor < 0 || !filemanager_launch_with(ds, editor, fullpath))
|
||||||
|
desktop_spawn_app(ds, "0:/apps/texteditor/texteditor.elf", fullpath);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
filemanager_show_open_with(fm, idx);
|
filemanager_show_open_with(fm, idx);
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
/*
|
||||||
|
* newfile.cpp
|
||||||
|
* "New" submenu of the Files background menu: new folders and new,
|
||||||
|
* empty documents of the types the installed apps can open
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "filemanager_internal.hpp"
|
||||||
|
|
||||||
|
namespace filemanager {
|
||||||
|
|
||||||
|
// The smallest file each app opens as an empty document. The Word Processor
|
||||||
|
// and Spreadsheet reject zero-byte files, so theirs carry a header:
|
||||||
|
// MWP v1: magic, version 1.0, font 0 (Roboto) at the default 18pt, 0 runs
|
||||||
|
// (paragraph styles take their defaults)
|
||||||
|
// MSS v2: magic, 0 cells, 26 columns x 100 rows
|
||||||
|
static constexpr uint8_t kEmptyMwp[] = {'M', 'W', 'P', '1', 1, 0, 0, 18, 0, 0};
|
||||||
|
static constexpr uint8_t kEmptyMss[] = {'M', 'S', 'S', '2', 0, 0, 26, 100};
|
||||||
|
|
||||||
|
struct NewFileTemplate {
|
||||||
|
const char* key; // stable id, kept in undo history
|
||||||
|
const char* label; // submenu entry
|
||||||
|
const char* name; // file name before uniquifying
|
||||||
|
const char* app_ext; // listed only if some app opens this type (null = always)
|
||||||
|
const uint8_t* data;
|
||||||
|
int size;
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr NewFileTemplate kTemplates[] = {
|
||||||
|
{"text", "Text Document", "New Text Document.txt", nullptr, nullptr, 0},
|
||||||
|
{"mwp", "Word Processor Document", "New Document.mwp", "mwp",
|
||||||
|
kEmptyMwp, (int)sizeof(kEmptyMwp)},
|
||||||
|
{"mss", "Spreadsheet", "New Spreadsheet.mss", "mss",
|
||||||
|
kEmptyMss, (int)sizeof(kEmptyMss)},
|
||||||
|
{"markdown", "Markdown Document", "New Document.md", nullptr, nullptr, 0},
|
||||||
|
{"empty", "Empty File", "New File", nullptr, nullptr, 0},
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr int kTemplateCount = (int)(sizeof(kTemplates) / sizeof(kTemplates[0]));
|
||||||
|
static_assert(kTemplateCount < FM_CTX_MAX_ITEMS, "New submenu holds Folder + the templates");
|
||||||
|
|
||||||
|
static const NewFileTemplate* template_by_key(const char* key) {
|
||||||
|
for (const NewFileTemplate& t : kTemplates) {
|
||||||
|
if (montauk::streq(t.key, key)) return &t;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* filemanager_new_file_label(int action) {
|
||||||
|
int i = action - CTX_NEW_FILE_BASE;
|
||||||
|
return i >= 0 && i < kTemplateCount ? kTemplates[i].label : "?";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Submenu --------------------------------------------------------------
|
||||||
|
|
||||||
|
void filemanager_build_new_submenu(FileManagerState* fm) {
|
||||||
|
fm->ctx_sub_count = 0;
|
||||||
|
fm->ctx_sub_items[fm->ctx_sub_count++] = CTX_NEW_FOLDER;
|
||||||
|
for (int i = 0; i < kTemplateCount; i++) {
|
||||||
|
const char* ext = kTemplates[i].app_ext;
|
||||||
|
if (ext && filemanager_assoc_default(fm->desktop, ext) < 0) continue;
|
||||||
|
fm->ctx_sub_items[fm->ctx_sub_count++] = CTX_NEW_FILE_BASE + i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int filemanager_new_submenu_items(const FileManagerState* fm, mtk::ContextMenuItem* items) {
|
||||||
|
for (int i = 0; i < fm->ctx_sub_count; i++) {
|
||||||
|
int action = fm->ctx_sub_items[i];
|
||||||
|
items[i] = {ctx_label_for(fm, action), action, true, ctx_shortcut(fm, action),
|
||||||
|
action == CTX_NEW_FOLDER && i + 1 < fm->ctx_sub_count};
|
||||||
|
}
|
||||||
|
return fm->ctx_sub_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool filemanager_new_submenu_layout(const FileManagerState* fm, int view_w, int view_h,
|
||||||
|
mtk::ContextMenuState* sub, int* sub_w) {
|
||||||
|
int row = -1;
|
||||||
|
for (int i = 0; i < fm->ctx_item_count; i++) {
|
||||||
|
if (fm->ctx_items[i] == CTX_NEW) { row = i; break; }
|
||||||
|
}
|
||||||
|
if (row < 0 || fm->ctx_sub_count <= 0) return false;
|
||||||
|
|
||||||
|
mtk::ContextMenuItem items[FM_CTX_MAX_ITEMS];
|
||||||
|
filemanager_build_context_items(fm, items, FM_CTX_MAX_ITEMS);
|
||||||
|
int menu_w = filemanager_context_menu_width(items, fm->ctx_item_count);
|
||||||
|
Rect menu = mtk::context_menu_rect(filemanager_context_state(fm), fm->ctx_item_count,
|
||||||
|
view_w, view_h, menu_w);
|
||||||
|
|
||||||
|
mtk::ContextMenuItem sub_items[FM_CTX_MAX_ITEMS];
|
||||||
|
int sub_count = filemanager_new_submenu_items(fm, sub_items);
|
||||||
|
*sub_w = filemanager_context_menu_width(sub_items, sub_count);
|
||||||
|
|
||||||
|
// Beside the "New" row, its first entry level with it; flipped to the
|
||||||
|
// left when there is no room on the right.
|
||||||
|
int x = menu.x + menu.w - 2;
|
||||||
|
if (x + *sub_w > view_w) x = menu.x - *sub_w + 2;
|
||||||
|
int y = menu.y + row * mtk::CONTEXT_MENU_ITEM_H;
|
||||||
|
*sub = {fm->ctx_open && fm->ctx_sub_open, x, y, fm->ctx_sub_hover};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Creating files -------------------------------------------------------
|
||||||
|
|
||||||
|
montauk::StatusCode filemanager_new_file_write(const char* path, const char* key) {
|
||||||
|
const NewFileTemplate* t = template_by_key(key);
|
||||||
|
if (!t) return montauk::InvalidArgument;
|
||||||
|
int fd = montauk::fcreate(path);
|
||||||
|
if (fd < 0) return filemanager_status_from_result(fd);
|
||||||
|
montauk::StatusCode status = montauk::Success;
|
||||||
|
if (t->size > 0) {
|
||||||
|
int rc = montauk::fwrite(fd, t->data, 0, (uint64_t)t->size);
|
||||||
|
if (rc < 0) status = filemanager_status_from_result(rc);
|
||||||
|
}
|
||||||
|
montauk::close(fd);
|
||||||
|
if (status != montauk::Success) montauk::fdelete(path);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool filemanager_new_file_unchanged(const char* path, const char* key) {
|
||||||
|
const NewFileTemplate* t = template_by_key(key);
|
||||||
|
if (!t) return false;
|
||||||
|
int fd = montauk::open(path);
|
||||||
|
if (fd < 0) return false;
|
||||||
|
bool same = montauk::getsize(fd) == (uint64_t)t->size;
|
||||||
|
if (same && t->size > 0) {
|
||||||
|
uint8_t buf[64];
|
||||||
|
same = t->size <= (int)sizeof(buf) &&
|
||||||
|
montauk::read(fd, buf, 0, (uint64_t)t->size) == t->size;
|
||||||
|
for (int i = 0; same && i < t->size; i++) same = buf[i] == t->data[i];
|
||||||
|
}
|
||||||
|
montauk::close(fd);
|
||||||
|
return same;
|
||||||
|
}
|
||||||
|
|
||||||
|
void filemanager_new_file(FileManagerState* fm, int action) {
|
||||||
|
int ti = action - CTX_NEW_FILE_BASE;
|
||||||
|
if (ti < 0 || ti >= kTemplateCount) return;
|
||||||
|
if (filemanager_is_computer_view(fm) || filemanager_is_virtual_view(fm)) return;
|
||||||
|
if (filemanager_in_trash(fm)) return;
|
||||||
|
const NewFileTemplate& t = kTemplates[ti];
|
||||||
|
|
||||||
|
char path[512];
|
||||||
|
filemanager_build_fullpath(path, sizeof(path), fm->current_path, t.name);
|
||||||
|
if (filemanager_path_exists(path))
|
||||||
|
filemanager_unique_path(fm->current_path, t.name, false, false, path, sizeof(path));
|
||||||
|
|
||||||
|
montauk::StatusCode status = filemanager_new_file_write(path, t.key);
|
||||||
|
filemanager_read_dir(fm);
|
||||||
|
if (status != montauk::Success) {
|
||||||
|
char heading[64];
|
||||||
|
snprintf(heading, sizeof(heading), "Couldn't create a new %s", t.label);
|
||||||
|
filemanager_show_error(fm->desktop, fm, heading, fm->current_path, status);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const char* created = path;
|
||||||
|
const char* key = t.key;
|
||||||
|
filemanager_undo_record(fm, FM_UNDO_NEW_FILE, &key, &created, 1);
|
||||||
|
|
||||||
|
// Select it and start renaming (the name sans extension is selected).
|
||||||
|
const char* name = path_basename(path);
|
||||||
|
for (int i = 0; i < fm->entry_count; i++) {
|
||||||
|
if (!montauk::streq(fm->entry_names[i], name)) continue;
|
||||||
|
filemanager_select_single(fm, i);
|
||||||
|
filemanager_start_rename(fm);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace filemanager
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* openwith.cpp
|
* openwith.cpp
|
||||||
* "Open With" app chooser for the embedded desktop file manager
|
* "Open With" app chooser for the Files file manager
|
||||||
* Copyright (c) 2026 Daniel Hammer
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
/*
|
||||||
|
* prefs.cpp
|
||||||
|
* Per-user Files preferences: hidden files, sidebar bookmarks and the
|
||||||
|
* remembered view of each folder (<home>/config/files.toml)
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "filemanager_internal.hpp"
|
||||||
|
#include <montauk/config.h>
|
||||||
|
|
||||||
|
namespace filemanager {
|
||||||
|
|
||||||
|
// Layout of files.toml:
|
||||||
|
// [view]
|
||||||
|
// show_hidden = false
|
||||||
|
// [sidebar]
|
||||||
|
// width = 160
|
||||||
|
// bookmarks = ["0:/users/me/Projects", ...]
|
||||||
|
// [folders]
|
||||||
|
// views = ["grid name asc 0:/users/me/Pictures", ...]
|
||||||
|
// A folder record is "<grid|list> <name|size|type|modified> <asc|desc> <path>",
|
||||||
|
// path last so it may contain spaces. Most recent first, capped.
|
||||||
|
|
||||||
|
static constexpr const char* PREFS_NAME = "files";
|
||||||
|
static constexpr int MAX_BOOKMARKS = 32;
|
||||||
|
static constexpr int MAX_FOLDER_VIEWS = 128;
|
||||||
|
|
||||||
|
struct FolderView {
|
||||||
|
char path[256];
|
||||||
|
bool grid;
|
||||||
|
FileManagerSortKey key;
|
||||||
|
bool desc;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Prefs {
|
||||||
|
bool show_hidden;
|
||||||
|
int sidebar_width; // 0 = default
|
||||||
|
char bookmarks[MAX_BOOKMARKS][256];
|
||||||
|
int bookmark_count;
|
||||||
|
FolderView views[MAX_FOLDER_VIEWS];
|
||||||
|
int view_count;
|
||||||
|
};
|
||||||
|
|
||||||
|
static Prefs g_prefs;
|
||||||
|
|
||||||
|
static bool mem_eq(const char* a, const char* b, int n) {
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
if (a[i] != b[i]) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char* sort_key_name(FileManagerSortKey key) {
|
||||||
|
switch (key) {
|
||||||
|
case FM_SORT_SIZE: return "size";
|
||||||
|
case FM_SORT_TYPE: return "type";
|
||||||
|
case FM_SORT_MODIFIED: return "modified";
|
||||||
|
default: return "name";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static FileManagerSortKey sort_key_from(const char* s, int len) {
|
||||||
|
auto is = [&](const char* w) {
|
||||||
|
int n = montauk::slen(w);
|
||||||
|
return n == len && mem_eq(s, w, n);
|
||||||
|
};
|
||||||
|
if (is("size")) return FM_SORT_SIZE;
|
||||||
|
if (is("type")) return FM_SORT_TYPE;
|
||||||
|
if (is("modified")) return FM_SORT_MODIFIED;
|
||||||
|
return FM_SORT_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
// "grid name asc /path with spaces" -> FolderView. False if malformed.
|
||||||
|
static bool parse_folder_view(const char* rec, FolderView* out) {
|
||||||
|
const char* p = rec;
|
||||||
|
const char* fields[3];
|
||||||
|
int lens[3];
|
||||||
|
for (int f = 0; f < 3; f++) {
|
||||||
|
while (*p == ' ') p++;
|
||||||
|
fields[f] = p;
|
||||||
|
while (*p && *p != ' ') p++;
|
||||||
|
lens[f] = (int)(p - fields[f]);
|
||||||
|
if (lens[f] == 0) return false;
|
||||||
|
}
|
||||||
|
while (*p == ' ') p++;
|
||||||
|
if (!*p) return false;
|
||||||
|
out->grid = lens[0] == 4 && mem_eq(fields[0], "grid", 4);
|
||||||
|
out->key = sort_key_from(fields[1], lens[1]);
|
||||||
|
out->desc = lens[2] == 4 && mem_eq(fields[2], "desc", 4);
|
||||||
|
montauk::strncpy(out->path, p, sizeof(out->path));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void read_from_disk(DesktopState* ds) {
|
||||||
|
montauk::memset(&g_prefs, 0, sizeof(g_prefs));
|
||||||
|
if (!ds || !ds->current_user[0]) return;
|
||||||
|
|
||||||
|
auto doc = montauk::config::load_user(ds->current_user, PREFS_NAME);
|
||||||
|
g_prefs.show_hidden = doc.get_bool("view.show_hidden", false);
|
||||||
|
int64_t width = doc.get_int("sidebar.width", 0);
|
||||||
|
g_prefs.sidebar_width = width > 0 && width <= FM_SIDEBAR_MAX_W ? (int)width : 0;
|
||||||
|
|
||||||
|
if (auto* arr = doc.get_array("sidebar.bookmarks")) {
|
||||||
|
for (int i = 0; i < arr->array.count && g_prefs.bookmark_count < MAX_BOOKMARKS; i++) {
|
||||||
|
auto* v = arr->array.items[i];
|
||||||
|
if (v->type != montauk::toml::Type::String || !v->str[0]) continue;
|
||||||
|
montauk::strncpy(g_prefs.bookmarks[g_prefs.bookmark_count++], v->str, 256);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (auto* arr = doc.get_array("folders.views")) {
|
||||||
|
for (int i = 0; i < arr->array.count && g_prefs.view_count < MAX_FOLDER_VIEWS; i++) {
|
||||||
|
auto* v = arr->array.items[i];
|
||||||
|
if (v->type != montauk::toml::Type::String) continue;
|
||||||
|
if (parse_folder_view(v->str, &g_prefs.views[g_prefs.view_count])) g_prefs.view_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
doc.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
static montauk::toml::Value* string_array(const char* key) {
|
||||||
|
return montauk::toml::Value::make_array(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void push_string(montauk::toml::Value* arr, const char* s) {
|
||||||
|
arr->array.push(montauk::toml::Value::make_string(nullptr, s, montauk::slen(s)));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void write_to_disk(DesktopState* ds) {
|
||||||
|
if (!ds || !ds->current_user[0]) return;
|
||||||
|
|
||||||
|
montauk::toml::Doc doc;
|
||||||
|
doc.init();
|
||||||
|
montauk::config::set_bool(&doc, "view.show_hidden", g_prefs.show_hidden);
|
||||||
|
if (g_prefs.sidebar_width > 0)
|
||||||
|
montauk::config::set_int(&doc, "sidebar.width", g_prefs.sidebar_width);
|
||||||
|
|
||||||
|
auto* bookmarks = string_array("sidebar.bookmarks");
|
||||||
|
for (int i = 0; i < g_prefs.bookmark_count; i++) push_string(bookmarks, g_prefs.bookmarks[i]);
|
||||||
|
doc.entries.push(bookmarks);
|
||||||
|
|
||||||
|
auto* views = string_array("folders.views");
|
||||||
|
for (int i = 0; i < g_prefs.view_count; i++) {
|
||||||
|
const FolderView& v = g_prefs.views[i];
|
||||||
|
char rec[300];
|
||||||
|
snprintf(rec, sizeof(rec), "%s %s %s %s", v.grid ? "grid" : "list",
|
||||||
|
sort_key_name(v.key), v.desc ? "desc" : "asc", v.path);
|
||||||
|
push_string(views, rec);
|
||||||
|
}
|
||||||
|
doc.entries.push(views);
|
||||||
|
|
||||||
|
montauk::config::save_user(ds->current_user, PREFS_NAME, &doc);
|
||||||
|
doc.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
void prefs_load(DesktopState* ds) {
|
||||||
|
read_from_disk(ds);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool prefs_show_hidden() {
|
||||||
|
return g_prefs.show_hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
void prefs_set_show_hidden(DesktopState* ds, bool show) {
|
||||||
|
read_from_disk(ds);
|
||||||
|
g_prefs.show_hidden = show;
|
||||||
|
write_to_disk(ds);
|
||||||
|
}
|
||||||
|
|
||||||
|
int prefs_sidebar_width() {
|
||||||
|
return g_prefs.sidebar_width;
|
||||||
|
}
|
||||||
|
|
||||||
|
void prefs_set_sidebar_width(DesktopState* ds, int width) {
|
||||||
|
read_from_disk(ds);
|
||||||
|
if (g_prefs.sidebar_width == width) return;
|
||||||
|
g_prefs.sidebar_width = width;
|
||||||
|
write_to_disk(ds);
|
||||||
|
}
|
||||||
|
|
||||||
|
int prefs_bookmark_count() {
|
||||||
|
return g_prefs.bookmark_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* prefs_bookmark(int i) {
|
||||||
|
return (i >= 0 && i < g_prefs.bookmark_count) ? g_prefs.bookmarks[i] : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool prefs_has_bookmark(const char* path) {
|
||||||
|
for (int i = 0; i < g_prefs.bookmark_count; i++) {
|
||||||
|
if (montauk::streq(g_prefs.bookmarks[i], path)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void prefs_add_bookmark(DesktopState* ds, const char* path) {
|
||||||
|
if (!path || !path[0]) return;
|
||||||
|
read_from_disk(ds);
|
||||||
|
if (prefs_has_bookmark(path) || g_prefs.bookmark_count >= MAX_BOOKMARKS) return;
|
||||||
|
montauk::strncpy(g_prefs.bookmarks[g_prefs.bookmark_count++], path, 256);
|
||||||
|
write_to_disk(ds);
|
||||||
|
}
|
||||||
|
|
||||||
|
void prefs_remove_bookmark(DesktopState* ds, const char* path) {
|
||||||
|
read_from_disk(ds);
|
||||||
|
for (int i = 0; i < g_prefs.bookmark_count; i++) {
|
||||||
|
if (!montauk::streq(g_prefs.bookmarks[i], path)) continue;
|
||||||
|
for (int j = i; j < g_prefs.bookmark_count - 1; j++)
|
||||||
|
montauk::strcpy(g_prefs.bookmarks[j], g_prefs.bookmarks[j + 1]);
|
||||||
|
g_prefs.bookmark_count--;
|
||||||
|
write_to_disk(ds);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool prefs_folder_view(const char* path, bool* grid, FileManagerSortKey* key, bool* desc) {
|
||||||
|
for (int i = 0; i < g_prefs.view_count; i++) {
|
||||||
|
const FolderView& v = g_prefs.views[i];
|
||||||
|
if (!montauk::streq(v.path, path)) continue;
|
||||||
|
*grid = v.grid;
|
||||||
|
*key = v.key;
|
||||||
|
*desc = v.desc;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void prefs_set_folder_view(DesktopState* ds, const char* path, bool grid,
|
||||||
|
FileManagerSortKey key, bool desc) {
|
||||||
|
if (!path || !path[0]) return;
|
||||||
|
read_from_disk(ds);
|
||||||
|
|
||||||
|
// Move (or insert) the record to the front; the oldest falls off the end.
|
||||||
|
int at = g_prefs.view_count < MAX_FOLDER_VIEWS ? g_prefs.view_count : MAX_FOLDER_VIEWS - 1;
|
||||||
|
for (int i = 0; i < g_prefs.view_count; i++) {
|
||||||
|
if (montauk::streq(g_prefs.views[i].path, path)) { at = i; break; }
|
||||||
|
}
|
||||||
|
if (at == g_prefs.view_count) g_prefs.view_count++;
|
||||||
|
for (int i = at; i > 0; i--) g_prefs.views[i] = g_prefs.views[i - 1];
|
||||||
|
|
||||||
|
FolderView& v = g_prefs.views[0];
|
||||||
|
montauk::strncpy(v.path, path, sizeof(v.path));
|
||||||
|
v.grid = grid;
|
||||||
|
v.key = key;
|
||||||
|
v.desc = desc;
|
||||||
|
write_to_disk(ds);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace filemanager
|
||||||
+8
-2
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* properties.cpp
|
* properties.cpp
|
||||||
* MTK file properties dialog for the embedded desktop file manager
|
* MTK file properties dialog for the Files file manager
|
||||||
* Copyright (c) 2026 Daniel Hammer
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -247,6 +247,9 @@ static void props_choose_icon(FilePropertiesDialogState* st,
|
|||||||
props_set_shared_icon(st, &ds->icon_folder_lg);
|
props_set_shared_icon(st, &ds->icon_folder_lg);
|
||||||
} else if (ds && type == FM_ENTRY_EXEC && ds->icon_exec_lg.pixels) {
|
} else if (ds && type == FM_ENTRY_EXEC && ds->icon_exec_lg.pixels) {
|
||||||
props_set_shared_icon(st, &ds->icon_exec_lg);
|
props_set_shared_icon(st, &ds->icon_exec_lg);
|
||||||
|
} else if (const SvgIcon* type_icon = type == FM_ENTRY_FILE
|
||||||
|
? filemanager_file_type_icon(fm->entry_names[idx], true) : nullptr) {
|
||||||
|
props_set_shared_icon(st, type_icon);
|
||||||
} else if (ds && ds->icon_file_lg.pixels) {
|
} else if (ds && ds->icon_file_lg.pixels) {
|
||||||
props_set_shared_icon(st, &ds->icon_file_lg);
|
props_set_shared_icon(st, &ds->icon_file_lg);
|
||||||
}
|
}
|
||||||
@@ -753,7 +756,10 @@ void filemanager_show_bulk_delete_confirmation(FileManagerState* fm,
|
|||||||
snprintf(st->name, sizeof(st->name), "%d items", out);
|
snprintf(st->name, sizeof(st->name), "%d items", out);
|
||||||
}
|
}
|
||||||
|
|
||||||
const SvgIcon& icon = (out == 1 && !st->is_dir) ? ds->icon_file_lg : ds->icon_folder_lg;
|
const SvgIcon* type_icon = out == 1 && fm->entry_types[indices[0]] == FM_ENTRY_FILE
|
||||||
|
? filemanager_file_type_icon(fm->entry_names[indices[0]], true) : nullptr;
|
||||||
|
const SvgIcon& icon = type_icon ? *type_icon
|
||||||
|
: (out == 1 && !st->is_dir) ? ds->icon_file_lg : ds->icon_folder_lg;
|
||||||
if (icon.pixels) st->icon = icon;
|
if (icon.pixels) st->icon = icon;
|
||||||
|
|
||||||
int w = delete_confirm_dialog_width(delete_confirm_subtitle(st));
|
int w = delete_confirm_dialog_width(delete_confirm_subtitle(st));
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* render.cpp
|
* render.cpp
|
||||||
* Drawing code for the embedded desktop file manager
|
* Drawing code for the Files file manager
|
||||||
* Copyright (c) 2026 Daniel Hammer
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -150,6 +150,16 @@ static void draw_column_header(Canvas& c, int x, int header_y, const char* label
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Accent outline around the entry a drag would drop into.
|
||||||
|
static void draw_drop_target(Canvas& c, FileManagerState* fm, int x, int y, int w, int h, int clip_y) {
|
||||||
|
if (!fm->drop_action) return;
|
||||||
|
if (y < clip_y) { h -= clip_y - y; y = clip_y; }
|
||||||
|
if (h <= 0 || w <= 0) return;
|
||||||
|
Color accent = fm->desktop ? fm->desktop->settings.accent_color : colors::ACCENT;
|
||||||
|
c.rect(x, y, w, h, accent);
|
||||||
|
c.rect(x + 1, y + 1, w - 2, h - 2, accent);
|
||||||
|
}
|
||||||
|
|
||||||
int filemanager_grid_row_height(FileManagerState* fm, int row, int cols) {
|
int filemanager_grid_row_height(FileManagerState* fm, int row, int cols) {
|
||||||
if (!fm || cols <= 0 || row < 0) return FM_GRID_CELL_H;
|
if (!fm || cols <= 0 || row < 0) return FM_GRID_CELL_H;
|
||||||
|
|
||||||
@@ -261,7 +271,7 @@ void filemanager_draw_header(Canvas& c, FileManagerState* fm, Color toolbar_colo
|
|||||||
bool in_trash = filemanager_in_trash(fm);
|
bool in_trash = filemanager_in_trash(fm);
|
||||||
// Rename is single-target only.
|
// Rename is single-target only.
|
||||||
bool has_rename = single_sel && !multi_sel && !in_trash;
|
bool has_rename = single_sel && !multi_sel && !in_trash;
|
||||||
bool has_clip = fm->clipboard_count > 0 && !filemanager_is_computer_view(fm)
|
bool has_clip = filemanager_clipboard_has_files(fm) && !filemanager_is_computer_view(fm)
|
||||||
&& !filemanager_is_virtual_view(fm) && !in_trash;
|
&& !filemanager_is_virtual_view(fm) && !in_trash;
|
||||||
Color dim_bg = Color::from_rgb(0xF0, 0xF0, 0xF0);
|
Color dim_bg = Color::from_rgb(0xF0, 0xF0, 0xF0);
|
||||||
|
|
||||||
@@ -294,41 +304,49 @@ void filemanager_draw_header(Canvas& c, FileManagerState* fm, Color toolbar_colo
|
|||||||
// ---- Path bar ----
|
// ---- Path bar ----
|
||||||
int pathbar_y = FM_TOOLBAR_H;
|
int pathbar_y = FM_TOOLBAR_H;
|
||||||
if (fm->pathbar_editing) {
|
if (fm->pathbar_editing) {
|
||||||
// Editable text input
|
filemanager_field_draw(c, fm->pathbar, filemanager_pathbar_box(c.w),
|
||||||
c.fill_rect(0, pathbar_y, c.w, FM_PATHBAR_H, colors::WHITE);
|
fm->desktop ? fm->desktop->settings.accent_color : colors::ACCENT);
|
||||||
c.rect(0, pathbar_y, c.w, FM_PATHBAR_H, colors::ACCENT);
|
|
||||||
int fh = system_font_height();
|
|
||||||
int ty = pathbar_y + (FM_PATHBAR_H - fh) / 2;
|
|
||||||
c.text(8, ty, fm->pathbar_buf, colors::TEXT_COLOR);
|
|
||||||
// Cursor
|
|
||||||
char prefix[256];
|
|
||||||
int cpos = fm->pathbar_cursor;
|
|
||||||
if (cpos > 255) cpos = 255;
|
|
||||||
for (int k = 0; k < cpos; k++) prefix[k] = fm->pathbar_buf[k];
|
|
||||||
prefix[cpos] = '\0';
|
|
||||||
int cx = 8 + text_width(prefix);
|
|
||||||
c.vline(cx, ty, fh, colors::ACCENT);
|
|
||||||
} else {
|
} else {
|
||||||
c.fill_rect(0, pathbar_y, c.w, FM_PATHBAR_H, Color::from_rgb(0xF0, 0xF0, 0xF0));
|
c.fill_rect(0, pathbar_y, c.w, FM_PATHBAR_H, Color::from_rgb(0xF0, 0xF0, 0xF0));
|
||||||
const char* pathbar_text = fm->current_path;
|
filemanager_draw_breadcrumbs(c, fm, pathbar_y);
|
||||||
char trash_label[256];
|
|
||||||
if (filemanager_is_computer_view(fm)) pathbar_text = "Computer";
|
|
||||||
else if (filemanager_is_virtual_view(fm)) pathbar_text = filemanager_virtual_view_label(fm->virtual_view);
|
|
||||||
else if (in_trash) {
|
|
||||||
// "Trash", or "Trash/<folder>" inside a trashed folder.
|
|
||||||
char files[256];
|
|
||||||
filemanager_trash_files_dir(ds, files, sizeof(files));
|
|
||||||
snprintf(trash_label, sizeof(trash_label), "Trash%s",
|
|
||||||
fm->current_path + montauk::slen(files));
|
|
||||||
pathbar_text = trash_label;
|
|
||||||
}
|
|
||||||
c.text(8, pathbar_y + 4, pathbar_text, colors::TEXT_COLOR);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path bar separator
|
// Path bar separator
|
||||||
c.hline(0, pathbar_y + FM_PATHBAR_H - 1, c.w, colors::BORDER);
|
c.hline(0, pathbar_y + FM_PATHBAR_H - 1, c.w, colors::BORDER);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool filemanager_rename_box(FileManagerState* fm, const FileManagerLayout& l, int ch, Rect* out) {
|
||||||
|
if (!fm->rename_active) return false;
|
||||||
|
int i = fm->rename_idx;
|
||||||
|
if (i < 0 || i >= fm->entry_count) return false;
|
||||||
|
|
||||||
|
int fh = system_font_height();
|
||||||
|
Rect r;
|
||||||
|
if (fm->grid_view) {
|
||||||
|
// Under the icon, centered on the cell and as wide as the name
|
||||||
|
// needs (up to three cells), kept inside the view.
|
||||||
|
int cols = l.grid_cols;
|
||||||
|
int cell_x = l.view_x + (i % cols) * FM_GRID_CELL_W;
|
||||||
|
int cell_y = l.list_y + filemanager_grid_row_y(fm, i / cols, cols)
|
||||||
|
- fm->scrollbar.scroll_offset;
|
||||||
|
int min_w = FM_GRID_CELL_W - 4;
|
||||||
|
int max_w = gui_max(gui_min(FM_GRID_CELL_W * 3, l.view_w - 4), min_w);
|
||||||
|
int w = gui_clamp(text_width(fm->rename.buf) + fm->rename.pad * 2 + 2, min_w, max_w);
|
||||||
|
int x = cell_x + FM_GRID_CELL_W / 2 - w / 2;
|
||||||
|
x = gui_max(gui_min(x, l.view_x + l.view_w - 2 - w), l.view_x + 2);
|
||||||
|
r = {x, cell_y + FM_GRID_PAD + FM_GRID_ICON, w, fh + 6};
|
||||||
|
} else {
|
||||||
|
// Over the name column; text lines up with the names (view_x + 30).
|
||||||
|
FileManagerListColumns cols = filemanager_list_columns(l.view_x, l.view_w);
|
||||||
|
int iy = l.list_y + i * FM_ITEM_H - fm->scrollbar.scroll_offset;
|
||||||
|
int x = l.view_x + 30 - fm->rename.pad;
|
||||||
|
r = {x, iy + 2, gui_max(cols.name_right - x, 60), FM_ITEM_H - 4};
|
||||||
|
}
|
||||||
|
if (r.y < l.list_y || r.y + r.h > ch) return false;
|
||||||
|
*out = r;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void filemanager_on_draw(Window* win, Framebuffer& fb) {
|
void filemanager_on_draw(Window* win, Framebuffer& fb) {
|
||||||
FileManagerState* fm = (FileManagerState*)win->app_data;
|
FileManagerState* fm = (FileManagerState*)win->app_data;
|
||||||
if (!fm) return;
|
if (!fm) return;
|
||||||
@@ -346,10 +364,10 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
|
|||||||
|
|
||||||
if (fm->grid_view) {
|
if (fm->grid_view) {
|
||||||
// ---- Grid View ----
|
// ---- Grid View ----
|
||||||
int list_y = FM_TOOLBAR_H + FM_PATHBAR_H;
|
FileManagerLayout l = filemanager_layout(fm, true, c.w);
|
||||||
|
int list_y = l.list_y;
|
||||||
int list_h = c.h - list_y;
|
int list_h = c.h - list_y;
|
||||||
int cols = (c.w - FM_SCROLLBAR_W) / FM_GRID_CELL_W;
|
int cols = l.grid_cols;
|
||||||
if (cols < 1) cols = 1;
|
|
||||||
int content_h = filemanager_grid_content_height(fm, cols);
|
int content_h = filemanager_grid_content_height(fm, cols);
|
||||||
|
|
||||||
// Update scrollbar
|
// Update scrollbar
|
||||||
@@ -360,7 +378,7 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
|
|||||||
for (int i = 0; i < fm->entry_count; i++) {
|
for (int i = 0; i < fm->entry_count; i++) {
|
||||||
int col = i % cols;
|
int col = i % cols;
|
||||||
int row = i / cols;
|
int row = i / cols;
|
||||||
int cell_x = col * FM_GRID_CELL_W;
|
int cell_x = l.view_x + col * FM_GRID_CELL_W;
|
||||||
int row_h = filemanager_grid_row_height(fm, row, cols);
|
int row_h = filemanager_grid_row_height(fm, row, cols);
|
||||||
int cell_y = list_y + filemanager_grid_row_y(fm, row, cols) - fm->scrollbar.scroll_offset;
|
int cell_y = list_y + filemanager_grid_row_y(fm, row, cols) - fm->scrollbar.scroll_offset;
|
||||||
|
|
||||||
@@ -372,10 +390,11 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
|
|||||||
if (cell_selected) {
|
if (cell_selected) {
|
||||||
int sy = gui_max(cell_y, list_y);
|
int sy = gui_max(cell_y, list_y);
|
||||||
int sh = gui_min(cell_y + row_h, c.h) - sy;
|
int sh = gui_min(cell_y + row_h, c.h) - sy;
|
||||||
int sw = gui_min(FM_GRID_CELL_W, c.w - FM_SCROLLBAR_W - cell_x);
|
int sw = gui_min(FM_GRID_CELL_W, l.view_x + l.view_w - cell_x);
|
||||||
if (sh > 0 && sw > 0)
|
if (sh > 0 && sw > 0)
|
||||||
c.fill_rect(cell_x, sy, sw, sh, selection_highlight(ds));
|
c.fill_rect(cell_x, sy, sw, sh, selection_highlight(ds));
|
||||||
}
|
}
|
||||||
|
if (i == fm->drop_target_idx) draw_drop_target(c, fm, cell_x, cell_y, FM_GRID_CELL_W, row_h, list_y);
|
||||||
|
|
||||||
// Large icon centered horizontally
|
// Large icon centered horizontally
|
||||||
int icon_x = cell_x + (FM_GRID_CELL_W - FM_GRID_ICON) / 2;
|
int icon_x = cell_x + (FM_GRID_CELL_W - FM_GRID_ICON) / 2;
|
||||||
@@ -385,7 +404,14 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
|
|||||||
if (fm->entry_types[i] == FM_ENTRY_SPECIAL_DIR) sfi = fm->drive_indices[i];
|
if (fm->entry_types[i] == FM_ENTRY_SPECIAL_DIR) sfi = fm->drive_indices[i];
|
||||||
else if (ds && fm->entry_types[i] == FM_ENTRY_DIR) sfi = special_folder_index(fm->entry_names[i]);
|
else if (ds && fm->entry_types[i] == FM_ENTRY_DIR) sfi = special_folder_index(fm->entry_names[i]);
|
||||||
|
|
||||||
if (sfi >= 0 && ds && ds->icon_special_folder_lg[sfi].pixels) {
|
const SvgIcon* thumb = filemanager_thumb_for(fm, i);
|
||||||
|
const SvgIcon* type_icon = fm->entry_types[i] == FM_ENTRY_FILE
|
||||||
|
? filemanager_file_type_icon(fm->entry_names[i], true) : nullptr;
|
||||||
|
if (thumb && thumb->pixels) {
|
||||||
|
// Thumbnails keep the image's aspect ratio inside the icon box.
|
||||||
|
c.icon(icon_x + (FM_GRID_ICON - thumb->width) / 2,
|
||||||
|
icon_y + (FM_GRID_ICON - thumb->height) / 2, *thumb);
|
||||||
|
} else if (sfi >= 0 && ds && ds->icon_special_folder_lg[sfi].pixels) {
|
||||||
c.icon(icon_x, icon_y, ds->icon_special_folder_lg[sfi]);
|
c.icon(icon_x, icon_y, ds->icon_special_folder_lg[sfi]);
|
||||||
} else if (fm->entry_types[i] == FM_ENTRY_VIRTUAL_ITEM
|
} else if (fm->entry_types[i] == FM_ENTRY_VIRTUAL_ITEM
|
||||||
&& i < fm->virtual_entry_count
|
&& i < fm->virtual_entry_count
|
||||||
@@ -410,6 +436,8 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
|
|||||||
c.icon(icon_x, icon_y, ds->icon_folder_lg);
|
c.icon(icon_x, icon_y, ds->icon_folder_lg);
|
||||||
} else if (ds && fm->entry_types[i] == FM_ENTRY_EXEC && ds->icon_exec_lg.pixels) {
|
} else if (ds && fm->entry_types[i] == FM_ENTRY_EXEC && ds->icon_exec_lg.pixels) {
|
||||||
c.icon(icon_x, icon_y, ds->icon_exec_lg);
|
c.icon(icon_x, icon_y, ds->icon_exec_lg);
|
||||||
|
} else if (type_icon) {
|
||||||
|
c.icon(icon_x, icon_y, *type_icon);
|
||||||
} else if (ds && ds->icon_file_lg.pixels) {
|
} else if (ds && ds->icon_file_lg.pixels) {
|
||||||
c.icon(icon_x, icon_y, ds->icon_file_lg);
|
c.icon(icon_x, icon_y, ds->icon_file_lg);
|
||||||
} else {
|
} else {
|
||||||
@@ -422,30 +450,10 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
|
|||||||
c.fill_rect(icon_x, iy_clip, FM_GRID_ICON, ih_clip, icon_c);
|
c.fill_rect(icon_x, iy_clip, FM_GRID_ICON, ih_clip, icon_c);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filename centered below icon, truncated if needed
|
// Filename centered below icon, truncated if needed. The entry
|
||||||
// If renaming this entry, draw the rename textbox instead
|
// being renamed shows its rename box instead (drawn after the
|
||||||
if (fm->rename_active && fm->rename_idx == i) {
|
// loop: it may be wider than the cell).
|
||||||
int ty = icon_y + FM_GRID_ICON + 2;
|
if (!(fm->rename_active && fm->rename_idx == i)) {
|
||||||
int tw = FM_GRID_CELL_W - 4;
|
|
||||||
if (ty >= list_y && ty + system_font_height() + 4 <= c.h) {
|
|
||||||
c.fill_rect(cell_x + 2, ty - 1, tw, system_font_height() + 2, colors::WHITE);
|
|
||||||
c.rect(cell_x + 2, ty - 1, tw, system_font_height() + 2, colors::ACCENT);
|
|
||||||
// Draw rename text (truncated to cell width)
|
|
||||||
char display[16];
|
|
||||||
int rlen = fm->rename_len;
|
|
||||||
if (rlen > 10) rlen = montauk::utf8::floor_boundary(fm->rename_buf, fm->rename_len, 10);
|
|
||||||
for (int k = 0; k < rlen; k++) display[k] = fm->rename_buf[k];
|
|
||||||
display[rlen] = '\0';
|
|
||||||
c.text(cell_x + 4, ty, display, colors::TEXT_COLOR);
|
|
||||||
// Cursor
|
|
||||||
char prefix[64];
|
|
||||||
int cpos = fm->rename_cursor < rlen ? fm->rename_cursor : rlen;
|
|
||||||
for (int k = 0; k < cpos; k++) prefix[k] = fm->rename_buf[k];
|
|
||||||
prefix[cpos] = '\0';
|
|
||||||
int cx = cell_x + 4 + text_width(prefix);
|
|
||||||
c.vline(cx, ty, system_font_height(), colors::ACCENT);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Prefer two grid label rows before truncating.
|
// Prefer two grid label rows before truncating.
|
||||||
int max_w = FM_GRID_CELL_W - 4;
|
int max_w = FM_GRID_CELL_W - 4;
|
||||||
const char* name = fm->entry_names[i];
|
const char* name = fm->entry_names[i];
|
||||||
@@ -471,10 +479,11 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
|
|||||||
// ---- List View ----
|
// ---- List View ----
|
||||||
|
|
||||||
// ---- Column headers ----
|
// ---- Column headers ----
|
||||||
int header_y = FM_TOOLBAR_H + FM_PATHBAR_H;
|
FileManagerLayout l = filemanager_layout(fm, false, c.w);
|
||||||
c.fill_rect(0, header_y, c.w, FM_HEADER_H, Color::from_rgb(0xF8, 0xF8, 0xF8));
|
int header_y = l.body_y;
|
||||||
|
c.fill_rect(l.view_x, header_y, c.w - l.view_x, FM_HEADER_H, Color::from_rgb(0xF8, 0xF8, 0xF8));
|
||||||
|
|
||||||
FileManagerListColumns cols = filemanager_list_columns(c.w);
|
FileManagerListColumns cols = filemanager_list_columns(l.view_x, l.view_w);
|
||||||
// Sorting only applies to real directories; Computer and the virtual
|
// Sorting only applies to real directories; Computer and the virtual
|
||||||
// views keep their curated order and show no sort arrow.
|
// views keep their curated order and show no sort arrow.
|
||||||
bool sortable = !filemanager_is_computer_view(fm) && !filemanager_is_virtual_view(fm);
|
bool sortable = !filemanager_is_computer_view(fm) && !filemanager_is_virtual_view(fm);
|
||||||
@@ -492,7 +501,7 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
|
|||||||
sortable && fm->sort_key == FM_SORT_TYPE, fm->sort_desc, dim);
|
sortable && fm->sort_key == FM_SORT_TYPE, fm->sort_desc, dim);
|
||||||
|
|
||||||
// Header separator
|
// Header separator
|
||||||
c.hline(0, header_y + FM_HEADER_H - 1, c.w, colors::BORDER);
|
c.hline(l.view_x, header_y + FM_HEADER_H - 1, c.w - l.view_x, colors::BORDER);
|
||||||
|
|
||||||
// Column separator lines
|
// Column separator lines
|
||||||
int first_col_x = cols.modified_x >= 0 ? cols.modified_x
|
int first_col_x = cols.modified_x >= 0 ? cols.modified_x
|
||||||
@@ -502,7 +511,7 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
|
|||||||
int tz = montauk::gettz();
|
int tz = montauk::gettz();
|
||||||
|
|
||||||
// ---- File entries ----
|
// ---- File entries ----
|
||||||
int list_y = header_y + FM_HEADER_H;
|
int list_y = l.list_y;
|
||||||
int list_h = c.h - list_y;
|
int list_h = c.h - list_y;
|
||||||
int visible_items = list_h / FM_ITEM_H;
|
int visible_items = list_h / FM_ITEM_H;
|
||||||
int content_h = fm->entry_count * FM_ITEM_H;
|
int content_h = fm->entry_count * FM_ITEM_H;
|
||||||
@@ -524,17 +533,21 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
|
|||||||
int sy = gui_max(iy, list_y);
|
int sy = gui_max(iy, list_y);
|
||||||
int sh = gui_min(iy + FM_ITEM_H, c.h) - sy;
|
int sh = gui_min(iy + FM_ITEM_H, c.h) - sy;
|
||||||
if (sh > 0)
|
if (sh > 0)
|
||||||
c.fill_rect(0, sy, c.w - FM_SCROLLBAR_W, sh, selection_highlight(fm->desktop));
|
c.fill_rect(l.view_x, sy, l.view_w, sh, selection_highlight(fm->desktop));
|
||||||
}
|
}
|
||||||
|
if (i == fm->drop_target_idx) draw_drop_target(c, fm, l.view_x, iy, l.view_w, FM_ITEM_H, list_y);
|
||||||
|
|
||||||
// Icon (skip if it would bleed above the list area)
|
// Icon (skip if it would bleed above the list area)
|
||||||
int ico_x = 8;
|
int ico_x = l.view_x + 8;
|
||||||
int ico_y = iy + (FM_ITEM_H - 16) / 2;
|
int ico_y = iy + (FM_ITEM_H - 16) / 2;
|
||||||
// Check for special folder icon
|
// Check for special folder icon
|
||||||
int sfi_sm = -1;
|
int sfi_sm = -1;
|
||||||
if (fm->entry_types[i] == FM_ENTRY_SPECIAL_DIR) sfi_sm = fm->drive_indices[i];
|
if (fm->entry_types[i] == FM_ENTRY_SPECIAL_DIR) sfi_sm = fm->drive_indices[i];
|
||||||
else if (ds && fm->entry_types[i] == FM_ENTRY_DIR) sfi_sm = special_folder_index(fm->entry_names[i]);
|
else if (ds && fm->entry_types[i] == FM_ENTRY_DIR) sfi_sm = special_folder_index(fm->entry_names[i]);
|
||||||
|
|
||||||
|
const SvgIcon* type_icon = fm->entry_types[i] == FM_ENTRY_FILE
|
||||||
|
? filemanager_file_type_icon(fm->entry_names[i], false) : nullptr;
|
||||||
|
|
||||||
if (ico_y < list_y) { /* clipped by header repaint */ }
|
if (ico_y < list_y) { /* clipped by header repaint */ }
|
||||||
else if (sfi_sm >= 0 && ds && ds->icon_special_folder[sfi_sm].pixels) {
|
else if (sfi_sm >= 0 && ds && ds->icon_special_folder[sfi_sm].pixels) {
|
||||||
c.icon(ico_x, ico_y, ds->icon_special_folder[sfi_sm]);
|
c.icon(ico_x, ico_y, ds->icon_special_folder[sfi_sm]);
|
||||||
@@ -560,6 +573,8 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
|
|||||||
c.icon(ico_x, ico_y, ds->icon_folder);
|
c.icon(ico_x, ico_y, ds->icon_folder);
|
||||||
} else if (ds && fm->entry_types[i] == FM_ENTRY_EXEC && ds->icon_exec.pixels) {
|
} else if (ds && fm->entry_types[i] == FM_ENTRY_EXEC && ds->icon_exec.pixels) {
|
||||||
c.icon(ico_x, ico_y, ds->icon_exec);
|
c.icon(ico_x, ico_y, ds->icon_exec);
|
||||||
|
} else if (type_icon) {
|
||||||
|
c.icon(ico_x, ico_y, *type_icon);
|
||||||
} else if (ds && ds->icon_file.pixels) {
|
} else if (ds && ds->icon_file.pixels) {
|
||||||
c.icon(ico_x, ico_y, ds->icon_file);
|
c.icon(ico_x, ico_y, ds->icon_file);
|
||||||
} else {
|
} else {
|
||||||
@@ -573,25 +588,12 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Name (or rename textbox)
|
// Name (or rename textbox)
|
||||||
int tx = 30;
|
int tx = l.view_x + 30;
|
||||||
int fm_sfh = system_font_height();
|
int fm_sfh = system_font_height();
|
||||||
int ty = iy + (FM_ITEM_H - fm_sfh) / 2;
|
int ty = iy + (FM_ITEM_H - fm_sfh) / 2;
|
||||||
|
|
||||||
if (fm->rename_active && fm->rename_idx == i && ty >= list_y && ty + fm_sfh <= c.h) {
|
// The entry being renamed shows its rename box (drawn later).
|
||||||
// Rename textbox inline
|
if (!(fm->rename_active && fm->rename_idx == i)) {
|
||||||
int rw = cols.name_right - tx;
|
|
||||||
c.fill_rect(tx - 2, ty - 1, rw, fm_sfh + 2, colors::WHITE);
|
|
||||||
c.rect(tx - 2, ty - 1, rw, fm_sfh + 2, colors::ACCENT);
|
|
||||||
c.text(tx, ty, fm->rename_buf, colors::TEXT_COLOR);
|
|
||||||
// Cursor
|
|
||||||
char prefix[64];
|
|
||||||
int cpos = fm->rename_cursor;
|
|
||||||
if (cpos > 63) cpos = 63;
|
|
||||||
for (int k = 0; k < cpos; k++) prefix[k] = fm->rename_buf[k];
|
|
||||||
prefix[cpos] = '\0';
|
|
||||||
int cx = tx + text_width(prefix);
|
|
||||||
c.vline(cx, ty, fm_sfh, colors::ACCENT);
|
|
||||||
} else {
|
|
||||||
if (ty >= list_y && ty + fm_sfh <= c.h) {
|
if (ty >= list_y && ty + fm_sfh <= c.h) {
|
||||||
// Truncate name to fit the name column width
|
// Truncate name to fit the name column width
|
||||||
const char* name = fm->entry_names[i];
|
const char* name = fm->entry_names[i];
|
||||||
@@ -641,7 +643,7 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
|
|||||||
filemanager_type_label(fm, i, type_str, sizeof(type_str));
|
filemanager_type_label(fm, i, type_str, sizeof(type_str));
|
||||||
char type_fit[24];
|
char type_fit[24];
|
||||||
fit_label_with_ellipsis(type_fit, sizeof(type_fit), type_str,
|
fit_label_with_ellipsis(type_fit, sizeof(type_fit), type_str,
|
||||||
c.w - FM_SCROLLBAR_W - cols.type_x - 4);
|
l.view_x + l.view_w - cols.type_x - 4);
|
||||||
c.text(cols.type_x, ty, type_fit, dim);
|
c.text(cols.type_x, ty, type_fit, dim);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -667,6 +669,19 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
|
|||||||
c.fill_rect(sbx + 1, tty, sbw - 2, th, sb_fg_color);
|
c.fill_rect(sbx + 1, tty, sbw - 2, th, sb_fg_color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FileManagerLayout layout = filemanager_layout(fm, fm->grid_view, c.w);
|
||||||
|
// Dropping onto the folder itself (not onto one of its entries).
|
||||||
|
if (fm->drop_serial && fm->drop_action && fm->drop_target_idx < 0 && fm->drop_sidebar_idx < 0) {
|
||||||
|
Color accent = ds ? ds->settings.accent_color : colors::ACCENT;
|
||||||
|
c.rect(layout.view_x, layout.body_y, c.w - layout.view_x, c.h - layout.body_y, accent);
|
||||||
|
c.rect(layout.view_x + 1, layout.body_y + 1, c.w - layout.view_x - 2, c.h - layout.body_y - 2, accent);
|
||||||
|
}
|
||||||
|
Rect rename_box;
|
||||||
|
if (filemanager_rename_box(fm, layout, c.h, &rename_box))
|
||||||
|
filemanager_field_draw(c, fm->rename, rename_box,
|
||||||
|
ds ? ds->settings.accent_color : colors::ACCENT);
|
||||||
|
if (layout.sidebar_w > 0) filemanager_sidebar_draw(c, fm, layout);
|
||||||
|
|
||||||
// Repaint header on top of content to clip scrolled icon overflow
|
// Repaint header on top of content to clip scrolled icon overflow
|
||||||
if (fm->scrollbar.scroll_offset > 0)
|
if (fm->scrollbar.scroll_offset > 0)
|
||||||
filemanager_draw_header(c, fm, toolbar_color, btn_bg);
|
filemanager_draw_header(c, fm, toolbar_color, btn_bg);
|
||||||
@@ -682,7 +697,20 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
|
|||||||
mtk::draw_context_menu(c, filemanager_context_state(fm), items,
|
mtk::draw_context_menu(c, filemanager_context_state(fm), items,
|
||||||
fm->ctx_item_count, theme,
|
fm->ctx_item_count, theme,
|
||||||
filemanager_context_menu_width(items, fm->ctx_item_count));
|
filemanager_context_menu_width(items, fm->ctx_item_count));
|
||||||
|
|
||||||
|
mtk::ContextMenuState sub;
|
||||||
|
int sub_w = 0;
|
||||||
|
if (filemanager_new_submenu_layout(fm, c.w, c.h, &sub, &sub_w) && sub.open) {
|
||||||
|
mtk::ContextMenuItem sub_items[FM_CTX_MAX_ITEMS];
|
||||||
|
int sub_count = filemanager_new_submenu_items(fm, sub_items);
|
||||||
|
mtk::draw_context_menu(c, sub, sub_items, sub_count, theme, sub_w);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cut/Copy/Paste menus of the text fields.
|
||||||
|
Color accent = ds ? ds->settings.accent_color : colors::ACCENT;
|
||||||
|
if (fm->rename_active) filemanager_field_draw_menu(c, fm->rename, accent);
|
||||||
|
if (fm->pathbar_editing) filemanager_field_draw_menu(c, fm->pathbar, accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace filemanager
|
} // namespace filemanager
|
||||||
@@ -0,0 +1,287 @@
|
|||||||
|
/*
|
||||||
|
* sidebar.cpp
|
||||||
|
* Sidebar for the Files file manager: places, bookmarks and drives
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "filemanager_internal.hpp"
|
||||||
|
|
||||||
|
namespace filemanager {
|
||||||
|
|
||||||
|
static constexpr int SIDEBAR_MAX_ITEMS = 96;
|
||||||
|
|
||||||
|
static SvgIcon g_icon_computer;
|
||||||
|
static SvgIcon g_icon_bookmark;
|
||||||
|
|
||||||
|
static void sidebar_load_icons() {
|
||||||
|
if (g_icon_computer.pixels) return;
|
||||||
|
Color def = colors::ICON_COLOR;
|
||||||
|
g_icon_computer = svg_load("0:/os/icons/computer.svg", 16, 16, def);
|
||||||
|
g_icon_bookmark = svg_load("0:/os/icons/folder.svg", 16, 16, def);
|
||||||
|
}
|
||||||
|
|
||||||
|
static FileManagerSidebarItem* sidebar_add(FileManagerState* fm, FileManagerSidebarKind kind,
|
||||||
|
const char* label, const char* path) {
|
||||||
|
if (fm->sidebar_count >= SIDEBAR_MAX_ITEMS) return nullptr;
|
||||||
|
FileManagerSidebarItem* item = &fm->sidebar_items[fm->sidebar_count++];
|
||||||
|
montauk::memset(item, 0, sizeof(*item));
|
||||||
|
item->kind = kind;
|
||||||
|
item->special_index = -1;
|
||||||
|
montauk::strncpy(item->label, label, sizeof(item->label));
|
||||||
|
if (path) montauk::strncpy(item->path, path, sizeof(item->path));
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
void filemanager_sidebar_rebuild(FileManagerState* fm) {
|
||||||
|
if (!fm) return;
|
||||||
|
if (!fm->sidebar_items) {
|
||||||
|
fm->sidebar_items = (FileManagerSidebarItem*)montauk::malloc(
|
||||||
|
SIDEBAR_MAX_ITEMS * sizeof(FileManagerSidebarItem));
|
||||||
|
if (!fm->sidebar_items) return;
|
||||||
|
}
|
||||||
|
sidebar_load_icons();
|
||||||
|
ensure_filemanager_icons_loaded(fm->desktop);
|
||||||
|
fm->sidebar_count = 0;
|
||||||
|
DesktopState* ds = fm->desktop;
|
||||||
|
bool has_home = ds && ds->home_dir[0];
|
||||||
|
|
||||||
|
sidebar_add(fm, FM_SIDEBAR_HEADER, "Places", nullptr);
|
||||||
|
sidebar_add(fm, FM_SIDEBAR_COMPUTER, "Computer", nullptr);
|
||||||
|
if (has_home) {
|
||||||
|
sidebar_add(fm, FM_SIDEBAR_FOLDER, "Home", ds->home_dir);
|
||||||
|
for (int sf = 0; sf < SF_COUNT; sf++) {
|
||||||
|
char path[256];
|
||||||
|
filemanager_build_fullpath(path, sizeof(path), ds->home_dir, sf_names[sf]);
|
||||||
|
if (!filemanager_path_is_dir(path)) continue;
|
||||||
|
FileManagerSidebarItem* item = sidebar_add(fm, FM_SIDEBAR_FOLDER, sf_names[sf], path);
|
||||||
|
if (item) item->special_index = sf;
|
||||||
|
}
|
||||||
|
sidebar_add(fm, FM_SIDEBAR_TRASH, "Trash", nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prefs_bookmark_count() > 0) {
|
||||||
|
sidebar_add(fm, FM_SIDEBAR_HEADER, "Bookmarks", nullptr);
|
||||||
|
for (int i = 0; i < prefs_bookmark_count(); i++) {
|
||||||
|
const char* path = prefs_bookmark(i);
|
||||||
|
FileManagerSidebarItem* item = sidebar_add(fm, FM_SIDEBAR_FOLDER, path_basename(path), path);
|
||||||
|
if (item) item->bookmark = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int drives[FM_MAX_DRIVES];
|
||||||
|
int drive_count = montauk::drivelist(drives, FM_MAX_DRIVES);
|
||||||
|
if (drive_count > 0) sidebar_add(fm, FM_SIDEBAR_HEADER, "Drives", nullptr);
|
||||||
|
for (int i = 0; i < drive_count; i++) {
|
||||||
|
int d = drives[i];
|
||||||
|
char path[8];
|
||||||
|
snprintf(path, sizeof(path), "%d:/", d);
|
||||||
|
char label[64];
|
||||||
|
char volume_label[48];
|
||||||
|
if (montauk::drivelabel(d, volume_label, sizeof(volume_label)) > 0 && volume_label[0])
|
||||||
|
snprintf(label, sizeof(label), "%s", volume_label);
|
||||||
|
else
|
||||||
|
snprintf(label, sizeof(label), "Volume %d", d);
|
||||||
|
FileManagerSidebarItem* item = sidebar_add(fm, FM_SIDEBAR_DRIVE, label, path);
|
||||||
|
if (item) item->drive_kind = montauk::drivekind(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fm->sidebar_hover >= fm->sidebar_count) fm->sidebar_hover = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same folder, ignoring one trailing slash ("0:/a/" == "0:/a").
|
||||||
|
static bool same_path(const char* a, const char* b) {
|
||||||
|
int la = montauk::slen(a);
|
||||||
|
int lb = montauk::slen(b);
|
||||||
|
if (la > 3 && a[la - 1] == '/') la--;
|
||||||
|
if (lb > 3 && b[lb - 1] == '/') lb--;
|
||||||
|
if (la != lb) return false;
|
||||||
|
for (int i = 0; i < la; i++) {
|
||||||
|
if (a[i] != b[i]) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool sidebar_item_is_current(FileManagerState* fm, const FileManagerSidebarItem& item) {
|
||||||
|
switch (item.kind) {
|
||||||
|
case FM_SIDEBAR_COMPUTER: return filemanager_is_computer_view(fm);
|
||||||
|
case FM_SIDEBAR_TRASH: return filemanager_is_trash_view(fm);
|
||||||
|
case FM_SIDEBAR_FOLDER:
|
||||||
|
case FM_SIDEBAR_DRIVE:
|
||||||
|
return !filemanager_is_computer_view(fm) && !filemanager_is_virtual_view(fm) &&
|
||||||
|
same_path(item.path, fm->current_path);
|
||||||
|
default: return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static const SvgIcon* sidebar_icon(FileManagerState* fm, const FileManagerSidebarItem& item) {
|
||||||
|
DesktopState* ds = fm->desktop;
|
||||||
|
switch (item.kind) {
|
||||||
|
case FM_SIDEBAR_COMPUTER: return &g_icon_computer;
|
||||||
|
case FM_SIDEBAR_TRASH:
|
||||||
|
return filemanager_trash_has_items(ds) && ds->icon_trash_full.pixels
|
||||||
|
? &ds->icon_trash_full : &ds->icon_trash;
|
||||||
|
case FM_SIDEBAR_DRIVE:
|
||||||
|
return item.drive_kind == 4 && ds->icon_drive_usb.pixels ? &ds->icon_drive_usb : &ds->icon_drive;
|
||||||
|
case FM_SIDEBAR_FOLDER:
|
||||||
|
if (item.special_index >= 0) return &ds->icon_special_folder[item.special_index];
|
||||||
|
if (!item.bookmark && ds->home_dir[0] && same_path(item.path, ds->home_dir))
|
||||||
|
return &ds->icon_home_folder;
|
||||||
|
return &g_icon_bookmark;
|
||||||
|
default: return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int sidebar_visible_rows(const FileManagerLayout& l, int ch) {
|
||||||
|
int rows = (ch - l.body_y) / FM_SIDEBAR_ITEM_H;
|
||||||
|
return rows > 0 ? rows : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void sidebar_clamp_scroll(FileManagerState* fm, const FileManagerLayout& l, int ch) {
|
||||||
|
int max_scroll = fm->sidebar_count - sidebar_visible_rows(l, ch);
|
||||||
|
if (max_scroll < 0) max_scroll = 0;
|
||||||
|
if (fm->sidebar_scroll > max_scroll) fm->sidebar_scroll = max_scroll;
|
||||||
|
if (fm->sidebar_scroll < 0) fm->sidebar_scroll = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void filemanager_sidebar_draw(Canvas& c, FileManagerState* fm, const FileManagerLayout& l) {
|
||||||
|
if (l.sidebar_w <= 0) return;
|
||||||
|
if (!fm->sidebar_items) filemanager_sidebar_rebuild(fm);
|
||||||
|
sidebar_clamp_scroll(fm, l, c.h);
|
||||||
|
|
||||||
|
DesktopState* ds = fm->desktop;
|
||||||
|
Color accent = ds ? ds->settings.accent_color : colors::ACCENT;
|
||||||
|
Color muted = Color::from_rgb(0x88, 0x88, 0x88);
|
||||||
|
int fh = system_font_height();
|
||||||
|
|
||||||
|
c.fill_rect(0, l.body_y, l.sidebar_w, c.h - l.body_y, Color::from_rgb(0xF6, 0xF6, 0xF6));
|
||||||
|
c.vline(l.sidebar_w - 1, l.body_y, c.h - l.body_y, colors::BORDER);
|
||||||
|
|
||||||
|
int rows = sidebar_visible_rows(l, c.h);
|
||||||
|
for (int r = 0; r < rows; r++) {
|
||||||
|
int idx = fm->sidebar_scroll + r;
|
||||||
|
if (idx >= fm->sidebar_count) break;
|
||||||
|
const FileManagerSidebarItem& item = fm->sidebar_items[idx];
|
||||||
|
int y = l.body_y + r * FM_SIDEBAR_ITEM_H;
|
||||||
|
|
||||||
|
if (item.kind == FM_SIDEBAR_HEADER) {
|
||||||
|
c.text(10, y + FM_SIDEBAR_ITEM_H - fh - 2, item.label, muted);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int rw = l.sidebar_w - 9;
|
||||||
|
if (sidebar_item_is_current(fm, item)) {
|
||||||
|
c.fill_rounded_rect(4, y + 1, rw, FM_SIDEBAR_ITEM_H - 2, 4,
|
||||||
|
gui::mtk::accent_hover_tint(accent));
|
||||||
|
} else if (idx == fm->sidebar_hover) {
|
||||||
|
c.fill_rounded_rect(4, y + 1, rw, FM_SIDEBAR_ITEM_H - 2, 4, Color::from_rgb(0xEA, 0xEA, 0xEA));
|
||||||
|
}
|
||||||
|
if (idx == fm->drop_sidebar_idx && fm->drop_action) {
|
||||||
|
c.rect(4, y + 1, rw, FM_SIDEBAR_ITEM_H - 2, accent);
|
||||||
|
c.rect(5, y + 2, rw - 2, FM_SIDEBAR_ITEM_H - 4, accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
const SvgIcon* icon = sidebar_icon(fm, item);
|
||||||
|
if (icon && icon->pixels) c.icon(12, y + (FM_SIDEBAR_ITEM_H - icon->height) / 2, *icon);
|
||||||
|
char fit[64];
|
||||||
|
filemanager_fit_text(item.label, fit, sizeof(fit), l.sidebar_w - 40);
|
||||||
|
c.text(34, y + (FM_SIDEBAR_ITEM_H - fh) / 2, fit, colors::TEXT_COLOR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int filemanager_sidebar_item_at(FileManagerState* fm, const FileManagerLayout& l, int x, int y) {
|
||||||
|
if (l.sidebar_w <= 0 || x < 0 || x >= l.sidebar_w || y < l.body_y) return -1;
|
||||||
|
int idx = fm->sidebar_scroll + (y - l.body_y) / FM_SIDEBAR_ITEM_H;
|
||||||
|
if (idx < 0 || idx >= fm->sidebar_count) return -1;
|
||||||
|
return fm->sidebar_items[idx].kind == FM_SIDEBAR_HEADER ? -1 : idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
void filemanager_sidebar_open(FileManagerState* fm, int idx) {
|
||||||
|
if (idx < 0 || idx >= fm->sidebar_count) return;
|
||||||
|
const FileManagerSidebarItem& item = fm->sidebar_items[idx];
|
||||||
|
switch (item.kind) {
|
||||||
|
case FM_SIDEBAR_COMPUTER: filemanager_go_home(fm); break;
|
||||||
|
case FM_SIDEBAR_TRASH: filemanager_open_trash(fm); break;
|
||||||
|
case FM_SIDEBAR_FOLDER:
|
||||||
|
case FM_SIDEBAR_DRIVE: {
|
||||||
|
char path[256];
|
||||||
|
montauk::strncpy(path, item.path, sizeof(path));
|
||||||
|
if (!filemanager_path_is_dir(path)) {
|
||||||
|
filemanager_show_error(fm->desktop, fm, "This folder no longer exists", path,
|
||||||
|
montauk::Success);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
filemanager_go_to_path(fm, path);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool filemanager_sidebar_target_path(FileManagerState* fm, int idx, char* out, int out_max) {
|
||||||
|
if (idx < 0 || idx >= fm->sidebar_count) return false;
|
||||||
|
const FileManagerSidebarItem& item = fm->sidebar_items[idx];
|
||||||
|
if (item.kind == FM_SIDEBAR_TRASH) return filemanager_trash_files_dir(fm->desktop, out, out_max);
|
||||||
|
if (item.kind != FM_SIDEBAR_FOLDER && item.kind != FM_SIDEBAR_DRIVE) return false;
|
||||||
|
montauk::strncpy(out, item.path, out_max);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void set_window_cursor(Window* win, FileManagerState* fm, int style) {
|
||||||
|
if (fm->cursor_style == style) return;
|
||||||
|
fm->cursor_style = style;
|
||||||
|
montauk::win_setcursor(win->ext_win_id, style);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool filemanager_sidebar_resize_mouse(Window* win, FileManagerState* fm,
|
||||||
|
const FileManagerLayout& l, int x, int y, MouseEvent& ev) {
|
||||||
|
if (fm->sidebar_resizing) {
|
||||||
|
if (ev.left_held()) {
|
||||||
|
fm->sidebar_width = filemanager_sidebar_width(x - fm->sidebar_drag_dx,
|
||||||
|
win->content_rect().w);
|
||||||
|
} else {
|
||||||
|
fm->sidebar_resizing = false;
|
||||||
|
prefs_set_sidebar_width(fm->desktop, fm->sidebar_width);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The grip straddles the border line at sidebar_w - 1.
|
||||||
|
bool on_edge = l.sidebar_w > 0 && y >= l.body_y &&
|
||||||
|
x >= l.sidebar_w - FM_SIDEBAR_GRIP && x < l.sidebar_w + FM_SIDEBAR_GRIP;
|
||||||
|
set_window_cursor(win, fm, on_edge ? CURSOR_RESIZE_H : CURSOR_ARROW);
|
||||||
|
if (!on_edge || !ev.left_pressed()) return false;
|
||||||
|
|
||||||
|
fm->sidebar_resizing = true;
|
||||||
|
fm->sidebar_drag_dx = x - l.sidebar_w;
|
||||||
|
fm->sidebar_width = l.sidebar_w;
|
||||||
|
fm->sidebar_hover = -1;
|
||||||
|
if (fm->pathbar_editing) filemanager_cancel_pathbar(fm);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool filemanager_sidebar_mouse(FileManagerState* fm, const FileManagerLayout& l,
|
||||||
|
int x, int y, MouseEvent& ev, int ch) {
|
||||||
|
bool inside = x >= 0 && x < l.sidebar_w && y >= l.body_y;
|
||||||
|
if (!inside) {
|
||||||
|
fm->sidebar_hover = -1;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fm->sidebar_hover = filemanager_sidebar_item_at(fm, l, x, y);
|
||||||
|
|
||||||
|
if (ev.scroll != 0) {
|
||||||
|
fm->sidebar_scroll -= ev.scroll;
|
||||||
|
sidebar_clamp_scroll(fm, l, ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
int idx = fm->sidebar_hover;
|
||||||
|
if (ev.left_pressed() && idx >= 0) {
|
||||||
|
filemanager_sidebar_open(fm, idx);
|
||||||
|
} else if (ev.right_pressed() && idx >= 0) {
|
||||||
|
// Sidebar rows use negative context targets: -2 - row.
|
||||||
|
filemanager_open_ctx_menu(fm, x, y, -2 - idx);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace filemanager
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* stb_truetype_impl.cpp
|
||||||
|
* Single compilation unit for stb_truetype in MontaukOS freestanding environment
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <montauk/heap.h>
|
||||||
|
#include <montauk/string.h>
|
||||||
|
#include <gui/stb_math.h>
|
||||||
|
|
||||||
|
// Override all stb_truetype dependencies before including the implementation
|
||||||
|
|
||||||
|
#define STBTT_ifloor(x) ((int) stb_floor(x))
|
||||||
|
#define STBTT_iceil(x) ((int) stb_ceil(x))
|
||||||
|
#define STBTT_sqrt(x) stb_sqrt(x)
|
||||||
|
#define STBTT_pow(x,y) stb_pow(x,y)
|
||||||
|
#define STBTT_fmod(x,y) stb_fmod(x,y)
|
||||||
|
#define STBTT_cos(x) stb_cos(x)
|
||||||
|
#define STBTT_acos(x) stb_acos(x)
|
||||||
|
#define STBTT_fabs(x) stb_fabs(x)
|
||||||
|
|
||||||
|
#define STBTT_malloc(x,u) ((void)(u), montauk::malloc(x))
|
||||||
|
#define STBTT_free(x,u) ((void)(u), montauk::mfree(x))
|
||||||
|
|
||||||
|
#define STBTT_memcpy(d,s,n) montauk::memcpy(d,s,n)
|
||||||
|
#define STBTT_memset(d,v,n) montauk::memset(d,v,n)
|
||||||
|
|
||||||
|
#define STBTT_strlen(x) montauk::slen(x)
|
||||||
|
|
||||||
|
#define STBTT_assert(x) ((void)(x))
|
||||||
|
|
||||||
|
#define STB_TRUETYPE_IMPLEMENTATION
|
||||||
|
#include <gui/stb_truetype.h>
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
/*
|
||||||
|
* textfield.cpp
|
||||||
|
* Single-line text field for Files (the rename box and the path bar):
|
||||||
|
* editing comes from mtk::TextInputState (selection, clipboard, word
|
||||||
|
* jumps); this adds horizontal scrolling, clipped drawing and
|
||||||
|
* double/triple-click selection
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "filemanager_internal.hpp"
|
||||||
|
|
||||||
|
namespace filemanager {
|
||||||
|
|
||||||
|
static constexpr uint64_t MULTI_CLICK_MS = 400;
|
||||||
|
|
||||||
|
int filemanager_field_len(const FileManagerTextField& f) {
|
||||||
|
return mtk::text_input_len(f.buf, f.cap);
|
||||||
|
}
|
||||||
|
|
||||||
|
void filemanager_field_set(FileManagerTextField& f, const char* text, int cap, int pad,
|
||||||
|
int sel_start, int sel_end, mtk::TextInputCharFilter filter) {
|
||||||
|
f.cap = cap > (int)sizeof(f.buf) ? (int)sizeof(f.buf) : cap;
|
||||||
|
montauk::strncpy(f.buf, text ? text : "", f.cap);
|
||||||
|
f.pad = pad;
|
||||||
|
f.filter = filter;
|
||||||
|
f.scroll = 0;
|
||||||
|
f.clicks = 0;
|
||||||
|
f.last_click = 0;
|
||||||
|
int len = filemanager_field_len(f);
|
||||||
|
mtk::text_input_reset(f.in);
|
||||||
|
f.in.selection_anchor = mtk::text_input_clamp_cursor(sel_start, len);
|
||||||
|
f.in.cursor = mtk::text_input_clamp_cursor(sel_end, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
void filemanager_field_select(FileManagerTextField& f, int anchor, int cursor) {
|
||||||
|
int len = filemanager_field_len(f);
|
||||||
|
f.in.selection_anchor = mtk::text_input_clamp_cursor(anchor, len);
|
||||||
|
f.in.cursor = mtk::text_input_clamp_cursor(cursor, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool filemanager_field_key(FileManagerTextField& f, const montauk::abi::KeyEvent& key) {
|
||||||
|
int r = mtk::text_input_key(f.in, f.buf, f.cap, key, f.filter, nullptr);
|
||||||
|
return r != mtk::TEXT_INPUT_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Byte index under window x, given where the text starts.
|
||||||
|
static int index_at(const FileManagerTextField& f, const Rect& box, int mx) {
|
||||||
|
// text_input_index_from_x measures from bounds.x + 8.
|
||||||
|
Rect origin = {box.x + f.pad - f.scroll - 8, box.y, box.w, box.h};
|
||||||
|
return mtk::text_input_index_from_x(origin, f.buf, mx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The word (or run of separators) around byte i.
|
||||||
|
static void word_at(const FileManagerTextField& f, int i, int* start, int* end) {
|
||||||
|
int len = filemanager_field_len(f);
|
||||||
|
if (i >= len && i > 0) i = len - 1;
|
||||||
|
bool word = len > 0 && mtk::text_input_word_char(f.buf[i]);
|
||||||
|
int s = i, e = i;
|
||||||
|
while (s > 0 && mtk::text_input_word_char(f.buf[s - 1]) == word) s--;
|
||||||
|
while (e < len && mtk::text_input_word_char(f.buf[e]) == word) e++;
|
||||||
|
*start = s;
|
||||||
|
*end = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool filemanager_field_mouse(FileManagerTextField& f, const Rect& box, int mx, int my,
|
||||||
|
const MouseEvent& ev, int view_w, int view_h, bool shift) {
|
||||||
|
// Its Cut/Copy/Paste menu takes every event while open.
|
||||||
|
if (f.in.context.open) {
|
||||||
|
mtk::text_input_handle_mouse(f.in, box, f.buf, f.cap, mx, my, ev.buttons,
|
||||||
|
ev.prev_buttons, view_w, view_h, true, false,
|
||||||
|
f.filter, nullptr);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool inside = box.contains(mx, my);
|
||||||
|
int len = filemanager_field_len(f);
|
||||||
|
|
||||||
|
if (ev.left_pressed() && inside) {
|
||||||
|
int i = index_at(f, box, mx);
|
||||||
|
uint64_t now = montauk::get_milliseconds();
|
||||||
|
f.clicks = (now - f.last_click < MULTI_CLICK_MS && f.clicks < 3) ? f.clicks + 1 : 1;
|
||||||
|
f.last_click = now;
|
||||||
|
|
||||||
|
if (f.clicks == 2) {
|
||||||
|
int s, e;
|
||||||
|
word_at(f, i, &s, &e);
|
||||||
|
filemanager_field_select(f, s, e);
|
||||||
|
} else if (f.clicks == 3) {
|
||||||
|
filemanager_field_select(f, 0, len);
|
||||||
|
} else {
|
||||||
|
mtk::text_input_set_cursor(f.in, len, i, shift);
|
||||||
|
f.in.dragging = true;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (f.in.dragging) {
|
||||||
|
if (ev.left_held()) f.in.cursor = index_at(f, box, mx);
|
||||||
|
else f.in.dragging = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ev.right_pressed() && inside) {
|
||||||
|
// Right-click outside the selection moves the caret there first.
|
||||||
|
int i = index_at(f, box, mx);
|
||||||
|
int s = mtk::text_input_selection_start(f.in, len);
|
||||||
|
int e = mtk::text_input_selection_end(f.in, len);
|
||||||
|
if (s == e || i < s || i > e) mtk::text_input_set_cursor(f.in, len, i, false);
|
||||||
|
mtk::context_menu_open(f.in.context, mx, my);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void text_clipped(Canvas& c, int x, int y, const char* s, Color color, const Rect& clip) {
|
||||||
|
if (fonts::system_font && fonts::system_font->valid) {
|
||||||
|
fonts::system_font->draw_to_buffer_clipped(c.pixels, c.w, c.h, x, y, s, color,
|
||||||
|
fonts::UI_SIZE, clip.x, clip.y,
|
||||||
|
clip.w, clip.h);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
c.text(x, y, s, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
void filemanager_field_draw(Canvas& c, FileManagerTextField& f, const Rect& box, Color accent) {
|
||||||
|
int len = filemanager_field_len(f);
|
||||||
|
int fh = system_font_height();
|
||||||
|
Rect inner = {box.x + 1, box.y + 1, box.w - 2, box.h - 2};
|
||||||
|
int view_w = box.w - f.pad * 2;
|
||||||
|
if (view_w < 1) view_w = 1;
|
||||||
|
|
||||||
|
// Keep the caret in view, and no blank space after the text's end.
|
||||||
|
int caret = mtk::text_input_prefix_width(f.buf, f.in.cursor, false);
|
||||||
|
int total = text_width_n(f.buf, len);
|
||||||
|
if (caret - f.scroll > view_w - 1) f.scroll = caret - view_w + 1;
|
||||||
|
if (caret < f.scroll) f.scroll = caret;
|
||||||
|
if (f.scroll > total - view_w + 1) f.scroll = total - view_w + 1;
|
||||||
|
if (f.scroll < 0) f.scroll = 0;
|
||||||
|
|
||||||
|
c.fill_rect(box.x, box.y, box.w, box.h, colors::WHITE);
|
||||||
|
c.rect(box.x, box.y, box.w, box.h, accent);
|
||||||
|
|
||||||
|
int tx = box.x + f.pad - f.scroll;
|
||||||
|
int ty = box.y + (box.h - fh) / 2;
|
||||||
|
int s = mtk::text_input_selection_start(f.in, len);
|
||||||
|
int e = mtk::text_input_selection_end(f.in, len);
|
||||||
|
if (s != e) {
|
||||||
|
int sx = gui_max(tx + mtk::text_input_prefix_width(f.buf, s, false), inner.x);
|
||||||
|
int ex = gui_min(tx + mtk::text_input_prefix_width(f.buf, e, false), inner.x + inner.w);
|
||||||
|
if (ex > sx) c.fill_rect(sx, ty - 1, ex - sx, fh + 2, mtk::make_theme(accent).text_selection);
|
||||||
|
}
|
||||||
|
text_clipped(c, tx, ty, f.buf, colors::TEXT_COLOR, inner);
|
||||||
|
if (s == e) {
|
||||||
|
int cx = tx + caret;
|
||||||
|
if (cx >= inner.x && cx < inner.x + inner.w) c.vline(cx, ty - 1, fh + 2, colors::TEXT_COLOR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void filemanager_field_draw_menu(Canvas& c, const FileManagerTextField& f, Color accent) {
|
||||||
|
if (!f.in.context.open) return;
|
||||||
|
mtk::Theme theme = mtk::make_theme(accent);
|
||||||
|
theme.border = colors::BORDER;
|
||||||
|
theme.text = colors::TEXT_COLOR;
|
||||||
|
int len = filemanager_field_len(f);
|
||||||
|
mtk::draw_text_input_context_menu(c, f.in, theme,
|
||||||
|
mtk::text_input_selection_start(f.in, len) !=
|
||||||
|
mtk::text_input_selection_end(f.in, len));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace filemanager
|
||||||
@@ -0,0 +1,442 @@
|
|||||||
|
/*
|
||||||
|
* thumbnails.cpp
|
||||||
|
* Image thumbnails for the Files grid view, rendered on a worker thread
|
||||||
|
* and cached in memory and under <home>/.cache/thumbnails
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "filemanager_internal.hpp"
|
||||||
|
#include <montauk/thread.h>
|
||||||
|
|
||||||
|
// stb_image, built JPEG-only into lib/libjpeg (its header pulls in libc
|
||||||
|
// headers this freestanding build does not have, so declare what we use).
|
||||||
|
extern "C" {
|
||||||
|
unsigned char* stbi_load_from_memory(const unsigned char* buffer, int len, int* x, int* y,
|
||||||
|
int* channels_in_file, int desired_channels);
|
||||||
|
int stbi_info_from_memory(const unsigned char* buffer, int len, int* x, int* y, int* comp);
|
||||||
|
void stbi_image_free(void* retval_from_stbi_load);
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace filemanager {
|
||||||
|
|
||||||
|
static constexpr int THUMB_SIZE = FM_GRID_ICON; // fits the 48x48 icon box
|
||||||
|
static constexpr uint64_t THUMB_MAX_FILE = 32ull << 20; // skip larger images
|
||||||
|
static constexpr uint64_t THUMB_MAX_PIXELS = 40ull << 20; // decoded size limit
|
||||||
|
static constexpr int THUMB_MEMORY_CACHE = 256;
|
||||||
|
|
||||||
|
struct ThumbKey {
|
||||||
|
char path[256];
|
||||||
|
int64_t mtime;
|
||||||
|
uint64_t size;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ThumbJob {
|
||||||
|
ThumbKey key;
|
||||||
|
int idx; // entry index in the listing that asked
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ThumbDone {
|
||||||
|
uint32_t gen;
|
||||||
|
int idx;
|
||||||
|
ThumbKey key;
|
||||||
|
SvgIcon icon;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ThumbCacheEntry {
|
||||||
|
ThumbKey key;
|
||||||
|
SvgIcon icon;
|
||||||
|
uint64_t used; // LRU stamp
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FileManagerThumbs {
|
||||||
|
montauk::Mutex lock;
|
||||||
|
uint32_t wake; // futex word, bumped when work arrives or on quit
|
||||||
|
bool quit;
|
||||||
|
int tid;
|
||||||
|
char cache_dir[256];
|
||||||
|
|
||||||
|
// Under lock: the current request and the worker's output.
|
||||||
|
uint32_t gen;
|
||||||
|
ThumbJob* jobs;
|
||||||
|
int job_count;
|
||||||
|
int job_next;
|
||||||
|
ThumbDone* done;
|
||||||
|
int done_count;
|
||||||
|
int done_cap;
|
||||||
|
|
||||||
|
// Under lock: thumbnails kept from earlier folders.
|
||||||
|
ThumbCacheEntry cache[THUMB_MEMORY_CACHE];
|
||||||
|
int cache_count;
|
||||||
|
uint64_t cache_clock;
|
||||||
|
|
||||||
|
// UI thread only: thumbnails of the current listing, by entry index.
|
||||||
|
SvgIcon* by_entry;
|
||||||
|
ThumbKey* by_entry_key;
|
||||||
|
int by_entry_cap;
|
||||||
|
};
|
||||||
|
|
||||||
|
static bool key_equal(const ThumbKey& a, const ThumbKey& b) {
|
||||||
|
return a.mtime == b.mtime && a.size == b.size && montauk::streq(a.path, b.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool thumbable(const char* name) {
|
||||||
|
return str_ends_with(name, ".jpg") || str_ends_with(name, ".jpeg") || str_ends_with(name, ".svg");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Caches ---------------------------------------------------------------
|
||||||
|
|
||||||
|
// Takes ownership of icon. Caller holds the lock.
|
||||||
|
static void cache_put_locked(FileManagerThumbs* t, const ThumbKey& key, SvgIcon icon) {
|
||||||
|
for (int i = 0; i < t->cache_count; i++) {
|
||||||
|
if (key_equal(t->cache[i].key, key)) {
|
||||||
|
montauk::mfree(t->cache[i].icon.pixels);
|
||||||
|
t->cache[i].icon = icon;
|
||||||
|
t->cache[i].used = ++t->cache_clock;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int slot = t->cache_count;
|
||||||
|
if (slot == THUMB_MEMORY_CACHE) {
|
||||||
|
slot = 0;
|
||||||
|
for (int i = 1; i < t->cache_count; i++) {
|
||||||
|
if (t->cache[i].used < t->cache[slot].used) slot = i;
|
||||||
|
}
|
||||||
|
montauk::mfree(t->cache[slot].icon.pixels);
|
||||||
|
} else {
|
||||||
|
t->cache_count++;
|
||||||
|
}
|
||||||
|
t->cache[slot].key = key;
|
||||||
|
t->cache[slot].icon = icon;
|
||||||
|
t->cache[slot].used = ++t->cache_clock;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A copy of a cached thumbnail (the cache keeps its own). Caller holds the lock.
|
||||||
|
static bool cache_get_locked(FileManagerThumbs* t, const ThumbKey& key, SvgIcon* out) {
|
||||||
|
for (int i = 0; i < t->cache_count; i++) {
|
||||||
|
if (!key_equal(t->cache[i].key, key)) continue;
|
||||||
|
const SvgIcon& src = t->cache[i].icon;
|
||||||
|
uint64_t bytes = (uint64_t)src.width * src.height * 4;
|
||||||
|
out->pixels = (uint32_t*)montauk::malloc(bytes);
|
||||||
|
if (!out->pixels) return false;
|
||||||
|
montauk::memcpy(out->pixels, src.pixels, bytes);
|
||||||
|
out->width = src.width;
|
||||||
|
out->height = src.height;
|
||||||
|
t->cache[i].used = ++t->cache_clock;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint64_t key_hash(const ThumbKey& key) {
|
||||||
|
uint64_t h = 1469598103934665603ull;
|
||||||
|
for (const char* p = key.path; *p; p++) { h ^= (uint8_t)*p; h *= 1099511628211ull; }
|
||||||
|
for (int i = 0; i < 8; i++) { h ^= (uint8_t)((uint64_t)key.mtime >> (i * 8)); h *= 1099511628211ull; }
|
||||||
|
for (int i = 0; i < 8; i++) { h ^= (uint8_t)(key.size >> (i * 8)); h *= 1099511628211ull; }
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DiskThumbHeader {
|
||||||
|
char magic[4]; // "MTH1"
|
||||||
|
uint16_t width, height;
|
||||||
|
int64_t mtime;
|
||||||
|
uint64_t size;
|
||||||
|
uint64_t path_hash; // the full key hash, guards against name collisions
|
||||||
|
};
|
||||||
|
|
||||||
|
static void disk_path(const FileManagerThumbs* t, const ThumbKey& key, char* out, int out_max) {
|
||||||
|
char name[40];
|
||||||
|
snprintf(name, sizeof(name), "%x%x.thumb", (unsigned)(key_hash(key) >> 32), (unsigned)key_hash(key));
|
||||||
|
filemanager_build_fullpath(out, out_max, t->cache_dir, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool disk_load(const FileManagerThumbs* t, const ThumbKey& key, SvgIcon* out) {
|
||||||
|
if (!t->cache_dir[0]) return false;
|
||||||
|
char path[320];
|
||||||
|
disk_path(t, key, path, sizeof(path));
|
||||||
|
int fd = montauk::open(path);
|
||||||
|
if (fd < 0) return false;
|
||||||
|
DiskThumbHeader h;
|
||||||
|
bool ok = montauk::read(fd, (uint8_t*)&h, 0, sizeof(h)) == (int)sizeof(h) &&
|
||||||
|
h.magic[0] == 'M' && h.magic[1] == 'T' && h.magic[2] == 'H' && h.magic[3] == '1' &&
|
||||||
|
h.mtime == key.mtime && h.size == key.size && h.path_hash == key_hash(key) &&
|
||||||
|
h.width > 0 && h.height > 0 && h.width <= THUMB_SIZE && h.height <= THUMB_SIZE;
|
||||||
|
if (ok) {
|
||||||
|
uint64_t bytes = (uint64_t)h.width * h.height * 4;
|
||||||
|
out->pixels = (uint32_t*)montauk::malloc(bytes);
|
||||||
|
ok = out->pixels && montauk::read(fd, (uint8_t*)out->pixels, sizeof(h), bytes) == (int)bytes;
|
||||||
|
if (ok) {
|
||||||
|
out->width = h.width;
|
||||||
|
out->height = h.height;
|
||||||
|
} else {
|
||||||
|
montauk::mfree(out->pixels);
|
||||||
|
out->pixels = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
montauk::close(fd);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void disk_store(const FileManagerThumbs* t, const ThumbKey& key, const SvgIcon& icon) {
|
||||||
|
if (!t->cache_dir[0]) return;
|
||||||
|
char path[320];
|
||||||
|
disk_path(t, key, path, sizeof(path));
|
||||||
|
int fd = montauk::fcreate(path);
|
||||||
|
if (fd < 0) return;
|
||||||
|
DiskThumbHeader h = {{'M', 'T', 'H', '1'}, (uint16_t)icon.width, (uint16_t)icon.height,
|
||||||
|
key.mtime, key.size, key_hash(key)};
|
||||||
|
montauk::fwrite(fd, (const uint8_t*)&h, 0, sizeof(h));
|
||||||
|
montauk::fwrite(fd, (const uint8_t*)icon.pixels, sizeof(h), (uint64_t)icon.width * icon.height * 4);
|
||||||
|
montauk::close(fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Rendering --------------------------------------------------------------
|
||||||
|
|
||||||
|
// Box-filter RGBA (stb order) down to fit THUMB_SIZE, keeping the aspect
|
||||||
|
// ratio; returns ARGB pixels.
|
||||||
|
static SvgIcon scale_rgba(const uint8_t* rgba, int w, int h) {
|
||||||
|
SvgIcon out = {nullptr, 0, 0};
|
||||||
|
int tw = THUMB_SIZE, th = THUMB_SIZE;
|
||||||
|
if (w >= h) th = (int)((int64_t)h * THUMB_SIZE / w);
|
||||||
|
else tw = (int)((int64_t)w * THUMB_SIZE / h);
|
||||||
|
if (tw < 1) tw = 1;
|
||||||
|
if (th < 1) th = 1;
|
||||||
|
if (w < tw) tw = w; // never enlarge
|
||||||
|
if (h < th) th = h;
|
||||||
|
|
||||||
|
out.pixels = (uint32_t*)montauk::malloc((uint64_t)tw * th * 4);
|
||||||
|
if (!out.pixels) return out;
|
||||||
|
for (int y = 0; y < th; y++) {
|
||||||
|
int y0 = (int)((int64_t)y * h / th);
|
||||||
|
int y1 = (int)((int64_t)(y + 1) * h / th);
|
||||||
|
if (y1 <= y0) y1 = y0 + 1;
|
||||||
|
for (int x = 0; x < tw; x++) {
|
||||||
|
int x0 = (int)((int64_t)x * w / tw);
|
||||||
|
int x1 = (int)((int64_t)(x + 1) * w / tw);
|
||||||
|
if (x1 <= x0) x1 = x0 + 1;
|
||||||
|
uint64_t r = 0, g = 0, b = 0, a = 0, n = 0;
|
||||||
|
// Sample at most 8x8 source pixels per output pixel: plenty for a
|
||||||
|
// thumbnail and bounded for huge images.
|
||||||
|
int sx = (x1 - x0 + 7) / 8, sy = (y1 - y0 + 7) / 8;
|
||||||
|
for (int yy = y0; yy < y1; yy += sy) {
|
||||||
|
const uint8_t* row = rgba + ((uint64_t)yy * w) * 4;
|
||||||
|
for (int xx = x0; xx < x1; xx += sx) {
|
||||||
|
const uint8_t* p = row + (uint64_t)xx * 4;
|
||||||
|
r += p[0]; g += p[1]; b += p[2]; a += p[3]; n++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.pixels[y * tw + x] = ((uint32_t)(a / n) << 24) | ((uint32_t)(r / n) << 16) |
|
||||||
|
((uint32_t)(g / n) << 8) | (uint32_t)(b / n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.width = tw;
|
||||||
|
out.height = th;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static SvgIcon render_thumb(const ThumbKey& key) {
|
||||||
|
SvgIcon none = {nullptr, 0, 0};
|
||||||
|
if (key.size == 0 || key.size > THUMB_MAX_FILE) return none;
|
||||||
|
if (str_ends_with(key.path, ".svg"))
|
||||||
|
return svg_load(key.path, THUMB_SIZE, THUMB_SIZE, colors::ICON_COLOR);
|
||||||
|
|
||||||
|
int fd = montauk::open(key.path);
|
||||||
|
if (fd < 0) return none;
|
||||||
|
uint8_t* data = (uint8_t*)montauk::malloc(key.size);
|
||||||
|
bool ok = data && montauk::read(fd, data, 0, key.size) == (int)key.size;
|
||||||
|
montauk::close(fd);
|
||||||
|
SvgIcon out = none;
|
||||||
|
int w = 0, h = 0, comp = 0;
|
||||||
|
if (ok && stbi_info_from_memory(data, (int)key.size, &w, &h, &comp) &&
|
||||||
|
w > 0 && h > 0 && (uint64_t)w * h <= THUMB_MAX_PIXELS) {
|
||||||
|
uint8_t* rgba = stbi_load_from_memory(data, (int)key.size, &w, &h, &comp, 4);
|
||||||
|
if (rgba) {
|
||||||
|
out = scale_rgba(rgba, w, h);
|
||||||
|
stbi_image_free(rgba);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
montauk::mfree(data);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Worker ---------------------------------------------------------------
|
||||||
|
|
||||||
|
static int thumbs_worker(void* arg) {
|
||||||
|
FileManagerThumbs* t = (FileManagerThumbs*)arg;
|
||||||
|
for (;;) {
|
||||||
|
uint32_t seen = __atomic_load_n(&t->wake, __ATOMIC_ACQUIRE);
|
||||||
|
|
||||||
|
t->lock.lock();
|
||||||
|
if (t->quit) { t->lock.unlock(); break; }
|
||||||
|
bool have = t->job_next < t->job_count;
|
||||||
|
ThumbJob job;
|
||||||
|
uint32_t gen = t->gen;
|
||||||
|
if (have) job = t->jobs[t->job_next++];
|
||||||
|
SvgIcon icon = {nullptr, 0, 0};
|
||||||
|
bool cached = have && cache_get_locked(t, job.key, &icon);
|
||||||
|
t->lock.unlock();
|
||||||
|
|
||||||
|
if (!have) {
|
||||||
|
montauk::futex_wait(&t->wake, seen);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cached && !disk_load(t, job.key, &icon)) {
|
||||||
|
icon = render_thumb(job.key);
|
||||||
|
if (icon.pixels) disk_store(t, job.key, icon);
|
||||||
|
}
|
||||||
|
if (!icon.pixels) continue;
|
||||||
|
|
||||||
|
t->lock.lock();
|
||||||
|
if (gen == t->gen && !t->quit) {
|
||||||
|
if (t->done_count == t->done_cap) {
|
||||||
|
int cap = t->done_cap ? t->done_cap * 2 : 32;
|
||||||
|
auto* grown = (ThumbDone*)montauk::realloc(t->done, (uint64_t)cap * sizeof(ThumbDone));
|
||||||
|
if (grown) { t->done = grown; t->done_cap = cap; }
|
||||||
|
}
|
||||||
|
if (t->done_count < t->done_cap) {
|
||||||
|
t->done[t->done_count++] = {gen, job.idx, job.key, icon};
|
||||||
|
icon.pixels = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t->lock.unlock();
|
||||||
|
montauk::mfree(icon.pixels); // stale or no room
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static FileManagerThumbs* thumbs_state(FileManagerState* fm) {
|
||||||
|
if (fm->thumbs) return fm->thumbs;
|
||||||
|
auto* t = (FileManagerThumbs*)montauk::malloc(sizeof(FileManagerThumbs));
|
||||||
|
if (!t) return nullptr;
|
||||||
|
montauk::memset(t, 0, sizeof(*t));
|
||||||
|
DesktopState* ds = fm->desktop;
|
||||||
|
if (ds && ds->home_dir[0]) {
|
||||||
|
char cache[256];
|
||||||
|
filemanager_build_fullpath(cache, sizeof(cache), ds->home_dir, ".cache");
|
||||||
|
montauk::fmkdir(cache);
|
||||||
|
filemanager_build_fullpath(t->cache_dir, sizeof(t->cache_dir), cache, "thumbnails");
|
||||||
|
montauk::fmkdir(t->cache_dir);
|
||||||
|
if (!filemanager_path_is_dir(t->cache_dir)) t->cache_dir[0] = '\0';
|
||||||
|
}
|
||||||
|
t->tid = montauk::thread_spawn(thumbs_worker, t, 128 * 1024);
|
||||||
|
if (t->tid <= 0) {
|
||||||
|
montauk::mfree(t);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
fm->thumbs = t;
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- UI side ----------------------------------------------------------------
|
||||||
|
|
||||||
|
void filemanager_thumbs_request(FileManagerState* fm) {
|
||||||
|
if (!fm || fm->headless) return;
|
||||||
|
FileManagerThumbs* t = fm->thumbs;
|
||||||
|
|
||||||
|
// Collect this listing's images before touching shared state.
|
||||||
|
bool listing = !filemanager_is_computer_view(fm) && !filemanager_is_virtual_view(fm);
|
||||||
|
int n = 0;
|
||||||
|
ThumbJob* jobs = nullptr;
|
||||||
|
if (listing && fm->entry_count > 0) {
|
||||||
|
for (int i = 0; i < fm->entry_count; i++) {
|
||||||
|
if (!fm->is_dir[i] && thumbable(fm->entry_names[i])) n++;
|
||||||
|
}
|
||||||
|
if (n > 0) {
|
||||||
|
if (!t) t = thumbs_state(fm);
|
||||||
|
if (!t) return;
|
||||||
|
jobs = (ThumbJob*)montauk::malloc((uint64_t)n * sizeof(ThumbJob));
|
||||||
|
if (!jobs) n = 0;
|
||||||
|
int j = 0;
|
||||||
|
for (int i = 0; i < fm->entry_count && j < n; i++) {
|
||||||
|
if (fm->is_dir[i] || !thumbable(fm->entry_names[i])) continue;
|
||||||
|
filemanager_build_fullpath(jobs[j].key.path, 256, fm->current_path, fm->entry_names[i]);
|
||||||
|
jobs[j].key.mtime = fm->entry_mtimes[i];
|
||||||
|
jobs[j].key.size = fm->entry_sizes[i];
|
||||||
|
jobs[j].idx = i;
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!t) return;
|
||||||
|
|
||||||
|
t->lock.lock();
|
||||||
|
// Thumbnails already shown move into the cache, so going back to a
|
||||||
|
// folder (or re-sorting this one) is instant.
|
||||||
|
for (int i = 0; i < t->by_entry_cap; i++) {
|
||||||
|
if (!t->by_entry[i].pixels) continue;
|
||||||
|
cache_put_locked(t, t->by_entry_key[i], t->by_entry[i]);
|
||||||
|
t->by_entry[i] = {nullptr, 0, 0};
|
||||||
|
}
|
||||||
|
for (int i = 0; i < t->done_count; i++) montauk::mfree(t->done[i].icon.pixels);
|
||||||
|
t->done_count = 0;
|
||||||
|
montauk::mfree(t->jobs);
|
||||||
|
t->jobs = jobs;
|
||||||
|
t->job_count = n;
|
||||||
|
t->job_next = 0;
|
||||||
|
t->gen++;
|
||||||
|
t->lock.unlock();
|
||||||
|
|
||||||
|
__atomic_add_fetch(&t->wake, 1, __ATOMIC_RELEASE);
|
||||||
|
montauk::futex_wake(&t->wake);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool filemanager_thumbs_poll(FileManagerState* fm) {
|
||||||
|
FileManagerThumbs* t = fm ? fm->thumbs : nullptr;
|
||||||
|
if (!t) return false;
|
||||||
|
|
||||||
|
t->lock.lock();
|
||||||
|
bool any = false;
|
||||||
|
for (int i = 0; i < t->done_count; i++) {
|
||||||
|
ThumbDone& d = t->done[i];
|
||||||
|
if (d.gen != t->gen || d.idx < 0 || d.idx >= fm->entry_count) {
|
||||||
|
montauk::mfree(d.icon.pixels);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (d.idx >= t->by_entry_cap) {
|
||||||
|
int cap = fm->entry_cap > d.idx ? fm->entry_cap : d.idx + 1;
|
||||||
|
auto* icons = (SvgIcon*)montauk::realloc(t->by_entry, (uint64_t)cap * sizeof(SvgIcon));
|
||||||
|
auto* keys = icons ? (ThumbKey*)montauk::realloc(t->by_entry_key, (uint64_t)cap * sizeof(ThumbKey)) : nullptr;
|
||||||
|
if (icons) t->by_entry = icons;
|
||||||
|
if (keys) t->by_entry_key = keys;
|
||||||
|
if (!icons || !keys) { montauk::mfree(d.icon.pixels); continue; }
|
||||||
|
for (int k = t->by_entry_cap; k < cap; k++) t->by_entry[k] = {nullptr, 0, 0};
|
||||||
|
t->by_entry_cap = cap;
|
||||||
|
}
|
||||||
|
montauk::mfree(t->by_entry[d.idx].pixels);
|
||||||
|
t->by_entry[d.idx] = d.icon;
|
||||||
|
t->by_entry_key[d.idx] = d.key;
|
||||||
|
any = true;
|
||||||
|
}
|
||||||
|
t->done_count = 0;
|
||||||
|
t->lock.unlock();
|
||||||
|
return any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SvgIcon* filemanager_thumb_for(FileManagerState* fm, int idx) {
|
||||||
|
FileManagerThumbs* t = fm ? fm->thumbs : nullptr;
|
||||||
|
if (!t || idx < 0 || idx >= t->by_entry_cap || !t->by_entry[idx].pixels) return nullptr;
|
||||||
|
return &t->by_entry[idx];
|
||||||
|
}
|
||||||
|
|
||||||
|
void filemanager_thumbs_free(FileManagerState* fm) {
|
||||||
|
FileManagerThumbs* t = fm ? fm->thumbs : nullptr;
|
||||||
|
if (!t) return;
|
||||||
|
t->lock.lock();
|
||||||
|
t->quit = true;
|
||||||
|
t->lock.unlock();
|
||||||
|
__atomic_add_fetch(&t->wake, 1, __ATOMIC_RELEASE);
|
||||||
|
montauk::futex_wake(&t->wake);
|
||||||
|
montauk::thread_join(t->tid, nullptr);
|
||||||
|
|
||||||
|
for (int i = 0; i < t->by_entry_cap; i++) montauk::mfree(t->by_entry[i].pixels);
|
||||||
|
for (int i = 0; i < t->done_count; i++) montauk::mfree(t->done[i].icon.pixels);
|
||||||
|
for (int i = 0; i < t->cache_count; i++) montauk::mfree(t->cache[i].icon.pixels);
|
||||||
|
montauk::mfree(t->by_entry);
|
||||||
|
montauk::mfree(t->by_entry_key);
|
||||||
|
montauk::mfree(t->done);
|
||||||
|
montauk::mfree(t->jobs);
|
||||||
|
montauk::mfree(t);
|
||||||
|
fm->thumbs = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace filemanager
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* trash.cpp
|
* trash.cpp
|
||||||
* Per-user Trash for the embedded desktop file manager
|
* Per-user Trash for the Files file manager
|
||||||
* Copyright (c) 2026 Daniel Hammer
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -156,7 +156,8 @@ void filemanager_trash_forget(const char* trashed) {
|
|||||||
if (trash_info_path_of(trashed, info, sizeof(info))) montauk::fdelete(info);
|
if (trash_info_path_of(trashed, info, sizeof(info))) montauk::fdelete(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
montauk::StatusCode filemanager_trash_item(const DesktopState* ds, const char* path) {
|
montauk::StatusCode filemanager_trash_item(const DesktopState* ds, const char* path,
|
||||||
|
char* trashed_out, int trashed_max) {
|
||||||
char files[256], info_dir[256];
|
char files[256], info_dir[256];
|
||||||
if (!path || !path[0] || !filemanager_trash_files_dir(ds, files, sizeof(files)) ||
|
if (!path || !path[0] || !filemanager_trash_files_dir(ds, files, sizeof(files)) ||
|
||||||
!trash_info_dir(ds, info_dir, sizeof(info_dir)))
|
!trash_info_dir(ds, info_dir, sizeof(info_dir)))
|
||||||
@@ -187,6 +188,7 @@ montauk::StatusCode filemanager_trash_item(const DesktopState* ds, const char* p
|
|||||||
|
|
||||||
montauk::StatusCode status = filemanager_status_from_result(montauk::frename(path, dest));
|
montauk::StatusCode status = filemanager_status_from_result(montauk::frename(path, dest));
|
||||||
if (status != montauk::Success) montauk::fdelete(info);
|
if (status != montauk::Success) montauk::fdelete(info);
|
||||||
|
else if (trashed_out && trashed_max > 0) montauk::strncpy(trashed_out, dest, trashed_max);
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,337 @@
|
|||||||
|
/*
|
||||||
|
* undo.cpp
|
||||||
|
* Undo and redo for the Files file manager
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "filemanager_internal.hpp"
|
||||||
|
|
||||||
|
namespace filemanager {
|
||||||
|
|
||||||
|
// Each step remembers the paths an operation touched: renames, moves and
|
||||||
|
// trashing are undone by moving things back, copies by trashing the copies,
|
||||||
|
// a new folder by removing it while still empty, a new file while still
|
||||||
|
// untouched. Before acting, every step
|
||||||
|
// checks the files are still where the operation left them and refuses
|
||||||
|
// (with a message) rather than guess.
|
||||||
|
|
||||||
|
static constexpr int UNDO_DEPTH = 20;
|
||||||
|
static constexpr int UNDO_MAX_ITEMS = 2048;
|
||||||
|
|
||||||
|
struct UndoStep {
|
||||||
|
FileManagerUndoKind kind;
|
||||||
|
int count;
|
||||||
|
char (*from)[256];
|
||||||
|
char (*to)[256];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FileManagerUndo {
|
||||||
|
UndoStep undo[UNDO_DEPTH];
|
||||||
|
int undo_count;
|
||||||
|
UndoStep redo[UNDO_DEPTH];
|
||||||
|
int redo_count;
|
||||||
|
};
|
||||||
|
|
||||||
|
static void step_free(UndoStep& step) {
|
||||||
|
montauk::mfree(step.from);
|
||||||
|
montauk::mfree(step.to);
|
||||||
|
step.from = nullptr;
|
||||||
|
step.to = nullptr;
|
||||||
|
step.count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool step_make(UndoStep& step, FileManagerUndoKind kind, const char* const* from,
|
||||||
|
const char* const* to, int count) {
|
||||||
|
step.kind = kind;
|
||||||
|
step.count = count;
|
||||||
|
step.from = (char(*)[256])montauk::malloc((uint64_t)count * 256);
|
||||||
|
step.to = (char(*)[256])montauk::malloc((uint64_t)count * 256);
|
||||||
|
if (!step.from || !step.to) {
|
||||||
|
step_free(step);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
montauk::strncpy(step.from[i], from ? from[i] : "", 256);
|
||||||
|
montauk::strncpy(step.to[i], to[i], 256);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void stack_push(UndoStep* stack, int* count, UndoStep& step) {
|
||||||
|
if (*count == UNDO_DEPTH) {
|
||||||
|
step_free(stack[0]);
|
||||||
|
for (int i = 1; i < UNDO_DEPTH; i++) stack[i - 1] = stack[i];
|
||||||
|
(*count)--;
|
||||||
|
}
|
||||||
|
stack[(*count)++] = step;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void stack_clear(UndoStep* stack, int* count) {
|
||||||
|
for (int i = 0; i < *count; i++) step_free(stack[i]);
|
||||||
|
*count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static FileManagerUndo* undo_state(FileManagerState* fm) {
|
||||||
|
if (!fm->undo) {
|
||||||
|
fm->undo = (FileManagerUndo*)montauk::malloc(sizeof(FileManagerUndo));
|
||||||
|
if (fm->undo) montauk::memset(fm->undo, 0, sizeof(FileManagerUndo));
|
||||||
|
}
|
||||||
|
return fm->undo;
|
||||||
|
}
|
||||||
|
|
||||||
|
void filemanager_undo_record(FileManagerState* fm, FileManagerUndoKind kind,
|
||||||
|
const char* const* from, const char* const* to, int count) {
|
||||||
|
if (!fm || count <= 0 || count > UNDO_MAX_ITEMS) return;
|
||||||
|
FileManagerUndo* u = undo_state(fm);
|
||||||
|
if (!u) return;
|
||||||
|
UndoStep step = {};
|
||||||
|
if (!step_make(step, kind, from, to, count)) return;
|
||||||
|
stack_push(u->undo, &u->undo_count, step);
|
||||||
|
stack_clear(u->redo, &u->redo_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool filemanager_can_undo(const FileManagerState* fm) {
|
||||||
|
return fm && fm->undo && fm->undo->undo_count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool filemanager_can_redo(const FileManagerState* fm) {
|
||||||
|
return fm && fm->undo && fm->undo->redo_count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char* kind_name(FileManagerUndoKind kind) {
|
||||||
|
switch (kind) {
|
||||||
|
case FM_UNDO_RENAME: return "Rename";
|
||||||
|
case FM_UNDO_NEW_FOLDER: return "New Folder";
|
||||||
|
case FM_UNDO_NEW_FILE: return "New File";
|
||||||
|
case FM_UNDO_MOVE: return "Move";
|
||||||
|
case FM_UNDO_COPY: return "Copy";
|
||||||
|
case FM_UNDO_TRASH: return "Move to Trash";
|
||||||
|
default: return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* filemanager_undo_label(const FileManagerState* fm) {
|
||||||
|
static char label[48];
|
||||||
|
if (!filemanager_can_undo(fm)) return "Undo";
|
||||||
|
snprintf(label, sizeof(label), "Undo %s", kind_name(fm->undo->undo[fm->undo->undo_count - 1].kind));
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* filemanager_redo_label(const FileManagerState* fm) {
|
||||||
|
static char label[48];
|
||||||
|
if (!filemanager_can_redo(fm)) return "Redo";
|
||||||
|
snprintf(label, sizeof(label), "Redo %s", kind_name(fm->undo->redo[fm->undo->redo_count - 1].kind));
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
void filemanager_undo_free(FileManagerState* fm) {
|
||||||
|
if (!fm || !fm->undo) return;
|
||||||
|
stack_clear(fm->undo->undo, &fm->undo->undo_count);
|
||||||
|
stack_clear(fm->undo->redo, &fm->undo->redo_count);
|
||||||
|
montauk::mfree(fm->undo);
|
||||||
|
fm->undo = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Applying a step ----------------------------------------------------
|
||||||
|
|
||||||
|
static void show_refusal(FileManagerState* fm, bool undo, const UndoStep& step, const char* why,
|
||||||
|
const char* path, montauk::StatusCode status = montauk::Success) {
|
||||||
|
char heading[192];
|
||||||
|
snprintf(heading, sizeof(heading), "Couldn't %s %s: %s", undo ? "undo" : "redo",
|
||||||
|
kind_name(step.kind), why);
|
||||||
|
filemanager_show_error(fm->desktop, fm, heading, path, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move every src[i] to dst[i]: renames where possible (instant, same volume),
|
||||||
|
// a background job for the rest. Returns false (after telling the user) if
|
||||||
|
// anything is not where the step expects it.
|
||||||
|
static bool move_pairs(FileManagerState* fm, bool undo, const UndoStep& step,
|
||||||
|
char (*src)[256], char (*dst)[256]) {
|
||||||
|
for (int i = 0; i < step.count; i++) {
|
||||||
|
if (!filemanager_path_exists(src[i])) {
|
||||||
|
show_refusal(fm, undo, step, "an item is no longer there", src[i]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (filemanager_path_exists(dst[i])) {
|
||||||
|
show_refusal(fm, undo, step, "something else now has its name", dst[i]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char** rest_from = (const char**)montauk::malloc((uint64_t)step.count * sizeof(char*));
|
||||||
|
const char** rest_to = (const char**)montauk::malloc((uint64_t)step.count * sizeof(char*));
|
||||||
|
if (!rest_from || !rest_to) {
|
||||||
|
montauk::mfree(rest_from);
|
||||||
|
montauk::mfree(rest_to);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int rest = 0;
|
||||||
|
for (int i = 0; i < step.count; i++) {
|
||||||
|
montauk::StatusCode st = filemanager_status_from_result(montauk::frename(src[i], dst[i]));
|
||||||
|
if (st == montauk::Success) continue;
|
||||||
|
if (filemanager_status_is_access_denied(st)) {
|
||||||
|
show_refusal(fm, undo, step, "access denied", src[i], st);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
rest_from[rest] = src[i];
|
||||||
|
rest_to[rest] = dst[i];
|
||||||
|
rest++;
|
||||||
|
}
|
||||||
|
// Different volumes: copy + delete in the background.
|
||||||
|
if (rest > 0) filemanager_start_pairs_job(fm, true, rest_from, rest_to, rest, false);
|
||||||
|
montauk::mfree(rest_from);
|
||||||
|
montauk::mfree(rest_to);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the copies a COPY step made: to the Trash where possible, deleted
|
||||||
|
// outright on drives without one (they are the copies this step created).
|
||||||
|
static bool remove_copies(FileManagerState* fm, const UndoStep& step) {
|
||||||
|
for (int i = 0; i < step.count; i++) {
|
||||||
|
if (!filemanager_path_exists(step.to[i])) {
|
||||||
|
show_refusal(fm, true, step, "a copy is no longer there", step.to[i]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const char** rest = (const char**)montauk::malloc((uint64_t)step.count * sizeof(char*));
|
||||||
|
if (!rest) return false;
|
||||||
|
int n = 0;
|
||||||
|
for (int i = 0; i < step.count; i++) {
|
||||||
|
if (filemanager_trash_item(fm->desktop, step.to[i]) != montauk::Success) rest[n++] = step.to[i];
|
||||||
|
}
|
||||||
|
if (n > 0) filemanager_start_delete_job(fm, rest, n);
|
||||||
|
montauk::mfree(rest);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool apply_step(FileManagerState* fm, const UndoStep& step, bool undo) {
|
||||||
|
switch (step.kind) {
|
||||||
|
case FM_UNDO_RENAME:
|
||||||
|
case FM_UNDO_MOVE:
|
||||||
|
return undo ? move_pairs(fm, true, step, step.to, step.from)
|
||||||
|
: move_pairs(fm, false, step, step.from, step.to);
|
||||||
|
|
||||||
|
case FM_UNDO_NEW_FOLDER: {
|
||||||
|
const char* path = step.to[0];
|
||||||
|
if (undo) {
|
||||||
|
if (!filemanager_path_is_dir(path)) {
|
||||||
|
show_refusal(fm, true, step, "the folder is no longer there", path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
montauk::StatusCode st = filemanager_status_from_result(montauk::fdelete(path));
|
||||||
|
if (st != montauk::Success) {
|
||||||
|
show_refusal(fm, true, step, "the folder isn't empty", path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (filemanager_path_exists(path)) {
|
||||||
|
show_refusal(fm, false, step, "something else now has its name", path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
montauk::StatusCode st = filemanager_status_from_result(montauk::fmkdir(path));
|
||||||
|
if (st != montauk::Success) show_refusal(fm, false, step, "it could not be created", path, st);
|
||||||
|
return st == montauk::Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
case FM_UNDO_NEW_FILE: {
|
||||||
|
const char* path = step.to[0];
|
||||||
|
if (undo) {
|
||||||
|
if (!filemanager_path_exists(path)) {
|
||||||
|
show_refusal(fm, true, step, "the file is no longer there", path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!filemanager_new_file_unchanged(path, step.from[0])) {
|
||||||
|
show_refusal(fm, true, step, "the file has been changed", path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
montauk::StatusCode st = filemanager_status_from_result(montauk::fdelete(path));
|
||||||
|
if (st != montauk::Success) show_refusal(fm, true, step, "it could not be removed", path, st);
|
||||||
|
return st == montauk::Success;
|
||||||
|
}
|
||||||
|
if (filemanager_path_exists(path)) {
|
||||||
|
show_refusal(fm, false, step, "something else now has its name", path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
montauk::StatusCode st = filemanager_new_file_write(path, step.from[0]);
|
||||||
|
if (st != montauk::Success) show_refusal(fm, false, step, "it could not be created", path, st);
|
||||||
|
return st == montauk::Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
case FM_UNDO_COPY:
|
||||||
|
if (undo) return remove_copies(fm, step);
|
||||||
|
for (int i = 0; i < step.count; i++) {
|
||||||
|
if (filemanager_path_exists(step.to[i])) {
|
||||||
|
show_refusal(fm, false, step, "something else now has its name", step.to[i]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const char** from = (const char**)montauk::malloc((uint64_t)step.count * sizeof(char*));
|
||||||
|
const char** to = (const char**)montauk::malloc((uint64_t)step.count * sizeof(char*));
|
||||||
|
bool ok = from && to;
|
||||||
|
if (ok) {
|
||||||
|
for (int i = 0; i < step.count; i++) { from[i] = step.from[i]; to[i] = step.to[i]; }
|
||||||
|
ok = filemanager_start_pairs_job(fm, false, from, to, step.count, false);
|
||||||
|
}
|
||||||
|
montauk::mfree(from);
|
||||||
|
montauk::mfree(to);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
case FM_UNDO_TRASH:
|
||||||
|
if (undo) {
|
||||||
|
for (int i = 0; i < step.count; i++) {
|
||||||
|
if (!filemanager_path_exists(step.to[i])) {
|
||||||
|
show_refusal(fm, true, step, "an item is no longer in the Trash", step.to[i]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int i = 0; i < step.count; i++) {
|
||||||
|
montauk::StatusCode st = filemanager_trash_restore(step.to[i], nullptr, 0);
|
||||||
|
if (st != montauk::Success) show_refusal(fm, true, step, "restoring failed", step.to[i], st);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < step.count; i++) {
|
||||||
|
// Trashing again may pick a different name in the Trash.
|
||||||
|
montauk::StatusCode st = filemanager_trash_item(fm->desktop, step.from[i],
|
||||||
|
step.to[i], 256);
|
||||||
|
if (st != montauk::Success) show_refusal(fm, false, step, "it could not be trashed", step.from[i], st);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void undo_redo(FileManagerState* fm, bool undo) {
|
||||||
|
if (!fm || fm->active_fileop) return;
|
||||||
|
FileManagerUndo* u = fm->undo;
|
||||||
|
if (!u) return;
|
||||||
|
UndoStep* from_stack = undo ? u->undo : u->redo;
|
||||||
|
int* from_count = undo ? &u->undo_count : &u->redo_count;
|
||||||
|
UndoStep* to_stack = undo ? u->redo : u->undo;
|
||||||
|
int* to_count = undo ? &u->redo_count : &u->undo_count;
|
||||||
|
if (*from_count == 0) return;
|
||||||
|
|
||||||
|
UndoStep step = from_stack[*from_count - 1];
|
||||||
|
if (!apply_step(fm, step, undo)) {
|
||||||
|
// Leave it in place: the user can sort the files out and try again.
|
||||||
|
filemanager_refresh(fm);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(*from_count)--;
|
||||||
|
stack_push(to_stack, to_count, step);
|
||||||
|
filemanager_clear_multi_selection(fm);
|
||||||
|
filemanager_refresh(fm);
|
||||||
|
}
|
||||||
|
|
||||||
|
void filemanager_undo(FileManagerState* fm) {
|
||||||
|
undo_redo(fm, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void filemanager_redo(FileManagerState* fm) {
|
||||||
|
undo_redo(fm, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace filemanager
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
#include <gui/standalone.hpp>
|
#include <gui/standalone.hpp>
|
||||||
#include <gui/truetype.hpp>
|
#include <gui/truetype.hpp>
|
||||||
#include <gui/svg.hpp>
|
#include <gui/svg.hpp>
|
||||||
|
#include <gui/transfer.hpp>
|
||||||
|
|
||||||
#include "svg_doc.hpp"
|
#include "svg_doc.hpp"
|
||||||
|
|
||||||
@@ -589,6 +590,20 @@ extern "C" void _start() {
|
|||||||
|
|
||||||
if (ev.type == 3) break; // close
|
if (ev.type == 3) break; // close
|
||||||
|
|
||||||
|
// Show an image dropped onto the window.
|
||||||
|
char dropped[512];
|
||||||
|
if (gui::handle_file_drop(win.id, ev, dropped, sizeof(dropped))) {
|
||||||
|
montauk::strncpy(g_filepath, dropped, sizeof(g_filepath));
|
||||||
|
g_load_ok = load_image(g_filepath);
|
||||||
|
if (g_load_ok) {
|
||||||
|
if (g_img_w > g_win_w || g_img_h > viewport_h()) zoom_fit();
|
||||||
|
else center_image();
|
||||||
|
}
|
||||||
|
render(canvas);
|
||||||
|
win.present();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Resize
|
// Resize
|
||||||
if (ev.type == 2) {
|
if (ev.type == 2) {
|
||||||
g_win_w = win.width;
|
g_win_w = win.width;
|
||||||
|
|||||||
@@ -9,4 +9,4 @@ visible = false
|
|||||||
|
|
||||||
[files]
|
[files]
|
||||||
# Extensions Files opens with this app (lowercase, no dot)
|
# Extensions Files opens with this app (lowercase, no dot)
|
||||||
types = ["jpg", "jpeg", "png", "bmp", "gif", "tga", "svg"]
|
types = ["jpg", "jpeg", "svg"]
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
#include <gui/svg.hpp>
|
#include <gui/svg.hpp>
|
||||||
#include <gui/dialogs.hpp>
|
#include <gui/dialogs.hpp>
|
||||||
#include <gui/clipboard.hpp>
|
#include <gui/clipboard.hpp>
|
||||||
|
#include <gui/transfer.hpp>
|
||||||
#include <gui/mtk/widgets.hpp>
|
#include <gui/mtk/widgets.hpp>
|
||||||
#include <gui/mtk/settings.hpp>
|
#include <gui/mtk/settings.hpp>
|
||||||
#include <gui/stb_math.h>
|
#include <gui/stb_math.h>
|
||||||
@@ -1488,6 +1489,16 @@ extern "C" void _start() {
|
|||||||
// Close
|
// Close
|
||||||
if (ev.type == 3) break;
|
if (ev.type == 3) break;
|
||||||
|
|
||||||
|
// A file dropped from Files opens here, unless it would discard
|
||||||
|
// unsaved changes (then the drop is refused).
|
||||||
|
char dropped[512];
|
||||||
|
if (gui::handle_file_drop(win_id, ev, dropped, sizeof(dropped), !g_modified)) {
|
||||||
|
load_file(dropped);
|
||||||
|
render(pixels);
|
||||||
|
montauk::win_present(win_id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Resize
|
// Resize
|
||||||
if (ev.type == 2) {
|
if (ev.type == 2) {
|
||||||
g_win_w = ev.resize.w;
|
g_win_w = ev.resize.w;
|
||||||
|
|||||||
@@ -167,6 +167,57 @@ ICONS=(
|
|||||||
"apps/scalable/user-trash-full.svg" # Files Trash (has items)
|
"apps/scalable/user-trash-full.svg" # Files Trash (has items)
|
||||||
"panel/sensors-temperature-symbolic.svg" # temperature panel icon
|
"panel/sensors-temperature-symbolic.svg" # temperature panel icon
|
||||||
"actions/symbolic/view-paged-symbolic.svg" # page view toggle (word processor)
|
"actions/symbolic/view-paged-symbolic.svg" # page view toggle (word processor)
|
||||||
|
# Files: file type icons (see programs/src/files/filetypes.cpp)
|
||||||
|
"mimetypes/scalable/application-javascript.svg"
|
||||||
|
"mimetypes/scalable/application-json.svg"
|
||||||
|
"mimetypes/scalable/application-pdf.svg"
|
||||||
|
"mimetypes/scalable/application-x-archive.svg"
|
||||||
|
"mimetypes/scalable/application-x-cd-image.svg"
|
||||||
|
"mimetypes/scalable/application-x-deb.svg"
|
||||||
|
"mimetypes/scalable/application-x-object.svg"
|
||||||
|
"mimetypes/scalable/application-x-raw-disk-image.svg"
|
||||||
|
"mimetypes/scalable/application-x-sharedlib.svg"
|
||||||
|
"mimetypes/scalable/application-x-sqlite3.svg"
|
||||||
|
"mimetypes/scalable/application-x-wine-extension-ini.svg"
|
||||||
|
"mimetypes/scalable/application-x-yaml.svg"
|
||||||
|
"mimetypes/scalable/audio-midi.svg"
|
||||||
|
"mimetypes/scalable/audio-x-generic.svg"
|
||||||
|
"mimetypes/scalable/audio-x-playlist.svg"
|
||||||
|
"mimetypes/scalable/font-x-generic.svg"
|
||||||
|
"mimetypes/scalable/image-svg+xml.svg"
|
||||||
|
"mimetypes/scalable/image-x-generic.svg"
|
||||||
|
"mimetypes/scalable/text-calendar.svg"
|
||||||
|
"mimetypes/scalable/text-css.svg"
|
||||||
|
"mimetypes/scalable/text-html.svg"
|
||||||
|
"mimetypes/scalable/text-markdown.svg"
|
||||||
|
"mimetypes/scalable/text-x-c++src.svg"
|
||||||
|
"mimetypes/scalable/text-x-chdr.svg"
|
||||||
|
"mimetypes/scalable/text-x-copying.svg"
|
||||||
|
"mimetypes/scalable/text-x-csharp.svg"
|
||||||
|
"mimetypes/scalable/text-x-csrc.svg"
|
||||||
|
"mimetypes/scalable/text-x-go.svg"
|
||||||
|
"mimetypes/scalable/text-x-haskell.svg"
|
||||||
|
"mimetypes/scalable/text-x-java.svg"
|
||||||
|
"mimetypes/scalable/text-x-log.svg"
|
||||||
|
"mimetypes/scalable/text-x-lua.svg"
|
||||||
|
"mimetypes/scalable/text-x-makefile.svg"
|
||||||
|
"mimetypes/scalable/text-xml.svg"
|
||||||
|
"mimetypes/scalable/text-x-patch.svg"
|
||||||
|
"mimetypes/scalable/text-x-perl.svg"
|
||||||
|
"mimetypes/scalable/text-x-php.svg"
|
||||||
|
"mimetypes/scalable/text-x-python.svg"
|
||||||
|
"mimetypes/scalable/text-x-readme.svg"
|
||||||
|
"mimetypes/scalable/text-x-ruby.svg"
|
||||||
|
"mimetypes/scalable/text-x-rust.svg"
|
||||||
|
"mimetypes/scalable/text-x-script.svg"
|
||||||
|
"mimetypes/scalable/text-x-sql.svg"
|
||||||
|
"mimetypes/scalable/text-x-tex.svg"
|
||||||
|
"mimetypes/scalable/text-x-typescript.svg"
|
||||||
|
"mimetypes/scalable/text-x-vcard.svg"
|
||||||
|
"mimetypes/scalable/video-x-generic.svg"
|
||||||
|
"mimetypes/scalable/x-office-document.svg"
|
||||||
|
"mimetypes/scalable/x-office-presentation.svg"
|
||||||
|
"mimetypes/scalable/x-office-spreadsheet.svg"
|
||||||
)
|
)
|
||||||
|
|
||||||
copied=0
|
copied=0
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ APPS=(
|
|||||||
"paint|apps/scalable/kolourpaint.svg"
|
"paint|apps/scalable/kolourpaint.svg"
|
||||||
"screenshot|apps/scalable/gnome-screenshot.svg"
|
"screenshot|apps/scalable/gnome-screenshot.svg"
|
||||||
"texteditor|apps/scalable/accessories-text-editor.svg"
|
"texteditor|apps/scalable/accessories-text-editor.svg"
|
||||||
|
"files|apps/scalable/system-file-manager.svg"
|
||||||
"mandelbrot|apps/scalable/applications-science.svg"
|
"mandelbrot|apps/scalable/applications-science.svg"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user