summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMegaMech <MegaMech@users.noreply.github.com>2025-02-07 22:24:00 -0700
committerGitHub <noreply@github.com>2025-02-07 22:24:00 -0700
commit008d527ed5bae64e18eff59310bd18d698d4c016 (patch)
treeac1d62f7ea28ad5ba81ca9640b6f490c3442ef55
parent7fe705227028ef136317fbc9eceafd8bd366e6a2 (diff)
Torch Static Compile (#187)
* Fix freecam rotation smoothing * Configure freecam controller * Fix cont z button and impl freecam on/off events * Add torch static comp * Fixed cmake * Copied yamls --------- Co-authored-by: KiritoDv <kiritodev01@gmail.com>
-rw-r--r--CMakeLists.txt273
-rw-r--r--include/portable-file-dialogs.h1770
-rw-r--r--src/port/GameExtractor.cpp56
-rw-r--r--src/port/GameExtractor.h17
m---------torch0
5 files changed, 2014 insertions, 102 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index bb56c388f..fda59b51b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -23,23 +23,19 @@ include(FindFontconfig)
include(cmake/lus-cvars.cmake)
if (WIN32)
-include(libultraship/cmake/automate-vcpkg.cmake)
-# Forced to use MSVC
-set(MSVC ON)
-
-if(MSVC)
+include(cmake/automate-vcpkg.cmake)
set(VCPKG_TRIPLET x64-windows-static)
set(VCPKG_TARGET_TRIPLET x64-windows-static)
-else()
-set(VCPKG_TRIPLET x64-mingw-static)
-set(VCPKG_TARGET_TRIPLET x64-mingw-static)
-endif()
-
vcpkg_bootstrap()
-vcpkg_install_packages(fontconfig sdl2 zlib bzip2 libzip libpng getopt dirent libusb pthread glew glfw3 spdlog)
+vcpkg_install_packages(zlib bzip2 libzip libpng sdl2 glew glfw3 nlohmann-json tinyxml2 spdlog libogg libvorbis)
-vcpkg_install_packages(zlib bzip2 libzip libpng sdl2 sdl2-net glew glfw3 nlohmann-json tinyxml2)
+set_property(DIRECTORY ${CMAKE_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT ${PROJECT_NAME})
+set_property(DIRECTORY ${CMAKE_SOURCE_DIR} PROPERTY VS_DEBUGGER_WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
+endif()
+if(NOT EXISTS ${CMAKE_BINARY_DIR}/config.yml)
+ execute_process(COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_SOURCE_DIR}/config.yml" "${CMAKE_BINARY_DIR}/config.yml")
+ execute_process(COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_SOURCE_DIR}/yamls/" "${CMAKE_BINARY_DIR}/yamls/")
endif()
if (MSVC)
@@ -58,6 +54,21 @@ set(CMAKE_XCODE_ATTRIBUTE_DEVELOPMENT_TEAM "YOUR_TEAM_ID")
set(PROJECT_ID "dev.net64.game")
endif()
+################################################################################
+# Set target arch type if empty. Visual studio solution generator provides it.
+################################################################################
+if (CMAKE_SYSTEM_NAME STREQUAL "Windows")
+ if(NOT CMAKE_VS_PLATFORM_NAME)
+ set(CMAKE_VS_PLATFORM_NAME "x64")
+ endif()
+ message("${CMAKE_VS_PLATFORM_NAME} architecture in use")
+
+ if(NOT ("${CMAKE_VS_PLATFORM_NAME}" STREQUAL "x64"
+ OR "${CMAKE_VS_PLATFORM_NAME}" STREQUAL "Win32"))
+ message(FATAL_ERROR "${CMAKE_VS_PLATFORM_NAME} arch is not supported!")
+ endif()
+endif()
+
# Set game compilation version
set(VERSION us)
set(USE_NETWORKING ON)
@@ -222,6 +233,14 @@ file(DOWNLOAD "https://raw.githubusercontent.com/DLTcollab/sse2neon/refs/heads/m
include_directories(${SSE2NEON_DIR})
+#=================== DRLibs ===================
+FetchContent_Declare(
+ dr_libs
+ GIT_REPOSITORY https://github.com/mackron/dr_libs.git
+ GIT_TAG da35f9d6c7374a95353fd1df1d394d44ab66cf01
+)
+FetchContent_MakeAvailable(dr_libs)
+
#==============================================================================#
# Libultraship Integration #
#==============================================================================#
@@ -258,45 +277,31 @@ include_directories(
${CMAKE_CURRENT_SOURCE_DIR}/libultraship/src/graphic
${SDL2_INCLUDE_DIRS}
${GLEW_INCLUDE_DIRS}
+ ${dr_libs_SOURCE_DIR}
)
+add_subdirectory(libultraship ${CMAKE_CURRENT_SOURCE_DIR}/libultraship)
+add_dependencies(${PROJECT_NAME} libultraship)
+target_link_libraries(${PROJECT_NAME} PRIVATE libultraship)
+
if (CMAKE_SYSTEM_NAME STREQUAL "Windows")
- if("${CMAKE_VS_PLATFORM_NAME}" STREQUAL "x64")
- target_compile_definitions(${PROJECT_NAME} PRIVATE
- "$<$<CONFIG:Debug>:"
- "_DEBUG;"
- "_CRT_SECURE_NO_WARNINGS;"
- "ENABLE_DX11;"
- ">"
- "$<$<CONFIG:Release>:"
- "NDEBUG"
- ">"
- "INCLUDE_GAME_PRINTF;"
- "UNICODE;"
- "_UNICODE"
- STORMLIB_NO_AUTO_LINK
- "_CRT_SECURE_NO_WARNINGS;"
- "_SILENCE_ALL_MS_EXT_DEPRECATION_WARNINGS;"
- )
- elseif("${CMAKE_VS_PLATFORM_NAME}" STREQUAL "Win32")
- target_compile_definitions(${PROJECT_NAME} PRIVATE
- "$<$<CONFIG:Debug>:"
- "NOINCLUDE_GAME_PRINTF;"
- "_DEBUG;"
- "_CRT_SECURE_NO_WARNINGS;"
- "ENABLE_OPENGL"
- ">"
- "$<$<CONFIG:Release>:"
- "NDEBUG;"
- ">"
- "INCLUDE_GAME_PRINTF;"
- "WIN32;"
- "UNICODE;"
- "_UNICODE"
- STORMLIB_NO_AUTO_LINK
- "_SILENCE_ALL_MS_EXT_DEPRECATION_WARNINGS;"
- )
- endif()
+ target_compile_definitions(${PROJECT_NAME} PRIVATE
+ "$<$<CONFIG:Debug>:"
+ "_DEBUG;"
+ "_CRT_SECURE_NO_WARNINGS;"
+ "ENABLE_DX11;"
+ ">"
+ "$<$<CONFIG:Release>:"
+ "NDEBUG"
+ ">"
+ "INCLUDE_GAME_PRINTF;"
+ "UNICODE;"
+ "_UNICODE;"
+ "_CRT_SECURE_NO_WARNINGS;"
+ "_SILENCE_ALL_MS_EXT_DEPRECATION_WARNINGS;"
+ )
+ add_compile_definitions(STORMLIB_NO_AUTO_LINK)
+ set(STORMLIB_NO_AUTO_LINK ON)
elseif (CMAKE_SYSTEM_NAME STREQUAL "CafeOS")
target_compile_definitions(${PROJECT_NAME} PRIVATE
"$<$<CONFIG:Debug>:"
@@ -327,13 +332,18 @@ elseif ("${CMAKE_CXX_COMPILER_ID}" MATCHES "GNU|Clang|AppleClang")
)
endif()
-add_subdirectory(libultraship ${CMAKE_CURRENT_SOURCE_DIR}/libultraship)
-
-add_dependencies(${PROJECT_NAME} libultraship)
-target_link_libraries(${PROJECT_NAME} PRIVATE libultraship)
if (CMAKE_SYSTEM_NAME STREQUAL "Windows")
+ find_package(Ogg CONFIG REQUIRED)
+ link_libraries(Ogg::ogg)
+
+ find_package(Vorbis CONFIG REQUIRED)
+ link_libraries(Vorbis::vorbisfile)
set(ADDITIONAL_LIBRARY_DEPENDENCIES
"$<$<BOOL:${USE_NETWORKING}>:SDL2_net::SDL2_net-static>"
+ "Ogg::ogg"
+ "Vorbis::vorbis"
+ "Vorbis::vorbisenc"
+ "Vorbis::vorbisfile"
)
elseif(CMAKE_SYSTEM_NAME STREQUAL "NintendoSwitch")
set(ADDITIONAL_LIBRARY_DEPENDENCIES
@@ -347,34 +357,48 @@ elseif(CMAKE_SYSTEM_NAME STREQUAL "CafeOS")
${DEVKITPRO}/portlibs/wiiu/include/
)
else()
+ find_package(Ogg REQUIRED)
+ find_package(Vorbis REQUIRED)
set(ADDITIONAL_LIBRARY_DEPENDENCIES
"$<$<BOOL:${USE_NETWORKING}>:SDL2_net::SDL2_net>"
+ "Ogg::ogg"
+ "Vorbis::vorbis"
+ "Vorbis::vorbisenc"
+ "Vorbis::vorbisfile"
)
endif()
if(USE_NETWORKING)
-if(MSVC)
-vcpkg_install_packages(sdl2-net)
-endif()
-find_package(SDL2_net REQUIRED)
-include_directories(${SDL2_NET_INCLUDE_DIRS})
-#target_link_libraries(${PROJECT_NAME} PRIVATE SDL2_net::SDL2_net)
-#target_compile_definitions(${PROJECT_NAME} PRIVATE USE_NETWORKING)
+ if(MSVC)
+ vcpkg_install_packages(sdl2-net)
+ endif()
+ find_package(SDL2_net REQUIRED)
+ include_directories(${SDL2_NET_INCLUDE_DIRS})
+ target_link_libraries(${PROJECT_NAME} PRIVATE SDL2_net::SDL2_net)
+ target_compile_definitions(${PROJECT_NAME} PRIVATE USE_NETWORKING)
endif()
-target_link_libraries(${PROJECT_NAME} PRIVATE "${ADDITIONAL_LIBRARY_DEPENDENCIES}")
+option(USE_STANDALONE "Build as a standalone executable" OFF)
+option(BUILD_STORMLIB "Build with StormLib support" OFF)
+
+option(BUILD_SM64 "Build with Super Mario 64 support" OFF)
+option(BUILD_SF64 "Build with Star Fox 64 support" OFF)
+option(BUILD_FZERO "Build with F-Zero X support" OFF)
+
+add_subdirectory(torch)
+target_link_libraries(${PROJECT_NAME} PRIVATE torch "${ADDITIONAL_LIBRARY_DEPENDENCIES}")
if(CMAKE_SYSTEM_NAME MATCHES "NintendoSwitch")
-nx_generate_nacp(Lylat.nacp
-NAME "${PROJECT_NAME}"
-AUTHOR "${PROJECT_TEAM}"
-VERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}"
+nx_generate_nacp(${PROJECT_NAME}.nacp
+ NAME "${PROJECT_NAME}"
+ AUTHOR "${PROJECT_TEAM}"
+ VERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}"
)
nx_create_nro(${PROJECT_NAME}
-NACP Lylat.nacp
+ NACP ${PROJECT_NAME}.nacp
)
INSTALL(FILES ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}.nro DESTINATION . COMPONENT ${PROJECT_NAME})
@@ -392,14 +416,14 @@ if(MSVC)
if("${CMAKE_VS_PLATFORM_NAME}" STREQUAL "x64")
target_compile_options(${PROJECT_NAME} PRIVATE
$<$<CONFIG:Debug>:
- /Wall;
+ /w;
/Od;
/MTd
>
$<$<CONFIG:Release>:
/Oi;
/Gy;
- /Wall;
+ /W3;
/MT
>
/permissive-;
@@ -408,6 +432,7 @@ if(MSVC)
${DEFAULT_CXX_EXCEPTION_HANDLING}
)
target_compile_options(${PROJECT_NAME} PRIVATE $<$<CONFIG:Debug>:/ZI;>)
+ target_compile_options(${PROJECT_NAME} PRIVATE $<$<CONFIG:Release>:/Zi;>)
elseif("${CMAKE_VS_PLATFORM_NAME}" STREQUAL "Win32")
target_compile_options(${PROJECT_NAME} PRIVATE
$<$<CONFIG:Debug>:
@@ -545,56 +570,56 @@ if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang|AppleClang")
>
)
else()
- if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64")
- set(CPU_OPTION -msse2 -mfpmath=sse)
- endif()
+ if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64")
+ set(CPU_OPTION -msse2 -mfpmath=sse)
+ endif()
- target_compile_options(${PROJECT_NAME} PRIVATE
- -Wall -Wextra -Wno-error
- -Wno-unused-parameter
- -Wno-unused-function
- -Wno-unused-variable
- -Wno-missing-field-initializers
- -Wno-parentheses
- -Wno-narrowing
- -Wno-missing-braces
- -Wno-int-conversion
- $<$<COMPILE_LANGUAGE:C>:
- -Werror-implicit-function-declaration
- -Wno-incompatible-pointer-types
- -fpermissive
- >
- $<$<COMPILE_LANGUAGE:CXX>:-fpermissive>
- $<$<COMPILE_LANGUAGE:CXX>:-Wno-deprecated-enum-enum-conversion>
- -pthread
- ${CPU_OPTION}
- )
+ target_compile_options(${PROJECT_NAME} PRIVATE
+ -Wall -Wextra -Wno-error
+ -Wno-unused-parameter
+ -Wno-unused-function
+ -Wno-unused-variable
+ -Wno-missing-field-initializers
+ -Wno-parentheses
+ -Wno-narrowing
+ -Wno-missing-braces
+ -Wno-int-conversion
+ $<$<COMPILE_LANGUAGE:C>:
+ -Werror-implicit-function-declaration
+ -Wno-incompatible-pointer-types
+ -fpermissive
+ >
+ $<$<COMPILE_LANGUAGE:CXX>:-fpermissive>
+ $<$<COMPILE_LANGUAGE:CXX>:-Wno-deprecated-enum-enum-conversion>
+ -pthread
+ ${CPU_OPTION}
+ )
- target_link_options(${PROJECT_NAME} PRIVATE
- -pthread
- -Wl,-export-dynamic
- )
+ target_link_options(${PROJECT_NAME} PRIVATE
+ -pthread
+ -Wl,-export-dynamic
+ )
endif()
endif()
include(ExternalProject)
-ExternalProject_Add(torch
-PREFIX torch
-SOURCE_DIR ${CMAKE_SOURCE_DIR}/torch
-CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/torch
+ExternalProject_Add(TorchExternal
+ PREFIX TorchExternal
+ SOURCE_DIR ${CMAKE_SOURCE_DIR}/torch
+ CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/torch
)
-ExternalProject_Get_Property(torch install_dir)
+ExternalProject_Get_Property(TorchExternal install_dir)
if (MSVC)
- set(TORCH_EXECUTABLE ${install_dir}/src/torch-build/$<CONFIGURATION>/torch)
+ set(TORCH_EXECUTABLE ${install_dir}/src/TorchExternal-build/$<CONFIGURATION>/torch)
else()
- set(TORCH_EXECUTABLE ${install_dir}/src/torch-build/torch)
+ set(TORCH_EXECUTABLE ${install_dir}/src/TorchExternal-build/torch)
endif()
message(STATUS "Torch executable path: ${TORCH_EXECUTABLE}")
add_custom_target(
ExtractAssets
- DEPENDS torch
+ DEPENDS TorchExternal
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMAND ${TORCH_EXECUTABLE} header -o baserom.us.z64
COMMAND ${TORCH_EXECUTABLE} o2r baserom.us.z64
@@ -602,3 +627,47 @@ add_custom_target(
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_SOURCE_DIR}/spaghetti.o2r" "${CMAKE_BINARY_DIR}/spaghetti.o2r"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_SOURCE_DIR}/ship.o2r" "${CMAKE_BINARY_DIR}/ship.o2r"
)
+
+add_custom_target(
+ GenerateO2R
+ DEPENDS TorchExternal
+ WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
+ COMMAND ${TORCH_EXECUTABLE} pack port spaghetti.o2r o2r
+ COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_SOURCE_DIR}/spaghetti.o2r" "${CMAKE_BINARY_DIR}/spaghetti.o2r"
+)
+
+if(CMAKE_SYSTEM_NAME MATCHES "Darwin")
+add_custom_target(CreateOSXIcons
+ COMMAND mkdir -p ${CMAKE_BINARY_DIR}/macosx/spaghettikart.iconset
+ COMMAND sips -z 16 16 logo.png --out ${CMAKE_BINARY_DIR}/macosx/spaghettikart.iconset/icon_16x16.png
+ COMMAND sips -z 32 32 logo.png --out ${CMAKE_BINARY_DIR}/macosx/spaghettikart.iconset/icon_16x16@2x.png
+ COMMAND sips -z 32 32 logo.png --out ${CMAKE_BINARY_DIR}/macosx/spaghettikart.iconset/icon_32x32.png
+ COMMAND sips -z 64 64 logo.png --out ${CMAKE_BINARY_DIR}/macosx/spaghettikart.iconset/icon_32x32@2x.png
+ COMMAND sips -z 128 128 logo.png --out ${CMAKE_BINARY_DIR}/macosx/spaghettikart.iconset/icon_128x128.png
+ COMMAND sips -z 256 256 logo.png --out ${CMAKE_BINARY_DIR}/macosx/spaghettikart.iconset/icon_128x128@2x.png
+ COMMAND sips -z 256 256 logo.png --out ${CMAKE_BINARY_DIR}/macosx/spaghettikart.iconset/icon_256x256.png
+ COMMAND sips -z 512 512 logo.png --out ${CMAKE_BINARY_DIR}/macosx/spaghettikart.iconset/icon_256x256@2x.png
+ COMMAND sips -z 512 512 logo.png --out ${CMAKE_BINARY_DIR}/macosx/spaghettikart.iconset/icon_512x512.png
+ COMMAND cp logo.png ${CMAKE_BINARY_DIR}/macosx/spaghettikart.iconset/icon_512x512@2x.png
+ COMMAND iconutil -c icns -o ${CMAKE_BINARY_DIR}/macosx/spaghettikart.icns ${CMAKE_BINARY_DIR}/macosx/spaghettikart.iconset
+ WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
+ COMMENT "Creating OSX icons ..."
+ )
+ add_dependencies(${PROJECT_NAME} CreateOSXIcons)
+ configure_file("${CMAKE_SOURCE_DIR}/Info.plist" "${CMAKE_BINARY_DIR}/Info.plist" COPYONLY)
+endif()
+
+set_property(TARGET ${PROJECT_NAME} PROPERTY APPIMAGE_DESKTOP_FILE_TERMINAL YES)
+set_property(TARGET ${PROJECT_NAME} PROPERTY APPIMAGE_DESKTOP_FILE "${CMAKE_SOURCE_DIR}/SpaghettiKart.desktop")
+set_property(TARGET ${PROJECT_NAME} PROPERTY APPIMAGE_ICON_FILE "${CMAKE_SOURCE_DIR}/logo.png")
+
+if(CMAKE_SYSTEM_NAME MATCHES "Linux")
+ set(CPACK_GENERATOR "External")
+elseif(CMAKE_SYSTEM_NAME MATCHES "Windows|NintendoSwitch|CafeOS")
+ set(CPACK_GENERATOR "ZIP")
+elseif(CMAKE_SYSTEM_NAME MATCHES "Darwin")
+ set(CPACK_GENERATOR "Bundle")
+endif()
+
+set(CPACK_PROJECT_CONFIG_FILE ${CMAKE_SOURCE_DIR}/cmake/configure-packaging.cmake)
+include(cmake/packaging.cmake) \ No newline at end of file
diff --git a/include/portable-file-dialogs.h b/include/portable-file-dialogs.h
new file mode 100644
index 000000000..8b46c9b70
--- /dev/null
+++ b/include/portable-file-dialogs.h
@@ -0,0 +1,1770 @@
+//
+// Portable File Dialogs
+//
+// Copyright © 2018–2022 Sam Hocevar <sam@hocevar.net>
+//
+// This library is free software. It comes without any warranty, to
+// the extent permitted by applicable law. You can redistribute it
+// and/or modify it under the terms of the Do What the Fuck You Want
+// to Public License, Version 2, as published by the WTFPL Task Force.
+// See http://www.wtfpl.net/ for more details.
+//
+
+#pragma once
+
+#if _WIN32
+#ifndef WIN32_LEAN_AND_MEAN
+#define WIN32_LEAN_AND_MEAN 1
+#endif
+#include <windows.h>
+#include <commdlg.h>
+#include <shlobj.h>
+#include <shobjidl.h> // IFileDialog
+#include <shellapi.h>
+#include <strsafe.h>
+#include <future> // std::async
+#include <userenv.h> // GetUserProfileDirectory()
+
+#elif __EMSCRIPTEN__
+#include <emscripten.h>
+
+#else
+#ifndef _POSIX_C_SOURCE
+#define _POSIX_C_SOURCE 2 // for popen()
+#endif
+#ifdef __APPLE__
+#ifndef _DARWIN_C_SOURCE
+#define _DARWIN_C_SOURCE
+#endif
+#endif
+#include <cstdio> // popen()
+#include <cstdlib> // std::getenv()
+#include <fcntl.h> // fcntl()
+#include <unistd.h> // read(), pipe(), dup2(), getuid()
+#include <csignal> // ::kill, std::signal
+#include <sys/stat.h> // stat()
+#include <sys/wait.h> // waitpid()
+#include <pwd.h> // getpwnam()
+#endif
+
+#include <string> // std::string
+#include <memory> // std::shared_ptr
+#include <iostream> // std::ostream
+#include <map> // std::map
+#include <set> // std::set
+#include <regex> // std::regex
+#include <thread> // std::mutex, std::this_thread
+#include <chrono> // std::chrono
+
+// Versions of mingw64 g++ up to 9.3.0 do not have a complete IFileDialog
+#ifndef PFD_HAS_IFILEDIALOG
+#define PFD_HAS_IFILEDIALOG 1
+#if (defined __MINGW64__ || defined __MINGW32__) && defined __GXX_ABI_VERSION
+#if __GXX_ABI_VERSION <= 1013
+#undef PFD_HAS_IFILEDIALOG
+#define PFD_HAS_IFILEDIALOG 0
+#endif
+#endif
+#endif
+
+namespace pfd {
+
+enum class button {
+ cancel = -1,
+ ok,
+ yes,
+ no,
+ abort,
+ retry,
+ ignore,
+};
+
+enum class choice {
+ ok = 0,
+ ok_cancel,
+ yes_no,
+ yes_no_cancel,
+ retry_cancel,
+ abort_retry_ignore,
+};
+
+enum class icon {
+ info = 0,
+ warning,
+ error,
+ question,
+};
+
+// Additional option flags for various dialog constructors
+enum class opt : uint8_t {
+ none = 0,
+ // For file open, allow multiselect.
+ multiselect = 0x1,
+ // For file save, force overwrite and disable the confirmation dialog.
+ force_overwrite = 0x2,
+ // For folder select, force path to be the provided argument instead
+ // of the last opened directory, which is the Microsoft-recommended,
+ // user-friendly behaviour.
+ force_path = 0x4,
+};
+
+inline opt operator|(opt a, opt b) {
+ return opt(uint8_t(a) | uint8_t(b));
+}
+inline bool operator&(opt a, opt b) {
+ return bool(uint8_t(a) & uint8_t(b));
+}
+
+// The settings class, only exposing to the user a way to set verbose mode
+// and to force a rescan of installed desktop helpers (zenity, kdialog…).
+class settings {
+ public:
+ static bool available();
+
+ static void verbose(bool value);
+ static void rescan();
+
+ protected:
+ explicit settings(bool resync = false);
+
+ bool check_program(std::string const& program);
+
+ inline bool is_osascript() const;
+ inline bool is_zenity() const;
+ inline bool is_kdialog() const;
+
+ enum class flag {
+ is_scanned = 0,
+ is_verbose,
+
+ has_zenity,
+ has_matedialog,
+ has_qarma,
+ has_kdialog,
+ is_vista,
+
+ max_flag,
+ };
+
+ // Static array of flags for internal state
+ bool const& flags(flag in_flag) const;
+
+ // Non-const getter for the static array of flags
+ bool& flags(flag in_flag);
+};
+
+// Internal classes, not to be used by client applications
+namespace internal {
+
+// Process wait timeout, in milliseconds
+static int const default_wait_timeout = 20;
+
+class executor {
+ friend class dialog;
+
+ public:
+ // High level function to get the result of a command
+ std::string result(int* exit_code = nullptr);
+
+ // High level function to abort
+ bool kill();
+
+#if _WIN32
+ void start_func(std::function<std::string(int*)> const& fun);
+ static BOOL CALLBACK enum_windows_callback(HWND hwnd, LPARAM lParam);
+#elif __EMSCRIPTEN__
+ void start(int exit_code);
+#else
+ void start_process(std::vector<std::string> const& command);
+#endif
+
+ ~executor();
+
+ protected:
+ bool ready(int timeout = default_wait_timeout);
+ void stop();
+
+ private:
+ bool m_running = false;
+ std::string m_stdout;
+ int m_exit_code = -1;
+#if _WIN32
+ std::future<std::string> m_future;
+ std::set<HWND> m_windows;
+ std::condition_variable m_cond;
+ std::mutex m_mutex;
+ DWORD m_tid;
+#elif __EMSCRIPTEN__ || __NX__
+ // FIXME: do something
+#else
+ pid_t m_pid = 0;
+ int m_fd = -1;
+#endif
+};
+
+class platform {
+ protected:
+#if _WIN32
+ // Helper class around LoadLibraryA() and GetProcAddress() with some safety
+ class dll {
+ public:
+ dll(std::string const& name);
+ ~dll();
+
+ template <typename T> class proc {
+ public:
+ proc(dll const& lib, std::string const& sym)
+ : m_proc(reinterpret_cast<T*>((void*)::GetProcAddress(lib.handle, sym.c_str()))) {
+ }
+
+ operator bool() const {
+ return m_proc != nullptr;
+ }
+ operator T*() const {
+ return m_proc;
+ }
+
+ private:
+ T* m_proc;
+ };
+
+ private:
+ HMODULE handle;
+ };
+
+ // Helper class around CoInitialize() and CoUnInitialize()
+ class ole32_dll : public dll {
+ public:
+ ole32_dll();
+ ~ole32_dll();
+ bool is_initialized();
+
+ private:
+ HRESULT m_state;
+ };
+
+ // Helper class around CreateActCtx() and ActivateActCtx()
+ class new_style_context {
+ public:
+ new_style_context();
+ ~new_style_context();
+
+ private:
+ HANDLE create();
+ ULONG_PTR m_cookie = 0;
+ };
+#endif
+};
+
+class dialog : protected settings, protected platform {
+ public:
+ bool ready(int timeout = default_wait_timeout) const;
+ bool kill() const;
+
+ protected:
+ explicit dialog();
+
+ std::vector<std::string> desktop_helper() const;
+ static std::string buttons_to_name(choice _choice);
+ static std::string get_icon_name(icon _icon);
+
+ std::string powershell_quote(std::string const& str) const;
+ std::string osascript_quote(std::string const& str) const;
+ std::string shell_quote(std::string const& str) const;
+
+ // Keep handle to executing command
+ std::shared_ptr<executor> m_async;
+};
+
+class file_dialog : public dialog {
+ protected:
+ enum type {
+ open,
+ save,
+ folder,
+ };
+
+ file_dialog(type in_type, std::string const& title, std::string const& default_path = "",
+ std::vector<std::string> const& filters = {}, opt options = opt::none);
+
+ protected:
+ std::string string_result();
+ std::vector<std::string> vector_result();
+
+#if _WIN32
+ static int CALLBACK bffcallback(HWND hwnd, UINT uMsg, LPARAM, LPARAM pData);
+#if PFD_HAS_IFILEDIALOG
+ std::string select_folder_vista(IFileDialog* ifd, bool force_path);
+#endif
+
+ std::wstring m_wtitle;
+ std::wstring m_wdefault_path;
+
+ std::vector<std::string> m_vector_result;
+#endif
+};
+
+} // namespace internal
+
+//
+// The path class provides some platform-specific path constants
+//
+
+class path : protected internal::platform {
+ public:
+ static std::string home();
+ static std::string separator();
+};
+
+//
+// The notify widget
+//
+
+class notify : public internal::dialog {
+ public:
+ notify(std::string const& title, std::string const& message, icon _icon = icon::info);
+};
+
+//
+// The message widget
+//
+
+class message : public internal::dialog {
+ public:
+ message(std::string const& title, std::string const& text, choice _choice = choice::ok_cancel,
+ icon _icon = icon::info);
+
+ button result();
+
+ private:
+ // Some extra logic to map the exit code to button number
+ std::map<int, button> m_mappings;
+};
+
+//
+// The open_file, save_file, and open_folder widgets
+//
+
+class open_file : public internal::file_dialog {
+ public:
+ open_file(std::string const& title, std::string const& default_path = "",
+ std::vector<std::string> const& filters = { "All Files", "*" }, opt options = opt::none);
+
+#if defined(__has_cpp_attribute)
+#if __has_cpp_attribute(deprecated)
+ // Backwards compatibility
+ [[deprecated("Use pfd::opt::multiselect instead of allow_multiselect")]]
+#endif
+#endif
+ open_file(std::string const& title, std::string const& default_path, std::vector<std::string> const& filters,
+ bool allow_multiselect);
+
+ std::vector<std::string> result();
+};
+
+class save_file : public internal::file_dialog {
+ public:
+ save_file(std::string const& title, std::string const& default_path = "",
+ std::vector<std::string> const& filters = { "All Files", "*" }, opt options = opt::none);
+
+#if defined(__has_cpp_attribute)
+#if __has_cpp_attribute(deprecated)
+ // Backwards compatibility
+ [[deprecated("Use pfd::opt::force_overwrite instead of confirm_overwrite")]]
+#endif
+#endif
+ save_file(std::string const& title, std::string const& default_path, std::vector<std::string> const& filters,
+ bool confirm_overwrite);
+
+ std::string result();
+};
+
+class select_folder : public internal::file_dialog {
+ public:
+ select_folder(std::string const& title, std::string const& default_path = "", opt options = opt::none);
+
+ std::string result();
+};
+
+//
+// Below this are all the method implementations. You may choose to define the
+// macro PFD_SKIP_IMPLEMENTATION everywhere before including this header except
+// in one place. This may reduce compilation times.
+//
+
+#if !defined PFD_SKIP_IMPLEMENTATION
+
+// internal free functions implementations
+
+namespace internal {
+
+#if _WIN32
+static inline std::wstring str2wstr(std::string const& str) {
+ int len = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), (int)str.size(), nullptr, 0);
+ std::wstring ret(len, '\0');
+ MultiByteToWideChar(CP_UTF8, 0, str.c_str(), (int)str.size(), (LPWSTR)ret.data(), (int)ret.size());
+ return ret;
+}
+
+static inline std::string wstr2str(std::wstring const& str) {
+ int len = WideCharToMultiByte(CP_UTF8, 0, str.c_str(), (int)str.size(), nullptr, 0, nullptr, nullptr);
+ std::string ret(len, '\0');
+ WideCharToMultiByte(CP_UTF8, 0, str.c_str(), (int)str.size(), (LPSTR)ret.data(), (int)ret.size(), nullptr, nullptr);
+ return ret;
+}
+
+static inline bool is_vista() {
+ OSVERSIONINFOEXW osvi;
+ memset(&osvi, 0, sizeof(osvi));
+ DWORDLONG const mask =
+ VerSetConditionMask(VerSetConditionMask(VerSetConditionMask(0, VER_MAJORVERSION, VER_GREATER_EQUAL),
+ VER_MINORVERSION, VER_GREATER_EQUAL),
+ VER_SERVICEPACKMAJOR, VER_GREATER_EQUAL);
+ osvi.dwOSVersionInfoSize = sizeof(osvi);
+ osvi.dwMajorVersion = HIBYTE(_WIN32_WINNT_VISTA);
+ osvi.dwMinorVersion = LOBYTE(_WIN32_WINNT_VISTA);
+ osvi.wServicePackMajor = 0;
+
+ return VerifyVersionInfoW(&osvi, VER_MAJORVERSION | VER_MINORVERSION | VER_SERVICEPACKMAJOR, mask) != FALSE;
+}
+#endif
+
+// This is necessary until C++20 which will have std::string::ends_with() etc.
+
+static inline bool ends_with(std::string const& str, std::string const& suffix) {
+ return suffix.size() <= str.size() && str.compare(str.size() - suffix.size(), suffix.size(), suffix) == 0;
+}
+
+static inline bool starts_with(std::string const& str, std::string const& prefix) {
+ return prefix.size() <= str.size() && str.compare(0, prefix.size(), prefix) == 0;
+}
+
+// This is necessary until C++17 which will have std::filesystem::is_directory
+
+static inline bool is_directory(std::string const& path) {
+#if _WIN32
+ auto attr = GetFileAttributesA(path.c_str());
+ return attr != INVALID_FILE_ATTRIBUTES && (attr & FILE_ATTRIBUTE_DIRECTORY);
+#elif __EMSCRIPTEN__
+ // TODO
+ return false;
+#else
+ struct stat s;
+ return stat(path.c_str(), &s) == 0 && S_ISDIR(s.st_mode);
+#endif
+}
+
+// This is necessary because getenv is not thread-safe
+
+static inline std::string getenv(std::string const& str) {
+#if _MSC_VER
+ char* buf = nullptr;
+ size_t size = 0;
+ if (_dupenv_s(&buf, &size, str.c_str()) == 0 && buf) {
+ std::string ret(buf);
+ free(buf);
+ return ret;
+ }
+ return "";
+#else
+ auto buf = std::getenv(str.c_str());
+ return buf ? buf : "";
+#endif
+}
+
+} // namespace internal
+
+// settings implementation
+
+inline settings::settings(bool resync) {
+ flags(flag::is_scanned) &= !resync;
+
+ if (flags(flag::is_scanned))
+ return;
+
+ auto pfd_verbose = internal::getenv("PFD_VERBOSE");
+ auto match_no = std::regex("(|0|no|false)", std::regex_constants::icase);
+ if (!std::regex_match(pfd_verbose, match_no))
+ flags(flag::is_verbose) = true;
+
+#if _WIN32
+ flags(flag::is_vista) = internal::is_vista();
+#elif !__APPLE__
+ flags(flag::has_zenity) = check_program("zenity");
+ flags(flag::has_matedialog) = check_program("matedialog");
+ flags(flag::has_qarma) = check_program("qarma");
+ flags(flag::has_kdialog) = check_program("kdialog");
+
+ // If multiple helpers are available, try to default to the best one
+ if (flags(flag::has_zenity) && flags(flag::has_kdialog)) {
+ auto desktop_name = internal::getenv("XDG_SESSION_DESKTOP");
+ if (desktop_name == std::string("gnome"))
+ flags(flag::has_kdialog) = false;
+ else if (desktop_name == std::string("KDE"))
+ flags(flag::has_zenity) = false;
+ }
+#endif
+
+ flags(flag::is_scanned) = true;
+}
+
+inline bool settings::available() {
+#if _WIN32
+ return true;
+#elif __APPLE__
+ return true;
+#elif __EMSCRIPTEN__
+ // FIXME: Return true after implementation is complete.
+ return false;
+#else
+ settings tmp;
+ return tmp.flags(flag::has_zenity) || tmp.flags(flag::has_matedialog) || tmp.flags(flag::has_qarma) ||
+ tmp.flags(flag::has_kdialog);
+#endif
+}
+
+inline void settings::verbose(bool value) {
+ settings().flags(flag::is_verbose) = value;
+}
+
+inline void settings::rescan() {
+ settings(/* resync = */ true);
+}
+
+// Check whether a program is present using “which”.
+inline bool settings::check_program(std::string const& program) {
+#if _WIN32
+ (void)program;
+ return false;
+#elif __EMSCRIPTEN__
+ (void)program;
+ return false;
+#else
+ int exit_code = -1;
+ internal::executor async;
+ async.start_process({ "/bin/sh", "-c", "which " + program });
+ async.result(&exit_code);
+ return exit_code == 0;
+#endif
+}
+
+inline bool settings::is_osascript() const {
+#if __APPLE__
+ return true;
+#else
+ return false;
+#endif
+}
+
+inline bool settings::is_zenity() const {
+ return flags(flag::has_zenity) || flags(flag::has_matedialog) || flags(flag::has_qarma);
+}
+
+inline bool settings::is_kdialog() const {
+ return flags(flag::has_kdialog);
+}
+
+inline bool const& settings::flags(flag in_flag) const {
+ static bool flags[size_t(flag::max_flag)];
+ return flags[size_t(in_flag)];
+}
+
+inline bool& settings::flags(flag in_flag) {
+ return const_cast<bool&>(static_cast<settings const*>(this)->flags(in_flag));
+}
+
+// path implementation
+inline std::string path::home() {
+#if _WIN32
+ // First try the USERPROFILE environment variable
+ auto user_profile = internal::getenv("USERPROFILE");
+ if (user_profile.size() > 0)
+ return user_profile;
+ // Otherwise, try GetUserProfileDirectory()
+ HANDLE token = nullptr;
+ DWORD len = MAX_PATH;
+ char buf[MAX_PATH] = { '\0' };
+ if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) {
+ dll userenv("userenv.dll");
+ dll::proc<BOOL WINAPI(HANDLE, LPSTR, LPDWORD)> get_user_profile_directory(userenv, "GetUserProfileDirectoryA");
+ get_user_profile_directory(token, buf, &len);
+ CloseHandle(token);
+ if (*buf)
+ return buf;
+ }
+#elif __EMSCRIPTEN__
+ return "/";
+#else
+ // First try the HOME environment variable
+ auto home = internal::getenv("HOME");
+ if (home.size() > 0)
+ return home;
+ // Otherwise, try getpwuid_r()
+ size_t len = 4096;
+#if defined(_SC_GETPW_R_SIZE_MAX)
+ auto size_max = sysconf(_SC_GETPW_R_SIZE_MAX);
+ if (size_max != -1)
+ len = size_t(size_max);
+#endif
+ std::vector<char> buf(len);
+ struct passwd pwd, *result;
+ if (getpwuid_r(getuid(), &pwd, buf.data(), buf.size(), &result) == 0)
+ return result->pw_dir;
+#endif
+ return "/";
+}
+
+inline std::string path::separator() {
+#if _WIN32
+ return "\\";
+#else
+ return "/";
+#endif
+}
+
+// executor implementation
+
+inline std::string internal::executor::result(int* exit_code /* = nullptr */) {
+ stop();
+ if (exit_code)
+ *exit_code = m_exit_code;
+ return m_stdout;
+}
+
+inline bool internal::executor::kill() {
+#if _WIN32
+ if (m_future.valid()) {
+ // Close all windows that weren’t open when we started the future
+ auto previous_windows = m_windows;
+ EnumWindows(&enum_windows_callback, (LPARAM)this);
+ for (auto hwnd : m_windows)
+ if (previous_windows.find(hwnd) == previous_windows.end()) {
+ SendMessage(hwnd, WM_CLOSE, 0, 0);
+ // Also send IDNO in case of a Yes/No or Abort/Retry/Ignore messagebox
+ SendMessage(hwnd, WM_COMMAND, IDNO, 0);
+ }
+ }
+#elif __EMSCRIPTEN__ || __NX__
+ // FIXME: do something
+ return false; // cannot kill
+#else
+ ::kill(m_pid, SIGKILL);
+#endif
+ stop();
+ return true;
+}
+
+#if _WIN32
+inline BOOL CALLBACK internal::executor::enum_windows_callback(HWND hwnd, LPARAM lParam) {
+ auto that = (executor*)lParam;
+
+ DWORD pid;
+ auto tid = GetWindowThreadProcessId(hwnd, &pid);
+ if (tid == that->m_tid)
+ that->m_windows.insert(hwnd);
+ return TRUE;
+}
+#endif
+
+#if _WIN32
+inline void internal::executor::start_func(std::function<std::string(int*)> const& fun) {
+ stop();
+
+ auto trampoline = [fun, this]() {
+ // Save our thread id so that the caller can cancel us
+ m_tid = GetCurrentThreadId();
+ EnumWindows(&enum_windows_callback, (LPARAM)this);
+ m_cond.notify_all();
+ return fun(&m_exit_code);
+ };
+
+ std::unique_lock<std::mutex> lock(m_mutex);
+ m_future = std::async(std::launch::async, trampoline);
+ m_cond.wait(lock);
+ m_running = true;
+}
+
+#elif __EMSCRIPTEN__
+inline void internal::executor::start(int exit_code) {
+ m_exit_code = exit_code;
+}
+
+#else
+inline void internal::executor::start_process(std::vector<std::string> const& command) {
+ stop();
+ m_stdout.clear();
+ m_exit_code = -1;
+
+ int in[2], out[2];
+ if (pipe(in) != 0 || pipe(out) != 0)
+ return;
+
+ m_pid = fork();
+ if (m_pid < 0)
+ return;
+
+ close(in[m_pid ? 0 : 1]);
+ close(out[m_pid ? 1 : 0]);
+
+ if (m_pid == 0) {
+ dup2(in[0], STDIN_FILENO);
+ dup2(out[1], STDOUT_FILENO);
+
+ // Ignore stderr so that it doesn’t pollute the console (e.g. GTK+ errors from zenity)
+ int fd = open("/dev/null", O_WRONLY);
+ dup2(fd, STDERR_FILENO);
+ close(fd);
+
+ std::vector<char*> args;
+ std::transform(command.cbegin(), command.cend(), std::back_inserter(args),
+ [](std::string const& s) { return const_cast<char*>(s.c_str()); });
+ args.push_back(nullptr); // null-terminate argv[]
+
+ execvp(args[0], args.data());
+ exit(1);
+ }
+
+ close(in[1]);
+ m_fd = out[0];
+ auto flags = fcntl(m_fd, F_GETFL);
+ fcntl(m_fd, F_SETFL, flags | O_NONBLOCK);
+
+ m_running = true;
+}
+#endif
+
+inline internal::executor::~executor() {
+ stop();
+}
+
+inline bool internal::executor::ready(int timeout /* = default_wait_timeout */) {
+ if (!m_running)
+ return true;
+
+#if _WIN32
+ if (m_future.valid()) {
+ auto status = m_future.wait_for(std::chrono::milliseconds(timeout));
+ if (status != std::future_status::ready) {
+ // On Windows, we need to run the message pump. If the async
+ // thread uses a Windows API dialog, it may be attached to the
+ // main thread and waiting for messages that only we can dispatch.
+ MSG msg;
+ while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) {
+ TranslateMessage(&msg);
+ DispatchMessage(&msg);
+ }
+ return false;
+ }
+
+ m_stdout = m_future.get();
+ }
+#elif __EMSCRIPTEN__ || __NX__
+ // FIXME: do something
+ (void)timeout;
+#else
+ char buf[BUFSIZ];
+ ssize_t received = read(m_fd, buf, BUFSIZ); // Flawfinder: ignore
+ if (received > 0) {
+ m_stdout += std::string(buf, received);
+ return false;
+ }
+
+ // Reap child process if it is dead. It is possible that the system has already reaped it
+ // (this happens when the calling application handles or ignores SIG_CHLD) and results in
+ // waitpid() failing with ECHILD. Otherwise we assume the child is running and we sleep for
+ // a little while.
+ int status;
+ pid_t child = waitpid(m_pid, &status, WNOHANG);
+ if (child != m_pid && (child >= 0 || errno != ECHILD)) {
+ // FIXME: this happens almost always at first iteration
+ std::this_thread::sleep_for(std::chrono::milliseconds(timeout));
+ return false;
+ }
+
+ close(m_fd);
+ m_exit_code = WEXITSTATUS(status);
+#endif
+
+ m_running = false;
+ return true;
+}
+
+inline void internal::executor::stop() {
+ // Loop until the user closes the dialog
+ while (!ready())
+ ;
+}
+
+// dll implementation
+
+#if _WIN32
+inline internal::platform::dll::dll(std::string const& name) : handle(::LoadLibraryA(name.c_str())) {
+}
+
+inline internal::platform::dll::~dll() {
+ if (handle)
+ ::FreeLibrary(handle);
+}
+#endif // _WIN32
+
+// ole32_dll implementation
+
+#if _WIN32
+inline internal::platform::ole32_dll::ole32_dll() : dll("ole32.dll") {
+ // Use COINIT_MULTITHREADED because COINIT_APARTMENTTHREADED causes crashes.
+ // See https://github.com/samhocevar/portable-file-dialogs/issues/51
+ auto coinit = proc<HRESULT WINAPI(LPVOID, DWORD)>(*this, "CoInitializeEx");
+ m_state = coinit(nullptr, COINIT_MULTITHREADED);
+}
+
+inline internal::platform::ole32_dll::~ole32_dll() {
+ if (is_initialized())
+ proc<void WINAPI()>(*this, "CoUninitialize")();
+}
+
+inline bool internal::platform::ole32_dll::is_initialized() {
+ return m_state == S_OK || m_state == S_FALSE;
+}
+#endif
+
+// new_style_context implementation
+
+#if _WIN32
+inline internal::platform::new_style_context::new_style_context() {
+ // Only create one activation context for the whole app lifetime.
+ static HANDLE hctx = create();
+
+ if (hctx != INVALID_HANDLE_VALUE)
+ ActivateActCtx(hctx, &m_cookie);
+}
+
+inline internal::platform::new_style_context::~new_style_context() {
+ DeactivateActCtx(0, m_cookie);
+}
+
+inline HANDLE internal::platform::new_style_context::create() {
+ // This “hack” seems to be necessary for this code to work on windows XP.
+ // Without it, dialogs do not show and close immediately. GetError()
+ // returns 0 so I don’t know what causes this. I was not able to reproduce
+ // this behavior on Windows 7 and 10 but just in case, let it be here for
+ // those versions too.
+ // This hack is not required if other dialogs are used (they load comdlg32
+ // automatically), only if message boxes are used.
+ dll comdlg32("comdlg32.dll");
+
+ // Using approach as shown here: https://stackoverflow.com/a/10444161
+ UINT len = ::GetSystemDirectoryA(nullptr, 0);
+ std::string sys_dir(len, '\0');
+ ::GetSystemDirectoryA(&sys_dir[0], len);
+
+ ACTCTXA act_ctx = {
+ // Do not set flag ACTCTX_FLAG_SET_PROCESS_DEFAULT, since it causes a
+ // crash with error “default context is already set”.
+ sizeof(act_ctx),
+ ACTCTX_FLAG_RESOURCE_NAME_VALID | ACTCTX_FLAG_ASSEMBLY_DIRECTORY_VALID,
+ "shell32.dll",
+ 0,
+ 0,
+ sys_dir.c_str(),
+ (LPCSTR)124,
+ nullptr,
+ 0,
+ };
+
+ return ::CreateActCtxA(&act_ctx);
+}
+#endif // _WIN32
+
+// dialog implementation
+
+inline bool internal::dialog::ready(int timeout /* = default_wait_timeout */) const {
+ return m_async->ready(timeout);
+}
+
+inline bool internal::dialog::kill() const {
+ return m_async->kill();
+}
+
+inline internal::dialog::dialog() : m_async(std::make_shared<executor>()) {
+}
+
+inline std::vector<std::string> internal::dialog::desktop_helper() const {
+#if __APPLE__
+ return { "osascript" };
+#else
+ return { flags(flag::has_zenity) ? "zenity"
+ : flags(flag::has_matedialog) ? "matedialog"
+ : flags(flag::has_qarma) ? "qarma"
+ : flags(flag::has_kdialog) ? "kdialog"
+ : "echo" };
+#endif
+}
+
+inline std::string internal::dialog::buttons_to_name(choice _choice) {
+ switch (_choice) {
+ case choice::ok_cancel:
+ return "okcancel";
+ case choice::yes_no:
+ return "yesno";
+ case choice::yes_no_cancel:
+ return "yesnocancel";
+ case choice::retry_cancel:
+ return "retrycancel";
+ case choice::abort_retry_ignore:
+ return "abortretryignore";
+ /* case choice::ok: */ default:
+ return "ok";
+ }
+}
+
+inline std::string internal::dialog::get_icon_name(icon _icon) {
+ switch (_icon) {
+ case icon::warning:
+ return "warning";
+ case icon::error:
+ return "error";
+ case icon::question:
+ return "question";
+ // Zenity wants "information" but WinForms wants "info"
+ /* case icon::info: */ default:
+#if _WIN32
+ return "info";
+#else
+ return "information";
+#endif
+ }
+}
+
+// This is only used for debugging purposes
+inline std::ostream& operator<<(std::ostream& s, std::vector<std::string> const& v) {
+ int not_first = 0;
+ for (auto& e : v)
+ s << (not_first++ ? " " : "") << e;
+ return s;
+}
+
+// Properly quote a string for Powershell: replace ' or " with '' or ""
+// FIXME: we should probably get rid of newlines!
+// FIXME: the \" sequence seems unsafe, too!
+// XXX: this is no longer used but I would like to keep it around just in case
+inline std::string internal::dialog::powershell_quote(std::string const& str) const {
+ return "'" + std::regex_replace(str, std::regex("['\"]"), "$&$&") + "'";
+}
+
+// Properly quote a string for osascript: replace \ or " with \\ or \"
+// XXX: this also used to replace ' with \' when popen was used, but it would be
+// smarter to do shell_quote(osascript_quote(...)) if this is needed again.
+inline std::string internal::dialog::osascript_quote(std::string const& str) const {
+ return "\"" + std::regex_replace(str, std::regex("[\\\\\"]"), "\\$&") + "\"";
+}
+
+// Properly quote a string for the shell: just replace ' with '\''
+// XXX: this is no longer used but I would like to keep it around just in case
+inline std::string internal::dialog::shell_quote(std::string const& str) const {
+ return "'" + std::regex_replace(str, std::regex("'"), "'\\''") + "'";
+}
+
+// file_dialog implementation
+
+inline internal::file_dialog::file_dialog(type in_type, std::string const& title,
+ std::string const& default_path /* = "" */,
+ std::vector<std::string> const& filters /* = {} */,
+ opt options /* = opt::none */) {
+#if _WIN32
+ std::string filter_list;
+ std::regex whitespace(" *");
+ for (size_t i = 0; i + 1 < filters.size(); i += 2) {
+ filter_list += filters[i] + '\0';
+ filter_list += std::regex_replace(filters[i + 1], whitespace, ";") + '\0';
+ }
+ filter_list += '\0';
+
+ m_async->start_func([this, in_type, title, default_path, filter_list, options](int* exit_code) -> std::string {
+ (void)exit_code;
+ m_wtitle = internal::str2wstr(title);
+ m_wdefault_path = internal::str2wstr(default_path);
+ auto wfilter_list = internal::str2wstr(filter_list);
+
+ // Initialise COM. This is required for the new folder selection window,
+ // (see https://github.com/samhocevar/portable-file-dialogs/pull/21)
+ // and to avoid random crashes with GetOpenFileNameW() (see
+ // https://github.com/samhocevar/portable-file-dialogs/issues/51)
+ ole32_dll ole32;
+
+ // Folder selection uses a different method
+ if (in_type == type::folder) {
+#if PFD_HAS_IFILEDIALOG
+ if (flags(flag::is_vista)) {
+ // On Vista and higher we should be able to use IFileDialog for folder selection
+ IFileDialog* ifd;
+ HRESULT hr = dll::proc<HRESULT WINAPI(REFCLSID, LPUNKNOWN, DWORD, REFIID, LPVOID*)>(
+ ole32, "CoCreateInstance")(CLSID_FileOpenDialog, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&ifd));
+
+ // In case CoCreateInstance fails (which it should not), try legacy approach
+ if (SUCCEEDED(hr))
+ return select_folder_vista(ifd, options & opt::force_path);
+ }
+#endif
+
+ BROWSEINFOW bi;
+ memset(&bi, 0, sizeof(bi));
+
+ bi.lpfn = &bffcallback;
+ bi.lParam = (LPARAM)this;
+
+ if (flags(flag::is_vista)) {
+ if (ole32.is_initialized())
+ bi.ulFlags |= BIF_NEWDIALOGSTYLE;
+ bi.ulFlags |= BIF_EDITBOX;
+ bi.ulFlags |= BIF_STATUSTEXT;
+ }
+
+ auto* list = SHBrowseForFolderW(&bi);
+ std::string ret;
+ if (list) {
+ auto buffer = new wchar_t[MAX_PATH];
+ SHGetPathFromIDListW(list, buffer);
+ dll::proc<void WINAPI(LPVOID)>(ole32, "CoTaskMemFree")(list);
+ ret = internal::wstr2str(buffer);
+ delete[] buffer;
+ }
+ return ret;
+ }
+
+ OPENFILENAMEW ofn;
+ memset(&ofn, 0, sizeof(ofn));
+ ofn.lStructSize = sizeof(OPENFILENAMEW);
+ ofn.hwndOwner = GetActiveWindow();
+
+ ofn.lpstrFilter = wfilter_list.c_str();
+
+ auto woutput = std::wstring(MAX_PATH * 256, L'\0');
+ ofn.lpstrFile = (LPWSTR)woutput.data();
+ ofn.nMaxFile = (DWORD)woutput.size();
+ if (!m_wdefault_path.empty()) {
+ // If a directory was provided, use it as the initial directory. If
+ // a valid path was provided, use it as the initial file. Otherwise,
+ // let the Windows API decide.
+ auto path_attr = GetFileAttributesW(m_wdefault_path.c_str());
+ if (path_attr != INVALID_FILE_ATTRIBUTES && (path_attr & FILE_ATTRIBUTE_DIRECTORY))
+ ofn.lpstrInitialDir = m_wdefault_path.c_str();
+ else if (m_wdefault_path.size() <= woutput.size())
+ // second argument is size of buffer, not length of string
+ StringCchCopyW(ofn.lpstrFile, MAX_PATH * 256 + 1, m_wdefault_path.c_str());
+ else {
+ ofn.lpstrFileTitle = (LPWSTR)m_wdefault_path.data();
+ ofn.nMaxFileTitle = (DWORD)m_wdefault_path.size();
+ }
+ }
+ ofn.lpstrTitle = m_wtitle.c_str();
+ ofn.Flags = OFN_NOCHANGEDIR | OFN_EXPLORER;
+
+ dll comdlg32("comdlg32.dll");
+
+ // Apply new visual style (required for windows XP)
+ new_style_context ctx;
+
+ if (in_type == type::save) {
+ if (!(options & opt::force_overwrite))
+ ofn.Flags |= OFN_OVERWRITEPROMPT;
+
+ dll::proc<BOOL WINAPI(LPOPENFILENAMEW)> get_save_file_name(comdlg32, "GetSaveFileNameW");
+ if (get_save_file_name(&ofn) == 0)
+ return "";
+ return internal::wstr2str(woutput.c_str());
+ } else {
+ if (options & opt::multiselect)
+ ofn.Flags |= OFN_ALLOWMULTISELECT;
+ ofn.Flags |= OFN_PATHMUSTEXIST;
+
+ dll::proc<BOOL WINAPI(LPOPENFILENAMEW)> get_open_file_name(comdlg32, "GetOpenFileNameW");
+ if (get_open_file_name(&ofn) == 0)
+ return "";
+ }
+
+ std::string prefix;
+ for (wchar_t const* p = woutput.c_str(); *p;) {
+ auto filename = internal::wstr2str(p);
+ p += wcslen(p);
+ // In multiselect mode, we advance p one wchar further and
+ // check for another filename. If there is one and the
+ // prefix is empty, it means we just read the prefix.
+ if ((options & opt::multiselect) && *++p && prefix.empty()) {
+ prefix = filename + "/";
+ continue;
+ }
+
+ m_vector_result.push_back(prefix + filename);
+ }
+
+ return "";
+ });
+#elif __EMSCRIPTEN__
+ // FIXME: do something
+ (void)in_type;
+ (void)title;
+ (void)default_path;
+ (void)filters;
+ (void)options;
+#else
+ auto command = desktop_helper();
+
+ if (is_osascript()) {
+ std::string script = "set ret to choose";
+ switch (in_type) {
+ case type::save:
+ script += " file name";
+ break;
+ case type::open:
+ default:
+ script += " file";
+ if (options & opt::multiselect)
+ script += " with multiple selections allowed";
+ break;
+ case type::folder:
+ script += " folder";
+ break;
+ }
+
+ if (default_path.size()) {
+ if (in_type == type::folder || is_directory(default_path))
+ script += " default location ";
+ else
+ script += " default name ";
+ script += osascript_quote(default_path);
+ }
+
+ script += " with prompt " + osascript_quote(title);
+
+ if (in_type == type::open) {
+ // Concatenate all user-provided filter patterns
+ std::string patterns;
+ for (size_t i = 0; i < filters.size() / 2; ++i)
+ patterns += " " + filters[2 * i + 1];
+
+ // Split the pattern list to check whether "*" is in there; if it
+ // is, we have to disable filters because there is no mechanism in
+ // OS X for the user to override the filter.
+ std::regex sep("\\s+");
+ std::string filter_list;
+ bool has_filter = true;
+ std::sregex_token_iterator iter(patterns.begin(), patterns.end(), sep, -1);
+ std::sregex_token_iterator end;
+ for (; iter != end; ++iter) {
+ auto pat = iter->str();
+ if (pat == "*" || pat == "*.*")
+ has_filter = false;
+ else if (internal::starts_with(pat, "*."))
+ filter_list += "," + osascript_quote(pat.substr(2, pat.size() - 2));
+ }
+
+ if (has_filter && filter_list.size() > 0) {
+ // There is a weird AppleScript bug where file extensions of length != 3 are
+ // ignored, e.g. type{"txt"} works, but type{"json"} does not. Fortunately if
+ // the whole list starts with a 3-character extension, everything works again.
+ // We use "///" for such an extension because we are sure it cannot appear in
+ // an actual filename.
+ script += " of type {\"///\"" + filter_list + "}";
+ }
+ }
+
+ if (in_type == type::open && (options & opt::multiselect)) {
+ script += "\nset s to \"\"";
+ script += "\nrepeat with i in ret";
+ script += "\n set s to s & (POSIX path of i) & \"\\n\"";
+ script += "\nend repeat";
+ script += "\ncopy s to stdout";
+ } else {
+ script += "\nPOSIX path of ret";
+ }
+
+ command.push_back("-e");
+ command.push_back(script);
+ } else if (is_zenity()) {
+ command.push_back("--file-selection");
+
+ // If the default path is a directory, make sure it ends with "/" otherwise zenity will
+ // open the file dialog in the parent directory.
+ auto filename_arg = "--filename=" + default_path;
+ if (in_type != type::folder && !ends_with(default_path, "/") && internal::is_directory(default_path))
+ filename_arg += "/";
+ command.push_back(filename_arg);
+
+ command.push_back("--title");
+ command.push_back(title);
+ command.push_back("--separator=\n");
+
+ for (size_t i = 0; i < filters.size() / 2; ++i) {
+ command.push_back("--file-filter");
+ command.push_back(filters[2 * i] + "|" + filters[2 * i + 1]);
+ }
+
+ if (in_type == type::save)
+ command.push_back("--save");
+ if (in_type == type::folder)
+ command.push_back("--directory");
+ if (!(options & opt::force_overwrite))
+ command.push_back("--confirm-overwrite");
+ if (options & opt::multiselect)
+ command.push_back("--multiple");
+ } else if (is_kdialog()) {
+ switch (in_type) {
+ case type::save:
+ command.push_back("--getsavefilename");
+ break;
+ case type::open:
+ command.push_back("--getopenfilename");
+ break;
+ case type::folder:
+ command.push_back("--getexistingdirectory");
+ break;
+ }
+ if (options & opt::multiselect) {
+ command.push_back("--multiple");
+ command.push_back("--separate-output");
+ }
+
+ command.push_back(default_path);
+
+ std::string filter;
+ for (size_t i = 0; i < filters.size() / 2; ++i)
+ filter += (i == 0 ? "" : " | ") + filters[2 * i] + "(" + filters[2 * i + 1] + ")";
+ command.push_back(filter);
+
+ command.push_back("--title");
+ command.push_back(title);
+ }
+
+ if (flags(flag::is_verbose))
+ std::cerr << "pfd: " << command << std::endl;
+
+ m_async->start_process(command);
+#endif
+}
+
+inline std::string internal::file_dialog::string_result() {
+#if _WIN32
+ return m_async->result();
+#else
+ auto ret = m_async->result();
+ // Strip potential trailing newline (zenity). Also strip trailing slash
+ // added by osascript for consistency with other backends.
+ while (!ret.empty() && (ret.back() == '\n' || ret.back() == '/'))
+ ret.pop_back();
+ return ret;
+#endif
+}
+
+inline std::vector<std::string> internal::file_dialog::vector_result() {
+#if _WIN32
+ m_async->result();
+ return m_vector_result;
+#else
+ std::vector<std::string> ret;
+ auto result = m_async->result();
+ for (;;) {
+ // Split result along newline characters
+ auto i = result.find('\n');
+ if (i == 0 || i == std::string::npos)
+ break;
+ ret.push_back(result.substr(0, i));
+ result = result.substr(i + 1, result.size());
+ }
+ return ret;
+#endif
+}
+
+#if _WIN32
+// Use a static function to pass as BFFCALLBACK for legacy folder select
+inline int CALLBACK internal::file_dialog::bffcallback(HWND hwnd, UINT uMsg, LPARAM, LPARAM pData) {
+ auto inst = (file_dialog*)pData;
+ switch (uMsg) {
+ case BFFM_INITIALIZED:
+ SendMessage(hwnd, BFFM_SETSELECTIONW, TRUE, (LPARAM)inst->m_wdefault_path.c_str());
+ break;
+ }
+ return 0;
+}
+
+#if PFD_HAS_IFILEDIALOG
+inline std::string internal::file_dialog::select_folder_vista(IFileDialog* ifd, bool force_path) {
+ std::string result;
+
+ IShellItem* folder;
+
+ // Load library at runtime so app doesn't link it at load time (which will fail on windows XP)
+ dll shell32("shell32.dll");
+ dll::proc<HRESULT WINAPI(PCWSTR, IBindCtx*, REFIID, void**)> create_item(shell32, "SHCreateItemFromParsingName");
+
+ if (!create_item)
+ return "";
+
+ auto hr = create_item(m_wdefault_path.c_str(), nullptr, IID_PPV_ARGS(&folder));
+
+ // Set default folder if found. This only sets the default folder. If
+ // Windows has any info about the most recently selected folder, it
+ // will display it instead. Generally, calling SetFolder() to set the
+ // current directory “is not a good or expected user experience and
+ // should therefore be avoided”:
+ // https://docs.microsoft.com/windows/win32/api/shobjidl_core/nf-shobjidl_core-ifiledialog-setfolder
+ if (SUCCEEDED(hr)) {
+ if (force_path)
+ ifd->SetFolder(folder);
+ else
+ ifd->SetDefaultFolder(folder);
+ folder->Release();
+ }
+
+ // Set the dialog title and option to select folders
+ ifd->SetOptions(FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM);
+ ifd->SetTitle(m_wtitle.c_str());
+
+ hr = ifd->Show(GetActiveWindow());
+ if (SUCCEEDED(hr)) {
+ IShellItem* item;
+ hr = ifd->GetResult(&item);
+ if (SUCCEEDED(hr)) {
+ wchar_t* wname = nullptr;
+ // This is unlikely to fail because we use FOS_FORCEFILESYSTEM, but try
+ // to output a debug message just in case.
+ if (SUCCEEDED(item->GetDisplayName(SIGDN_FILESYSPATH, &wname))) {
+ result = internal::wstr2str(std::wstring(wname));
+ dll::proc<void WINAPI(LPVOID)>(ole32_dll(), "CoTaskMemFree")(wname);
+ } else {
+ if (SUCCEEDED(item->GetDisplayName(SIGDN_NORMALDISPLAY, &wname))) {
+ auto name = internal::wstr2str(std::wstring(wname));
+ dll::proc<void WINAPI(LPVOID)>(ole32_dll(), "CoTaskMemFree")(wname);
+ std::cerr << "pfd: failed to get path for " << name << std::endl;
+ } else
+ std::cerr << "pfd: item of unknown type selected" << std::endl;
+ }
+
+ item->Release();
+ }
+ }
+
+ ifd->Release();
+
+ return result;
+}
+#endif
+#endif
+
+// notify implementation
+
+inline notify::notify(std::string const& title, std::string const& message, icon _icon /* = icon::info */) {
+ if (_icon == icon::question) // Not supported by notifications
+ _icon = icon::info;
+
+#if _WIN32
+ // Use a static shared pointer for notify_icon so that we can delete
+ // it whenever we need to display a new one, and we can also wait
+ // until the program has finished running.
+ struct notify_icon_data : public NOTIFYICONDATAW {
+ ~notify_icon_data() {
+ Shell_NotifyIconW(NIM_DELETE, this);
+ }
+ };
+
+ static std::shared_ptr<notify_icon_data> nid;
+
+ // Release the previous notification icon, if any, and allocate a new
+ // one. Note that std::make_shared() does value initialization, so there
+ // is no need to memset the structure.
+ nid = nullptr;
+ nid = std::make_shared<notify_icon_data>();
+
+ // For XP support
+ nid->cbSize = NOTIFYICONDATAW_V2_SIZE;
+ nid->hWnd = nullptr;
+ nid->uID = 0;
+
+ // Flag Description:
+ // - NIF_ICON The hIcon member is valid.
+ // - NIF_MESSAGE The uCallbackMessage member is valid.
+ // - NIF_TIP The szTip member is valid.
+ // - NIF_STATE The dwState and dwStateMask members are valid.
+ // - NIF_INFO Use a balloon ToolTip instead of a standard ToolTip. The szInfo, uTimeout, szInfoTitle, and
+ // dwInfoFlags members are valid.
+ // - NIF_GUID Reserved.
+ nid->uFlags = NIF_MESSAGE | NIF_ICON | NIF_INFO;
+
+ // Flag Description
+ // - NIIF_ERROR An error icon.
+ // - NIIF_INFO An information icon.
+ // - NIIF_NONE No icon.
+ // - NIIF_WARNING A warning icon.
+ // - NIIF_ICON_MASK Version 6.0. Reserved.
+ // - NIIF_NOSOUND Version 6.0. Do not play the associated sound. Applies only to balloon ToolTips
+ switch (_icon) {
+ case icon::warning:
+ nid->dwInfoFlags = NIIF_WARNING;
+ break;
+ case icon::error:
+ nid->dwInfoFlags = NIIF_ERROR;
+ break;
+ /* case icon::info: */ default:
+ nid->dwInfoFlags = NIIF_INFO;
+ break;
+ }
+
+ ENUMRESNAMEPROC icon_enum_callback = [](HMODULE, LPCTSTR, LPTSTR lpName, LONG_PTR lParam) -> BOOL {
+ ((NOTIFYICONDATAW*)lParam)->hIcon = ::LoadIcon(GetModuleHandle(nullptr), lpName);
+ return false;
+ };
+
+ nid->hIcon = ::LoadIcon(nullptr, IDI_APPLICATION);
+ ::EnumResourceNames(nullptr, RT_GROUP_ICON, icon_enum_callback, (LONG_PTR)nid.get());
+
+ nid->uTimeout = 5000;
+
+ StringCchCopyW(nid->szInfoTitle, ARRAYSIZE(nid->szInfoTitle), internal::str2wstr(title).c_str());
+ StringCchCopyW(nid->szInfo, ARRAYSIZE(nid->szInfo), internal::str2wstr(message).c_str());
+
+ // Display the new icon
+ Shell_NotifyIconW(NIM_ADD, nid.get());
+#elif __EMSCRIPTEN__
+ // FIXME: do something
+ (void)title;
+ (void)message;
+#else
+ auto command = desktop_helper();
+
+ if (is_osascript()) {
+ command.push_back("-e");
+ command.push_back("display notification " + osascript_quote(message) + " with title " + osascript_quote(title));
+ } else if (is_zenity()) {
+ command.push_back("--notification");
+ command.push_back("--window-icon");
+ command.push_back(get_icon_name(_icon));
+ command.push_back("--text");
+ command.push_back(title + "\n" + message);
+ } else if (is_kdialog()) {
+ command.push_back("--icon");
+ command.push_back(get_icon_name(_icon));
+ command.push_back("--title");
+ command.push_back(title);
+ command.push_back("--passivepopup");
+ command.push_back(message);
+ command.push_back("5");
+ }
+
+ if (flags(flag::is_verbose))
+ std::cerr << "pfd: " << command << std::endl;
+
+ m_async->start_process(command);
+#endif
+}
+
+// message implementation
+
+inline message::message(std::string const& title, std::string const& text, choice _choice /* = choice::ok_cancel */,
+ icon _icon /* = icon::info */) {
+#if _WIN32
+ // Use MB_SYSTEMMODAL rather than MB_TOPMOST to ensure the message window is brought
+ // to front. See https://github.com/samhocevar/portable-file-dialogs/issues/52
+ UINT style = MB_SYSTEMMODAL;
+ switch (_icon) {
+ case icon::warning:
+ style |= MB_ICONWARNING;
+ break;
+ case icon::error:
+ style |= MB_ICONERROR;
+ break;
+ case icon::question:
+ style |= MB_ICONQUESTION;
+ break;
+ /* case icon::info: */ default:
+ style |= MB_ICONINFORMATION;
+ break;
+ }
+
+ switch (_choice) {
+ case choice::ok_cancel:
+ style |= MB_OKCANCEL;
+ break;
+ case choice::yes_no:
+ style |= MB_YESNO;
+ break;
+ case choice::yes_no_cancel:
+ style |= MB_YESNOCANCEL;
+ break;
+ case choice::retry_cancel:
+ style |= MB_RETRYCANCEL;
+ break;
+ case choice::abort_retry_ignore:
+ style |= MB_ABORTRETRYIGNORE;
+ break;
+ /* case choice::ok: */ default:
+ style |= MB_OK;
+ break;
+ }
+
+ m_mappings[IDCANCEL] = button::cancel;
+ m_mappings[IDOK] = button::ok;
+ m_mappings[IDYES] = button::yes;
+ m_mappings[IDNO] = button::no;
+ m_mappings[IDABORT] = button::abort;
+ m_mappings[IDRETRY] = button::retry;
+ m_mappings[IDIGNORE] = button::ignore;
+
+ m_async->start_func([text, title, style](int* exit_code) -> std::string {
+ auto wtext = internal::str2wstr(text);
+ auto wtitle = internal::str2wstr(title);
+ // Apply new visual style (required for all Windows versions)
+ new_style_context ctx;
+ *exit_code = MessageBoxW(GetActiveWindow(), wtext.c_str(), wtitle.c_str(), style);
+ return "";
+ });
+
+#elif __EMSCRIPTEN__
+ std::string full_message;
+ switch (_icon) {
+ case icon::warning:
+ full_message = "⚠️";
+ break;
+ case icon::error:
+ full_message = "⛔";
+ break;
+ case icon::question:
+ full_message = "❓";
+ break;
+ /* case icon::info: */ default:
+ full_message = "ℹ";
+ break;
+ }
+
+ full_message += ' ' + title + "\n\n" + text;
+
+ // This does not really start an async task; it just passes the
+ // EM_ASM_INT return value to a fake start() function.
+ m_async->start(EM_ASM_INT(
+ {
+ if ($1)
+ return window.confirm(UTF8ToString($0)) ? 0 : -1;
+ alert(UTF8ToString($0));
+ return 0;
+ },
+ full_message.c_str(), _choice == choice::ok_cancel));
+#else
+ auto command = desktop_helper();
+
+ if (is_osascript()) {
+ std::string script = "display dialog " + osascript_quote(text) + " with title " + osascript_quote(title);
+ auto if_cancel = button::cancel;
+ switch (_choice) {
+ case choice::ok_cancel:
+ script += "buttons {\"OK\", \"Cancel\"}"
+ " default button \"OK\""
+ " cancel button \"Cancel\"";
+ break;
+ case choice::yes_no:
+ script += "buttons {\"Yes\", \"No\"}"
+ " default button \"Yes\""
+ " cancel button \"No\"";
+ if_cancel = button::no;
+ break;
+ case choice::yes_no_cancel:
+ script += "buttons {\"Yes\", \"No\", \"Cancel\"}"
+ " default button \"Yes\""
+ " cancel button \"Cancel\"";
+ break;
+ case choice::retry_cancel:
+ script += "buttons {\"Retry\", \"Cancel\"}"
+ " default button \"Retry\""
+ " cancel button \"Cancel\"";
+ break;
+ case choice::abort_retry_ignore:
+ script += "buttons {\"Abort\", \"Retry\", \"Ignore\"}"
+ " default button \"Abort\""
+ " cancel button \"Retry\"";
+ if_cancel = button::retry;
+ break;
+ case choice::ok:
+ default:
+ script += "buttons {\"OK\"}"
+ " default button \"OK\""
+ " cancel button \"OK\"";
+ if_cancel = button::ok;
+ break;
+ }
+ m_mappings[1] = if_cancel;
+ m_mappings[256] = if_cancel; // XXX: I think this was never correct
+ script += " with icon ";
+ switch (_icon) {
+#define PFD_OSX_ICON(n) \
+ "alias ((path to library folder from system domain) as text " \
+ "& \"CoreServices:CoreTypes.bundle:Contents:Resources:" n ".icns\")"
+ case icon::info:
+ default:
+ script += PFD_OSX_ICON("ToolBarInfo");
+ break;
+ case icon::warning:
+ script += "caution";
+ break;
+ case icon::error:
+ script += "stop";
+ break;
+ case icon::question:
+ script += PFD_OSX_ICON("GenericQuestionMarkIcon");
+ break;
+#undef PFD_OSX_ICON
+ }
+
+ command.push_back("-e");
+ command.push_back(script);
+ } else if (is_zenity()) {
+ switch (_choice) {
+ case choice::ok_cancel:
+ command.insert(command.end(), { "--question", "--cancel-label=Cancel", "--ok-label=OK" });
+ break;
+ case choice::yes_no:
+ // Do not use standard --question because it causes “No” to return -1,
+ // which is inconsistent with the “Yes/No/Cancel” mode below.
+ command.insert(command.end(), { "--question", "--switch", "--extra-button=No", "--extra-button=Yes" });
+ break;
+ case choice::yes_no_cancel:
+ command.insert(command.end(), { "--question", "--switch", "--extra-button=Cancel", "--extra-button=No",
+ "--extra-button=Yes" });
+ break;
+ case choice::retry_cancel:
+ command.insert(command.end(),
+ { "--question", "--switch", "--extra-button=Cancel", "--extra-button=Retry" });
+ break;
+ case choice::abort_retry_ignore:
+ command.insert(command.end(), { "--question", "--switch", "--extra-button=Ignore",
+ "--extra-button=Abort", "--extra-button=Retry" });
+ break;
+ case choice::ok:
+ default:
+ switch (_icon) {
+ case icon::error:
+ command.push_back("--error");
+ break;
+ case icon::warning:
+ command.push_back("--warning");
+ break;
+ default:
+ command.push_back("--info");
+ break;
+ }
+ }
+
+ command.insert(command.end(), { "--title", title, "--width=300", "--height=0", // sensible defaults
+ "--no-markup", // do not interpret text as Pango markup
+ "--text", text, "--icon-name=dialog-" + get_icon_name(_icon) });
+ } else if (is_kdialog()) {
+ if (_choice == choice::ok) {
+ switch (_icon) {
+ case icon::error:
+ command.push_back("--error");
+ break;
+ case icon::warning:
+ command.push_back("--sorry");
+ break;
+ default:
+ command.push_back("--msgbox");
+ break;
+ }
+ } else {
+ std::string flag = "--";
+ if (_icon == icon::warning || _icon == icon::error)
+ flag += "warning";
+ flag += "yesno";
+ if (_choice == choice::yes_no_cancel)
+ flag += "cancel";
+ command.push_back(flag);
+ if (_choice == choice::yes_no || _choice == choice::yes_no_cancel) {
+ m_mappings[0] = button::yes;
+ m_mappings[256] = button::no;
+ }
+ }
+
+ command.push_back(text);
+ command.push_back("--title");
+ command.push_back(title);
+
+ // Must be after the above part
+ if (_choice == choice::ok_cancel)
+ command.insert(command.end(), { "--yes-label", "OK", "--no-label", "Cancel" });
+ }
+
+ if (flags(flag::is_verbose))
+ std::cerr << "pfd: " << command << std::endl;
+
+ m_async->start_process(command);
+#endif
+}
+
+inline button message::result() {
+ int exit_code;
+ auto ret = m_async->result(&exit_code);
+ // osascript will say "button returned:Cancel\n"
+ // and others will just say "Cancel\n"
+ if (internal::ends_with(ret, "Cancel\n"))
+ return button::cancel;
+ if (internal::ends_with(ret, "OK\n"))
+ return button::ok;
+ if (internal::ends_with(ret, "Yes\n"))
+ return button::yes;
+ if (internal::ends_with(ret, "No\n"))
+ return button::no;
+ if (internal::ends_with(ret, "Abort\n"))
+ return button::abort;
+ if (internal::ends_with(ret, "Retry\n"))
+ return button::retry;
+ if (internal::ends_with(ret, "Ignore\n"))
+ return button::ignore;
+ if (m_mappings.count(exit_code) != 0)
+ return m_mappings[exit_code];
+ return exit_code == 0 ? button::ok : button::cancel;
+}
+
+// open_file implementation
+
+inline open_file::open_file(std::string const& title, std::string const& default_path /* = "" */,
+ std::vector<std::string> const& filters /* = { "All Files", "*" } */,
+ opt options /* = opt::none */)
+ : file_dialog(type::open, title, default_path, filters, options) {
+}
+
+inline open_file::open_file(std::string const& title, std::string const& default_path,
+ std::vector<std::string> const& filters, bool allow_multiselect)
+ : open_file(title, default_path, filters, (allow_multiselect ? opt::multiselect : opt::none)) {
+}
+
+inline std::vector<std::string> open_file::result() {
+ return vector_result();
+}
+
+// save_file implementation
+
+inline save_file::save_file(std::string const& title, std::string const& default_path /* = "" */,
+ std::vector<std::string> const& filters /* = { "All Files", "*" } */,
+ opt options /* = opt::none */)
+ : file_dialog(type::save, title, default_path, filters, options) {
+}
+
+inline save_file::save_file(std::string const& title, std::string const& default_path,
+ std::vector<std::string> const& filters, bool confirm_overwrite)
+ : save_file(title, default_path, filters, (confirm_overwrite ? opt::none : opt::force_overwrite)) {
+}
+
+inline std::string save_file::result() {
+ return string_result();
+}
+
+// select_folder implementation
+
+inline select_folder::select_folder(std::string const& title, std::string const& default_path /* = "" */,
+ opt options /* = opt::none */)
+ : file_dialog(type::folder, title, default_path, {}, options) {
+}
+
+inline std::string select_folder::result() {
+ return string_result();
+}
+
+#endif // PFD_SKIP_IMPLEMENTATION
+
+} // namespace pfd \ No newline at end of file
diff --git a/src/port/GameExtractor.cpp b/src/port/GameExtractor.cpp
new file mode 100644
index 000000000..4a222c681
--- /dev/null
+++ b/src/port/GameExtractor.cpp
@@ -0,0 +1,56 @@
+#include "GameExtractor.h"
+#include <unordered_map>
+
+#include <fstream>
+
+#include "Context.h"
+#include "spdlog/spdlog.h"
+#include "portable-file-dialogs.h"
+#include <port/Engine.h>
+
+std::unordered_map<std::string, std::string> mGameList = {
+ { "579c48e211ae952530ffc8738709f078d5dd215e", "Mario Kart 64 (US)" },
+};
+
+bool GameExtractor::SelectGameFromUI() {
+#if !defined(__IOS__) || !defined(__ANDROID__) || !defined(__SWITCH__)
+ auto selection = pfd::open_file("Select a file", ".", { "N64 Roms", "*.z64" }).result();
+
+ if (selection.empty()) {
+ return false;
+ }
+
+ this->mGamePath = selection[0];
+#else
+ this->mGamePath = Ship::Context::GetPathRelativeToAppDirectory("baserom.us.z64");
+#endif
+
+ std::ifstream file(this->mGamePath, std::ios::binary);
+ this->mGameData = std::vector<uint8_t>( std::istreambuf_iterator( file ), {} );
+ file.close();
+ return true;
+}
+
+std::optional<std::string> GameExtractor::ValidateChecksum() const {
+ const auto rom = new N64::Cartridge(this->mGameData);
+ rom->Initialize();
+ auto hash = rom->GetHash();
+
+ if (mGameList.find(hash) == mGameList.end()) {
+ return std::nullopt;
+ }
+
+ return mGameList[hash];
+}
+
+bool GameExtractor::GenerateOTR() const {
+ Companion::Instance = new Companion(this->mGameData, ArchiveType::O2R, false);
+
+ try {
+ Companion::Instance->Init(ExportType::Binary);
+ } catch (const std::exception& e) {
+ return false;
+ }
+
+ return true;
+}
diff --git a/src/port/GameExtractor.h b/src/port/GameExtractor.h
new file mode 100644
index 000000000..e51e83aae
--- /dev/null
+++ b/src/port/GameExtractor.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include "Companion.h"
+#include <filesystem>
+#include <vector>
+#include <cstdint>
+
+class GameExtractor {
+public:
+ static bool GenAssetFile();
+ std::optional<std::string> ValidateChecksum() const;
+ bool SelectGameFromUI();
+ bool GenerateOTR() const;
+private:
+ fs::path mGamePath;
+ std::vector<uint8_t> mGameData;
+};
diff --git a/torch b/torch
-Subproject cc3063afe6168bb19bc5b37580716e2b2465c87
+Subproject a1a50c60ece9854422cd2b9896525f4591fc6b8