mirror of
https://github.com/Toastconcern/FrameStudio.git
synced 2026-10-28 22:02:10 +01:00
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>
198 lines
8.0 KiB
C++
198 lines
8.0 KiB
C++
// platform.h on Windows: Win32 dialogs and message boxes, CreateProcess, the shell's folders.
|
|
#include "platform.h"
|
|
|
|
#include <windows.h> // before the other Win32 headers
|
|
#include <commdlg.h>
|
|
#include <shellapi.h>
|
|
#include <shlobj.h>
|
|
|
|
#include <mutex>
|
|
|
|
namespace platform {
|
|
|
|
namespace {
|
|
HWND g_owner = nullptr;
|
|
}
|
|
|
|
std::wstring widen(const std::string& s) {
|
|
if (s.empty()) return {};
|
|
const int n = MultiByteToWideChar(CP_UTF8, 0, s.data(), static_cast<int>(s.size()), nullptr, 0);
|
|
std::wstring w(n, L'\0');
|
|
MultiByteToWideChar(CP_UTF8, 0, s.data(), static_cast<int>(s.size()), w.data(), n);
|
|
return w;
|
|
}
|
|
|
|
std::string narrow(const std::wstring& w) {
|
|
if (w.empty()) return {};
|
|
const int n = WideCharToMultiByte(CP_UTF8, 0, w.data(), static_cast<int>(w.size()), nullptr, 0, nullptr, nullptr);
|
|
std::string s(n, '\0');
|
|
WideCharToMultiByte(CP_UTF8, 0, w.data(), static_cast<int>(w.size()), s.data(), n, nullptr, nullptr);
|
|
return s;
|
|
}
|
|
|
|
fs::path exeDir() {
|
|
std::wstring buf(MAX_PATH, L'\0');
|
|
for (;;) {
|
|
const DWORD n = GetModuleFileNameW(nullptr, buf.data(), static_cast<DWORD>(buf.size()));
|
|
if (n < buf.size()) { buf.resize(n); break; }
|
|
buf.resize(buf.size() * 2);
|
|
}
|
|
return fs::path(buf).parent_path();
|
|
}
|
|
|
|
fs::path userDataDir() {
|
|
wchar_t buf[MAX_PATH] = {};
|
|
const DWORD n = GetEnvironmentVariableW(L"APPDATA", buf, MAX_PATH);
|
|
return n ? fs::path(buf) : fs::temp_directory_path();
|
|
}
|
|
|
|
fs::path documentsDir() {
|
|
wchar_t buf[MAX_PATH] = {};
|
|
return SUCCEEDED(SHGetFolderPathW(nullptr, CSIDL_PERSONAL, nullptr, 0, buf)) ? fs::path(buf) : userDataDir();
|
|
}
|
|
|
|
FILE* openFile(const fs::path& path, const char* mode) { return _wfopen(path.c_str(), widen(mode).c_str()); }
|
|
|
|
std::tm localTime(std::time_t t) {
|
|
std::tm tm = {};
|
|
localtime_s(&tm, &t);
|
|
return tm;
|
|
}
|
|
|
|
int run(const std::string& commandLine, const fs::path& workingDir, std::string& out) {
|
|
// Inheritable handles exist only between CreatePipe and the CloseHandle after CreateProcess. Send to Frame and
|
|
// Install run commands from two threads: without this lock one child could inherit the other's pipe, whose
|
|
// reader then never sees end-of-file until that child exits.
|
|
static std::mutex spawn;
|
|
std::unique_lock<std::mutex> spawning(spawn);
|
|
SECURITY_ATTRIBUTES sa = {sizeof sa, nullptr, TRUE};
|
|
HANDLE rd = nullptr, wr = nullptr;
|
|
if (!CreatePipe(&rd, &wr, &sa, 0)) return -1;
|
|
SetHandleInformation(rd, HANDLE_FLAG_INHERIT, 0);
|
|
HANDLE nul = CreateFileW(L"NUL", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, &sa, OPEN_EXISTING, 0, nullptr);
|
|
STARTUPINFOW si = {sizeof si};
|
|
si.dwFlags = STARTF_USESTDHANDLES;
|
|
si.hStdInput = nul;
|
|
si.hStdOutput = si.hStdError = wr;
|
|
PROCESS_INFORMATION pi = {};
|
|
std::wstring cmd = widen(commandLine);
|
|
const std::wstring cwd = workingDir.wstring();
|
|
const BOOL ok = CreateProcessW(nullptr, cmd.data(), nullptr, nullptr, TRUE, CREATE_NO_WINDOW, nullptr,
|
|
cwd.empty() ? nullptr : cwd.c_str(), &si, &pi);
|
|
CloseHandle(wr);
|
|
if (nul != INVALID_HANDLE_VALUE) CloseHandle(nul);
|
|
spawning.unlock();
|
|
if (!ok) { CloseHandle(rd); out = "could not start: " + commandLine; return -1; }
|
|
char buf[4096];
|
|
DWORD n = 0;
|
|
while (ReadFile(rd, buf, sizeof buf, &n, nullptr) && n) out.append(buf, n);
|
|
CloseHandle(rd);
|
|
WaitForSingleObject(pi.hProcess, INFINITE);
|
|
DWORD code = 1;
|
|
GetExitCodeProcess(pi.hProcess, &code);
|
|
CloseHandle(pi.hProcess);
|
|
CloseHandle(pi.hThread);
|
|
return static_cast<int>(code);
|
|
}
|
|
|
|
// Windows' OpenSSH when installed, else Git for Windows' copy, else whatever PATH finds.
|
|
std::string sshTool(const char* name) {
|
|
wchar_t found[MAX_PATH] = {};
|
|
const std::wstring exe = widen(name) + L".exe";
|
|
if (SearchPathW(nullptr, exe.c_str(), nullptr, MAX_PATH, found, nullptr)) return "\"" + narrow(found) + "\" ";
|
|
wchar_t win[MAX_PATH] = {};
|
|
GetSystemDirectoryW(win, MAX_PATH);
|
|
std::vector<fs::path> tries = {fs::path(win) / L"OpenSSH" / exe};
|
|
for (const wchar_t* pf : {L"ProgramW6432", L"ProgramFiles", L"LOCALAPPDATA"}) {
|
|
wchar_t d[MAX_PATH] = {};
|
|
if (GetEnvironmentVariableW(pf, d, MAX_PATH)) {
|
|
tries.push_back(fs::path(d) / L"Git" / L"usr" / L"bin" / exe);
|
|
tries.push_back(fs::path(d) / L"Programs" / L"Git" / L"usr" / L"bin" / exe);
|
|
}
|
|
}
|
|
std::error_code ec;
|
|
for (const fs::path& t : tries) if (fs::exists(t, ec)) return "\"" + narrow(t.wstring()) + "\" ";
|
|
return std::string(name) + " ";
|
|
}
|
|
|
|
void openPath(const std::string& pathOrUrl) {
|
|
ShellExecuteW(nullptr, L"open", widen(pathOrUrl).c_str(), nullptr, nullptr, SW_SHOWNORMAL);
|
|
}
|
|
|
|
void setDialogOwner(void* nativeWindow) { g_owner = static_cast<HWND>(nativeWindow); }
|
|
|
|
namespace {
|
|
UINT iconFlag(Icon icon) {
|
|
switch (icon) {
|
|
case Icon::Warning: return MB_ICONWARNING;
|
|
case Icon::Error: return MB_ICONERROR;
|
|
case Icon::Question: return MB_ICONQUESTION;
|
|
default: return MB_ICONINFORMATION;
|
|
}
|
|
}
|
|
|
|
// "Meta home (*.apk)\0*.apk\0...All files\0*.*\0\0", as OPENFILENAMEW wants it.
|
|
std::wstring filterString(const std::vector<FileFilter>& filters) {
|
|
std::wstring s;
|
|
for (const FileFilter& f : filters) {
|
|
std::wstring patterns;
|
|
for (const std::string& p : f.patterns) patterns += (patterns.empty() ? L"" : L";") + widen(p);
|
|
s += widen(f.description) + L" (" + patterns + L")" + std::wstring(1, L'\0') + patterns + std::wstring(1, L'\0');
|
|
}
|
|
s += L"All files";
|
|
s += L'\0';
|
|
s += L"*.*";
|
|
s += L'\0';
|
|
return s; // (the string's own terminator ends the list)
|
|
}
|
|
} // namespace
|
|
|
|
void message(const std::string& title, const std::string& text, Icon icon) {
|
|
MessageBoxW(g_owner, widen(text).c_str(), widen(title).c_str(), MB_OK | iconFlag(icon));
|
|
}
|
|
|
|
Answer ask(const std::string& title, const std::string& text, bool withCancel, Icon icon) {
|
|
const int r = MessageBoxW(g_owner, widen(text).c_str(), widen(title).c_str(), (withCancel ? MB_YESNOCANCEL : MB_YESNO) | iconFlag(icon));
|
|
return r == IDYES ? Answer::Yes : r == IDNO ? Answer::No : Answer::Cancel;
|
|
}
|
|
|
|
std::vector<std::string> openFiles(const std::string& title, const std::vector<FileFilter>& filters, bool multiple) {
|
|
std::vector<wchar_t> buf(multiple ? 32768 : 4096, L'\0');
|
|
const std::wstring filter = filterString(filters), wtitle = widen(title);
|
|
OPENFILENAMEW ofn = {sizeof ofn};
|
|
ofn.hwndOwner = g_owner;
|
|
ofn.lpstrFilter = filter.c_str();
|
|
ofn.lpstrFile = buf.data();
|
|
ofn.nMaxFile = static_cast<DWORD>(buf.size());
|
|
ofn.lpstrTitle = wtitle.c_str();
|
|
ofn.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR | (multiple ? OFN_ALLOWMULTISELECT | OFN_EXPLORER : 0);
|
|
if (!GetOpenFileNameW(&ofn)) return {};
|
|
// Multi-select returns "dir\0file1\0file2\0\0"; a single pick returns "full path\0".
|
|
std::vector<std::string> picked;
|
|
const wchar_t* p = buf.data();
|
|
const std::wstring first = p;
|
|
p += first.size() + 1;
|
|
if (!*p) picked.push_back(narrow(first));
|
|
for (; *p; p += std::wcslen(p) + 1) picked.push_back(narrow((fs::path(first) / p).wstring()));
|
|
return picked;
|
|
}
|
|
|
|
std::string saveFile(const std::string& title, const std::string& suggestedName, const std::vector<FileFilter>& filters,
|
|
const std::string& extension) {
|
|
std::vector<wchar_t> buf(4096, L'\0');
|
|
const std::wstring name = widen(suggestedName), filter = filterString(filters), wtitle = widen(title), ext = widen(extension);
|
|
std::wcsncpy(buf.data(), name.c_str(), buf.size() - 1);
|
|
OPENFILENAMEW ofn = {sizeof ofn};
|
|
ofn.hwndOwner = g_owner;
|
|
ofn.lpstrFilter = filter.c_str();
|
|
ofn.lpstrDefExt = ext.empty() ? nullptr : ext.c_str();
|
|
ofn.lpstrFile = buf.data();
|
|
ofn.nMaxFile = static_cast<DWORD>(buf.size());
|
|
ofn.lpstrTitle = wtitle.c_str();
|
|
ofn.Flags = OFN_OVERWRITEPROMPT | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR;
|
|
if (!GetSaveFileNameW(&ofn)) return {};
|
|
return narrow(buf.data());
|
|
}
|
|
|
|
} // namespace platform
|