Files
Toastconcern--FrameStudio/studio/platform_posix.cpp
T
ToastconcernandClaude Opus 5.5 f7345a5ac3 Frame Studio and Frame Home: cross-platform builds
Frame Studio now builds for Windows (x64, x86, ARM64), Linux (x64, x86, ARM64)
and macOS (Apple Silicon, Intel):
- studio/platform.h with Win32 and POSIX implementations (dialogs through
  tinyfiledialogs on Linux/macOS), a GLFW window beside the Win32 one, and a
  platform-neutral frame pacer
- MoltenVK support in the renderer (portability enumeration) and bundled in
  the macOS app
- 32-bit builds: large-address-aware, LZMA packing within a 32-bit address space
- unaligned file reads in the loaders go through loadUnaligned (no UB)
- scripts/build-studio.sh, studio/build_windows.bat [x64|x86|arm64],
  cmake/llvm-mingw.cmake, and a GitHub Actions workflow building every target

Frame Home installs without Studio: frame/install-frame-home.{sh,ps1} and
"Install Frame Home.bat" (packaged as FrameHome-installer.zip).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 12:03:25 -05:00

241 lines
9.5 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// platform.h on Linux and macOS: dialogs through tinyfiledialogs (zenity / kdialog on Linux, the system's own panels
// on macOS), programs through /bin/sh, folders after the XDG base directories (Linux) or ~/Library (macOS).
#include "platform.h"
#include <tinyfiledialogs.h>
#include <fcntl.h>
#include <spawn.h>
#include <sys/wait.h>
#include <unistd.h>
#ifdef __APPLE__
#include <mach-o/dyld.h>
#endif
#include <cerrno>
#include <cstdlib>
#include <cstring>
#include <mutex>
extern char** environ;
namespace platform {
namespace {
fs::path home() {
const char* h = std::getenv("HOME");
return h && *h ? fs::path(h) : fs::temp_directory_path();
}
// The text a dialog shows: tinyfiledialogs refuses quotes (it hands the text to a shell), so they become the
// typographic ones ("Couldn't" reads the same as "Couldn’t").
std::string dialogText(const std::string& s) {
std::string out;
for (char c : s) out += c == '\'' || c == '`' ? "’" : c == '"' ? "”" : std::string(1, c);
return out;
}
// One argument for /bin/sh: in single quotes, its own single quotes closed, escaped and reopened.
std::string shellQuote(const std::string& s) {
std::string q = "'";
for (char c : s) q += c == '\'' ? std::string("'\\''") : std::string(1, c);
return q + "'";
}
const char* iconName(Icon icon) {
switch (icon) {
case Icon::Warning: return "warning";
case Icon::Error: return "error";
case Icon::Question: return "question";
default: return "info";
}
}
// tinyfiledialogs takes one filter: every pattern of every line, described by the first line.
struct OneFilter {
std::vector<std::string> patterns;
std::vector<const char*> pointers;
std::string description;
explicit OneFilter(const std::vector<FileFilter>& filters) {
for (const FileFilter& f : filters) {
if (description.empty()) description = f.description;
patterns.insert(patterns.end(), f.patterns.begin(), f.patterns.end());
}
for (const std::string& p : patterns) pointers.push_back(p.c_str());
}
};
} // namespace
// wchar_t is UTF-32 here: decode and encode UTF-8 directly.
std::wstring widen(const std::string& s) {
std::wstring w;
w.reserve(s.size());
for (size_t i = 0; i < s.size();) {
const unsigned char c = static_cast<unsigned char>(s[i]);
const int extra = c < 0x80 ? 0 : c < 0xE0 ? 1 : c < 0xF0 ? 2 : 3;
uint32_t cp = extra == 0 ? c : extra == 1 ? c & 0x1F : extra == 2 ? c & 0x0F : c & 0x07;
if (extra > 0 && i + extra >= s.size()) { w += L'�'; break; } // a sequence cut short
for (int k = 1; k <= extra; ++k) cp = (cp << 6) | (static_cast<unsigned char>(s[i + k]) & 0x3F);
w += static_cast<wchar_t>(cp);
i += 1 + extra;
}
return w;
}
std::string narrow(const std::wstring& w) {
std::string s;
s.reserve(w.size());
for (wchar_t wc : w) {
const uint32_t cp = static_cast<uint32_t>(wc);
if (cp < 0x80) s += static_cast<char>(cp);
else if (cp < 0x800) { s += static_cast<char>(0xC0 | (cp >> 6)); s += static_cast<char>(0x80 | (cp & 0x3F)); }
else if (cp < 0x10000) {
s += static_cast<char>(0xE0 | (cp >> 12)); s += static_cast<char>(0x80 | ((cp >> 6) & 0x3F)); s += static_cast<char>(0x80 | (cp & 0x3F));
} else {
s += static_cast<char>(0xF0 | (cp >> 18)); s += static_cast<char>(0x80 | ((cp >> 12) & 0x3F));
s += static_cast<char>(0x80 | ((cp >> 6) & 0x3F)); s += static_cast<char>(0x80 | (cp & 0x3F));
}
}
return s;
}
fs::path exeDir() {
std::error_code ec;
#ifdef __APPLE__
char buf[4096];
uint32_t size = sizeof buf;
if (_NSGetExecutablePath(buf, &size) == 0) return fs::canonical(buf, ec).parent_path();
return fs::current_path(ec);
#else
const fs::path self = fs::read_symlink("/proc/self/exe", ec);
return ec ? fs::current_path(ec) : self.parent_path();
#endif
}
fs::path userDataDir() {
#ifdef __APPLE__
return home() / "Library" / "Application Support";
#else
const char* xdg = std::getenv("XDG_DATA_HOME");
return xdg && *xdg ? fs::path(xdg) : home() / ".local" / "share";
#endif
}
fs::path documentsDir() {
static const fs::path found = [] {
std::error_code ec;
#ifndef __APPLE__
// xdg-user-dirs' answer (localized folder names), when it's set up
std::string out;
if (run("xdg-user-dir DOCUMENTS", {}, out) == 0) {
while (!out.empty() && (out.back() == '\n' || out.back() == '\r')) out.pop_back();
if (!out.empty() && fs::is_directory(out, ec) && fs::path(out) != home()) return fs::path(out);
}
#endif
const fs::path docs = home() / "Documents";
return fs::is_directory(docs, ec) ? docs : home();
}();
return found;
}
FILE* openFile(const fs::path& path, const char* mode) { return std::fopen(path.c_str(), mode); }
std::tm localTime(std::time_t t) {
std::tm tm = {};
localtime_r(&t, &tm);
return tm;
}
int run(const std::string& commandLine, const fs::path& workingDir, std::string& out) {
// Both ends close on exec: a command started meanwhile by another thread (Send to Frame and Install run at once)
// must not inherit this pipe, or its reader never sees the end. The child's own copies (dup2) stay open.
int pipeFds[2];
{
static std::mutex creating; // (no pipe2 on macOS: made and marked as one step)
std::lock_guard<std::mutex> lock(creating);
if (::pipe(pipeFds) != 0) return -1;
::fcntl(pipeFds[0], F_SETFD, FD_CLOEXEC);
::fcntl(pipeFds[1], F_SETFD, FD_CLOEXEC);
}
posix_spawn_file_actions_t actions;
posix_spawn_file_actions_init(&actions);
posix_spawn_file_actions_addopen(&actions, 0, "/dev/null", O_RDONLY, 0);
posix_spawn_file_actions_adddup2(&actions, pipeFds[1], 1);
posix_spawn_file_actions_adddup2(&actions, pipeFds[1], 2);
posix_spawn_file_actions_addclose(&actions, pipeFds[0]);
posix_spawn_file_actions_addclose(&actions, pipeFds[1]);
// (the working directory through the shell: posix_spawn_file_actions_addchdir isn't everywhere yet)
const std::string line = workingDir.empty() ? commandLine : "cd " + shellQuote(workingDir.string()) + " && " + commandLine;
const char* argv[] = {"/bin/sh", "-c", line.c_str(), nullptr};
pid_t pid = -1;
const int spawned = posix_spawn(&pid, "/bin/sh", &actions, nullptr, const_cast<char* const*>(argv), environ);
posix_spawn_file_actions_destroy(&actions);
::close(pipeFds[1]);
if (spawned != 0) { ::close(pipeFds[0]); out = "could not start: " + commandLine; return -1; }
char buf[4096];
for (ssize_t n; (n = ::read(pipeFds[0], buf, sizeof buf)) > 0;) out.append(buf, static_cast<size_t>(n));
::close(pipeFds[0]);
int status = 0;
while (::waitpid(pid, &status, 0) < 0 && errno == EINTR) {}
return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
}
std::string sshTool(const char* name) { return std::string(name) + " "; }
void openPath(const std::string& pathOrUrl) {
#ifdef __APPLE__
const std::string cmd = "open " + shellQuote(pathOrUrl) + " >/dev/null 2>&1 &";
#else
const std::string cmd = "xdg-open " + shellQuote(pathOrUrl) + " >/dev/null 2>&1 &";
#endif
std::string ignored;
run(cmd, {}, ignored);
}
void setDialogOwner(void*) {}
void message(const std::string& title, const std::string& text, Icon icon) {
tinyfd_messageBox(dialogText(title).c_str(), dialogText(text).c_str(), "ok", iconName(icon), 1);
}
Answer ask(const std::string& title, const std::string& text, bool withCancel, Icon icon) {
const int r = tinyfd_messageBox(dialogText(title).c_str(), dialogText(text).c_str(), withCancel ? "yesnocancel" : "yesno", iconName(icon), 1);
// tinyfiledialogs: 1 yes; yesnocancel: 2 no, 0 cancel; yesno: 0 no
return r == 1 ? Answer::Yes : withCancel && r == 0 ? Answer::Cancel : Answer::No;
}
std::vector<std::string> openFiles(const std::string& title, const std::vector<FileFilter>& filters, bool multiple) {
const OneFilter f(filters);
const char* picked = tinyfd_openFileDialog(dialogText(title).c_str(), nullptr, static_cast<int>(f.pointers.size()),
f.pointers.empty() ? nullptr : f.pointers.data(),
f.description.empty() ? nullptr : f.description.c_str(), multiple ? 1 : 0);
std::vector<std::string> out;
if (!picked) return out;
// several files come back joined by '|'
std::string all = picked;
for (size_t at = 0; at <= all.size();) {
const size_t bar = all.find('|', at);
const std::string one = all.substr(at, bar == std::string::npos ? std::string::npos : bar - at);
if (!one.empty()) out.push_back(one);
if (bar == std::string::npos) break;
at = bar + 1;
}
return out;
}
std::string saveFile(const std::string& title, const std::string& suggestedName, const std::vector<FileFilter>& filters,
const std::string& extension) {
const OneFilter f(filters);
const char* picked = tinyfd_saveFileDialog(dialogText(title).c_str(), suggestedName.c_str(), static_cast<int>(f.pointers.size()),
f.pointers.empty() ? nullptr : f.pointers.data(),
f.description.empty() ? nullptr : f.description.c_str());
if (!picked) return {};
std::string path = picked;
if (!extension.empty() && fs::path(path).extension().empty()) path += "." + extension;
return path;
}
} // namespace platform