diff --git a/CMakeLists.txt b/CMakeLists.txt index 1d6106d..bf71861 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,22 +1,35 @@ cmake_minimum_required(VERSION 3.4) project( snoretoast VERSION 0.5.99) +set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/) option(BUILD_EXAMPLES "Whether to build the examples" OFF) option(BUILD_STATIC_RUNTIME "Whether link statically to the msvc runtime" ON) include(GenerateExportHeader) +include(SnoreMacros) +include(cmakerc/CMakeRC) + set(CMAKE_CXX_STANDARD 17) set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/bin) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/bin) +if (BUILD_STATIC_RUNTIME) + #link runtime static + if(MSVC) + foreach(_bt DEBUG RELEASE RELWITHDEBINFO) + string(REPLACE "/MD" "/MT" CMAKE_CXX_FLAGS_${_bt} ${CMAKE_CXX_FLAGS_${_bt}}) + endforeach(_bt DEBUG RELEASE RELWITHDEBINFO) + endif(MSVC) +endif() + add_subdirectory(data) add_subdirectory(src) if (BUILD_EXAMPLES) add_subdirectory(examples) endif() diff --git a/cmake/cmakerc/CMakeRC.cmake b/cmake/cmakerc/CMakeRC.cmake new file mode 100644 index 0000000..409136f --- /dev/null +++ b/cmake/cmakerc/CMakeRC.cmake @@ -0,0 +1,617 @@ +# This block is executed when generating an intermediate resource file, not when +# running in CMake configure mode +if(_CMRC_GENERATE_MODE) + # Read in the digits + file(READ "${INPUT_FILE}" bytes HEX) + # Format each pair into a character literal. Heuristics seem to favor doing + # the conversion in groups of five for fastest conversion + string(REGEX REPLACE "(..)(..)(..)(..)(..)" "'\\\\x\\1','\\\\x\\2','\\\\x\\3','\\\\x\\4','\\\\x\\5'," chars "${bytes}") + # Since we did this in groups, we have some leftovers to clean up + string(LENGTH "${bytes}" n_bytes2) + math(EXPR n_bytes "${n_bytes2} / 2") + math(EXPR remainder "${n_bytes} % 5") # <-- '5' is the grouping count from above + set(cleanup_re "$") + set(cleanup_sub ) + while(remainder) + set(cleanup_re "(..)${cleanup_re}") + set(cleanup_sub "'\\\\x\\${remainder}',${cleanup_sub}") + math(EXPR remainder "${remainder} - 1") + endwhile() + if(NOT cleanup_re STREQUAL "$") + string(REGEX REPLACE "${cleanup_re}" "${cleanup_sub}" chars "${chars}") + endif() + string(CONFIGURE [[ + namespace { const char file_array[] = { @chars@ }; } + namespace cmrc { namespace @NAMESPACE@ { namespace res_chars { + extern const char* const @SYMBOL@_begin = file_array; + extern const char* const @SYMBOL@_end = file_array + @n_bytes@; + }}} + ]] code) + file(WRITE "${OUTPUT_FILE}" "${code}") + # Exit from the script. Nothing else needs to be processed + return() +endif() + +set(_version 2.0.0) + +cmake_minimum_required(VERSION 3.3) +include(CMakeParseArguments) + +if(COMMAND cmrc_add_resource_library) + if(NOT DEFINED _CMRC_VERSION OR NOT (_version STREQUAL _CMRC_VERSION)) + message(WARNING "More than one CMakeRC version has been included in this project.") + endif() + # CMakeRC has already been included! Don't do anything + return() +endif() + +set(_CMRC_VERSION "${_version}" CACHE INTERNAL "CMakeRC version. Used for checking for conflicts") + +set(_CMRC_SCRIPT "${CMAKE_CURRENT_LIST_FILE}" CACHE INTERNAL "Path to CMakeRC script") + +function(_cmrc_normalize_path var) + set(path "${${var}}") + file(TO_CMAKE_PATH "${path}" path) + while(path MATCHES "//") + string(REPLACE "//" "/" path "${path}") + endwhile() + string(REGEX REPLACE "/+$" "" path "${path}") + set("${var}" "${path}" PARENT_SCOPE) +endfunction() + +get_filename_component(_inc_dir "${CMAKE_BINARY_DIR}/_cmrc/include" ABSOLUTE) +set(CMRC_INCLUDE_DIR "${_inc_dir}" CACHE INTERNAL "Directory for CMakeRC include files") +# Let's generate the primary include file +file(MAKE_DIRECTORY "${CMRC_INCLUDE_DIR}/cmrc") +set(hpp_content [==[ +#ifndef CMRC_CMRC_HPP_INCLUDED +#define CMRC_CMRC_HPP_INCLUDED + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cmrc { namespace detail { struct dummy; } } + +#define CMRC_DECLARE(libid) \ + namespace cmrc { namespace detail { \ + struct dummy; \ + static_assert(std::is_same::value, "CMRC_DECLARE() must only appear at the global namespace"); \ + } } \ + namespace cmrc { namespace libid { \ + cmrc::embedded_filesystem get_filesystem(); \ + } } static_assert(true, "") + +namespace cmrc { + +class file { + const char* _begin = nullptr; + const char* _end = nullptr; + +public: + using iterator = const char*; + using const_iterator = iterator; + iterator begin() const noexcept { return _begin; } + iterator cbegin() const noexcept { return _begin; } + iterator end() const noexcept { return _end; } + iterator cend() const noexcept { return _end; } + std::size_t size() const { return std::distance(begin(), end()); } + + file() = default; + file(iterator beg, iterator end) noexcept : _begin(beg), _end(end) {} +}; + +class directory_entry; + +namespace detail { + +class directory; +class file_data; + +class file_or_directory { + union _data_t { + class file_data* file_data; + class directory* directory; + } _data; + bool _is_file = true; + +public: + explicit file_or_directory(file_data& f) { + _data.file_data = &f; + } + explicit file_or_directory(directory& d) { + _data.directory = &d; + _is_file = false; + } + bool is_file() const noexcept { + return _is_file; + } + bool is_directory() const noexcept { + return !is_file(); + } + const directory& as_directory() const noexcept { + assert(!is_file()); + return *_data.directory; + } + const file_data& as_file() const noexcept { + assert(is_file()); + return *_data.file_data; + } +}; + +class file_data { +public: + const char* begin_ptr; + const char* end_ptr; + file_data(const file_data&) = delete; + file_data(const char* b, const char* e) : begin_ptr(b), end_ptr(e) {} +}; + +inline std::pair split_path(const std::string& path) { + auto first_sep = path.find("/"); + if (first_sep == path.npos) { + return std::make_pair(path, ""); + } else { + return std::make_pair(path.substr(0, first_sep), path.substr(first_sep + 1)); + } +} + +struct created_subdirectory { + class directory& directory; + class file_or_directory& index_entry; +}; + +class directory { + std::list _files; + std::list _dirs; + std::map _index; + + using base_iterator = std::map::const_iterator; + +public: + + directory() = default; + directory(const directory&) = delete; + + created_subdirectory add_subdir(std::string name) & { + _dirs.emplace_back(); + auto& back = _dirs.back(); + auto& fod = _index.emplace(name, back).first->second; + return created_subdirectory{back, fod}; + } + + file_or_directory* add_file(std::string name, const char* begin, const char* end) & { + assert(_index.find(name) == _index.end()); + _files.emplace_back(begin, end); + return &_index.emplace(name, _files.back()).first->second; + } + + const file_or_directory* get(const std::string& path) const { + auto pair = split_path(path); + auto child = _index.find(pair.first); + if (child == _index.end()) { + return nullptr; + } + auto& entry = child->second; + if (pair.second.empty()) { + // We're at the end of the path + return &entry; + } + + if (entry.is_file()) { + // We can't traverse into a file. Stop. + return nullptr; + } + // Keep going down + return entry.as_directory().get(pair.second); + } + + class iterator { + base_iterator _base_iter; + base_iterator _end_iter; + public: + using value_type = directory_entry; + using difference_type = std::ptrdiff_t; + using pointer = const value_type*; + using reference = const value_type&; + using iterator_category = std::input_iterator_tag; + + iterator() = default; + explicit iterator(base_iterator iter, base_iterator end) : _base_iter(iter), _end_iter(end) {} + + iterator begin() const noexcept { + return *this; + } + + iterator end() const noexcept { + return iterator(_end_iter, _end_iter); + } + + inline value_type operator*() const noexcept; + + bool operator==(const iterator& rhs) const noexcept { + return _base_iter == rhs._base_iter; + } + + bool operator!=(const iterator& rhs) const noexcept { + return !(*this == rhs); + } + + iterator operator++() noexcept { + auto cp = *this; + ++_base_iter; + return cp; + } + + iterator& operator++(int) noexcept { + ++_base_iter; + return *this; + } + }; + + using const_iterator = iterator; + + iterator begin() const noexcept { + return iterator(_index.begin(), _index.end()); + } + + iterator end() const noexcept { + return iterator(); + } +}; + +inline std::string normalize_path(std::string path) { + while (path.find("/") == 0) { + path.erase(path.begin()); + } + while (!path.empty() && (path.rfind("/") == path.size() - 1)) { + path.pop_back(); + } + auto off = path.npos; + while ((off = path.find("//")) != path.npos) { + path.erase(path.begin() + off); + } + return path; +} + +using index_type = std::map; + +} // detail + +class directory_entry { + std::string _fname; + const detail::file_or_directory* _item; + +public: + directory_entry() = delete; + explicit directory_entry(std::string filename, const detail::file_or_directory& item) + : _fname(filename) + , _item(&item) + {} + + const std::string& filename() const & { + return _fname; + } + std::string filename() const && { + return std::move(_fname); + } + + bool is_file() const { + return _item->is_file(); + } + + bool is_directory() const { + return _item->is_directory(); + } +}; + +directory_entry detail::directory::iterator::operator*() const noexcept { + assert(begin() != end()); + return directory_entry(_base_iter->first, _base_iter->second); +} + +using directory_iterator = detail::directory::iterator; + +class embedded_filesystem { + // Never-null: + const cmrc::detail::index_type* _index; + const detail::file_or_directory* _get(std::string path) const { + path = detail::normalize_path(path); + auto found = _index->find(path); + if (found == _index->end()) { + return nullptr; + } else { + return found->second; + } + } + +public: + explicit embedded_filesystem(const detail::index_type& index) + : _index(&index) + {} + + file open(const std::string& path) const { + auto entry_ptr = _get(path); + if (!entry_ptr || !entry_ptr->is_file()) { + throw std::system_error(make_error_code(std::errc::no_such_file_or_directory), path); + } + auto& dat = entry_ptr->as_file(); + return file{dat.begin_ptr, dat.end_ptr}; + } + + bool is_file(const std::string& path) const noexcept { + auto entry_ptr = _get(path); + return entry_ptr && entry_ptr->is_file(); + } + + bool is_directory(const std::string& path) const noexcept { + auto entry_ptr = _get(path); + return entry_ptr && entry_ptr->is_directory(); + } + + bool exists(const std::string& path) const noexcept { + return !!_get(path); + } + + directory_iterator iterate_directory(const std::string& path) const { + auto entry_ptr = _get(path); + if (!entry_ptr) { + throw std::system_error(make_error_code(std::errc::no_such_file_or_directory), path); + } + if (!entry_ptr->is_directory()) { + throw std::system_error(make_error_code(std::errc::not_a_directory), path); + } + return entry_ptr->as_directory().begin(); + } +}; + +} + +#endif // CMRC_CMRC_HPP_INCLUDED +]==]) + +set(cmrc_hpp "${CMRC_INCLUDE_DIR}/cmrc/cmrc.hpp" CACHE INTERNAL "") +set(_generate 1) +if(EXISTS "${cmrc_hpp}") + file(READ "${cmrc_hpp}" _current) + if(_current STREQUAL hpp_content) + set(_generate 0) + endif() +endif() +file(GENERATE OUTPUT "${cmrc_hpp}" CONTENT "${hpp_content}" CONDITION ${_generate}) + +add_library(cmrc-base INTERFACE) +target_include_directories(cmrc-base INTERFACE "${CMRC_INCLUDE_DIR}") +# Signal a basic C++11 feature to require C++11. +target_compile_features(cmrc-base INTERFACE cxx_nullptr) +set_property(TARGET cmrc-base PROPERTY INTERFACE_CXX_EXTENSIONS OFF) +add_library(cmrc::base ALIAS cmrc-base) + +function(cmrc_add_resource_library name) + set(args ALIAS NAMESPACE) + cmake_parse_arguments(ARG "" "${args}" "" "${ARGN}") + # Generate the identifier for the resource library's namespace + set(ns_re "[a-zA-Z_][a-zA-Z0-9_]*") + if(NOT DEFINED ARG_NAMESPACE) + # Check that the library name is also a valid namespace + if(NOT name MATCHES "${ns_re}") + message(SEND_ERROR "Library name is not a valid namespace. Specify the NAMESPACE argument") + endif() + set(ARG_NAMESPACE "${name}") + else() + if(NOT ARG_NAMESPACE MATCHES "${ns_re}") + message(SEND_ERROR "NAMESPACE for ${name} is not a valid C++ namespace identifier (${ARG_NAMESPACE})") + endif() + endif() + set(libname "${name}") + # Generate a library with the compiled in character arrays. + string(CONFIGURE [=[ + #include + #include + #include + + namespace cmrc { + namespace @ARG_NAMESPACE@ { + + namespace res_chars { + // These are the files which are available in this resource library + $, + > + } + + namespace { + + const cmrc::detail::index_type& + get_root_index() { + static cmrc::detail::directory root_directory_; + static cmrc::detail::file_or_directory root_directory_fod{root_directory_}; + static cmrc::detail::index_type root_index; + root_index.emplace("", &root_directory_fod); + struct dir_inl { + class cmrc::detail::directory& directory; + }; + dir_inl root_directory_dir{root_directory_}; + (void)root_directory_dir; + $, + > + $, + > + return root_index; + } + + } + + cmrc::embedded_filesystem get_filesystem() { + static auto& index = get_root_index(); + return cmrc::embedded_filesystem{index}; + } + + } // @ARG_NAMESPACE@ + } // cmrc + ]=] cpp_content @ONLY) + get_filename_component(libdir "${CMAKE_CURRENT_BINARY_DIR}/__cmrc_${name}" ABSOLUTE) + get_filename_component(lib_tmp_cpp "${libdir}/lib_.cpp" ABSOLUTE) + string(REPLACE "\n " "\n" cpp_content "${cpp_content}") + file(GENERATE OUTPUT "${lib_tmp_cpp}" CONTENT "${cpp_content}") + get_filename_component(libcpp "${libdir}/lib.cpp" ABSOLUTE) + add_custom_command(OUTPUT "${libcpp}" + DEPENDS "${lib_tmp_cpp}" "${cmrc_hpp}" + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${lib_tmp_cpp}" "${libcpp}" + COMMENT "Generating ${name} resource loader" + ) + # Generate the actual static library. Each source file is just a single file + # with a character array compiled in containing the contents of the + # corresponding resource file. + add_library(${name} STATIC ${libcpp}) + set_property(TARGET ${name} PROPERTY CMRC_LIBDIR "${libdir}") + set_property(TARGET ${name} PROPERTY CMRC_NAMESPACE "${ARG_NAMESPACE}") + target_link_libraries(${name} PUBLIC cmrc::base) + set_property(TARGET ${name} PROPERTY CMRC_IS_RESOURCE_LIBRARY TRUE) + if(ARG_ALIAS) + add_library("${ARG_ALIAS}" ALIAS ${name}) + endif() + cmrc_add_resources(${name} ${ARG_UNPARSED_ARGUMENTS}) +endfunction() + +function(_cmrc_register_dirs name dirpath) + if(dirpath STREQUAL "") + return() + endif() + # Skip this dir if we have already registered it + get_target_property(registered "${name}" _CMRC_REGISTERED_DIRS) + if(dirpath IN_LIST registered) + return() + endif() + # Register the parent directory first + get_filename_component(parent "${dirpath}" DIRECTORY) + if(NOT parent STREQUAL "") + _cmrc_register_dirs("${name}" "${parent}") + endif() + # Now generate the registration + set_property(TARGET "${name}" APPEND PROPERTY _CMRC_REGISTERED_DIRS "${dirpath}") + _cm_encode_fpath(sym "${dirpath}") + if(parent STREQUAL "") + set(parent_sym root_directory) + else() + _cm_encode_fpath(parent_sym "${parent}") + endif() + get_filename_component(leaf "${dirpath}" NAME) + set_property( + TARGET "${name}" + APPEND PROPERTY CMRC_MAKE_DIRS + "static auto ${sym}_dir = ${parent_sym}_dir.directory.add_subdir(\"${leaf}\")\;" + "root_index.emplace(\"${dirpath}\", &${sym}_dir.index_entry)\;" + ) +endfunction() + +function(cmrc_add_resources name) + get_target_property(is_reslib ${name} CMRC_IS_RESOURCE_LIBRARY) + if(NOT TARGET ${name} OR NOT is_reslib) + message(SEND_ERROR "cmrc_add_resources called on target '${name}' which is not an existing resource library") + return() + endif() + + set(options) + set(args WHENCE PREFIX) + set(list_args) + cmake_parse_arguments(ARG "${options}" "${args}" "${list_args}" "${ARGN}") + + if(NOT ARG_WHENCE) + set(ARG_WHENCE ${CMAKE_CURRENT_SOURCE_DIR}) + endif() + _cmrc_normalize_path(ARG_WHENCE) + get_filename_component(ARG_WHENCE "${ARG_WHENCE}" ABSOLUTE) + + # Generate the identifier for the resource library's namespace + get_target_property(lib_ns "${name}" CMRC_NAMESPACE) + + get_target_property(libdir ${name} CMRC_LIBDIR) + get_target_property(target_dir ${name} SOURCE_DIR) + file(RELATIVE_PATH reldir "${target_dir}" "${CMAKE_CURRENT_SOURCE_DIR}") + if(reldir MATCHES "^\\.\\.") + message(SEND_ERROR "Cannot call cmrc_add_resources in a parent directory from the resource library target") + return() + endif() + + foreach(input IN LISTS ARG_UNPARSED_ARGUMENTS) + _cmrc_normalize_path(input) + get_filename_component(abs_in "${input}" ABSOLUTE) + # Generate a filename based on the input filename that we can put in + # the intermediate directory. + file(RELATIVE_PATH relpath "${ARG_WHENCE}" "${abs_in}") + if(relpath MATCHES "^\\.\\.") + # For now we just error on files that exist outside of the soure dir. + message(SEND_ERROR "Cannot add file '${input}': File must be in a subdirectory of ${ARG_WHENCE}") + continue() + endif() + if(DEFINED ARG_PREFIX) + _cmrc_normalize_path(ARG_PREFIX) + endif() + if(ARG_PREFIX AND NOT ARG_PREFIX MATCHES "/$") + set(ARG_PREFIX "${ARG_PREFIX}/") + endif() + get_filename_component(dirpath "${ARG_PREFIX}${relpath}" DIRECTORY) + _cmrc_register_dirs("${name}" "${dirpath}") + get_filename_component(abs_out "${libdir}/intermediate/${relpath}.cpp" ABSOLUTE) + # Generate a symbol name relpath the file's character array + _cm_encode_fpath(sym "${relpath}") + # Get the symbol name for the parent directory + if(dirpath STREQUAL "") + set(parent_sym root_directory) + else() + _cm_encode_fpath(parent_sym "${dirpath}") + endif() + # Generate the rule for the intermediate source file + _cmrc_generate_intermediate_cpp(${lib_ns} ${sym} "${abs_out}" "${abs_in}") + target_sources(${name} PRIVATE "${abs_out}") + set_property(TARGET ${name} APPEND PROPERTY CMRC_EXTERN_DECLS + "// Pointers to ${input}" + "extern const char* const ${sym}_begin\;" + "extern const char* const ${sym}_end\;" + ) + get_filename_component(leaf "${relpath}" NAME) + set_property( + TARGET ${name} + APPEND PROPERTY CMRC_MAKE_FILES + "root_index.emplace(" + " \"${ARG_PREFIX}${relpath}\"," + " ${parent_sym}_dir.directory.add_file(" + " \"${leaf}\"," + " res_chars::${sym}_begin," + " res_chars::${sym}_end" + " )" + ")\;" + ) + endforeach() +endfunction() + +function(_cmrc_generate_intermediate_cpp lib_ns symbol outfile infile) + add_custom_command( + # This is the file we will generate + OUTPUT "${outfile}" + # These are the primary files that affect the output + DEPENDS "${infile}" "${_CMRC_SCRIPT}" + COMMAND + "${CMAKE_COMMAND}" + -D_CMRC_GENERATE_MODE=TRUE + -DNAMESPACE=${lib_ns} + -DSYMBOL=${symbol} + "-DINPUT_FILE=${infile}" + "-DOUTPUT_FILE=${outfile}" + -P "${_CMRC_SCRIPT}" + COMMENT "Generating intermediate file for ${infile}" + ) +endfunction() + +function(_cm_encode_fpath var fpath) + string(MAKE_C_IDENTIFIER "${fpath}" ident) + string(MD5 hash "${fpath}") + string(SUBSTRING "${hash}" 0 4 hash) + set(${var} f_${hash}_${ident} PARENT_SCOPE) +endfunction() diff --git a/cmake/cmakerc/LICENSE.txt b/cmake/cmakerc/LICENSE.txt new file mode 100644 index 0000000..2d02f19 --- /dev/null +++ b/cmake/cmakerc/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 vector-of-bool + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/data/CMakeLists.txt b/data/CMakeLists.txt index e69de29..4bd49e3 100644 --- a/data/CMakeLists.txt +++ b/data/CMakeLists.txt @@ -0,0 +1 @@ +cmrc_add_resource_library(snoreretoastsources 256-256-snoretoast.png NAMESPACE SnoreToastResource) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a8f50d4..9c5419f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,46 +1,37 @@ -if (BUILD_STATIC_RUNTIME) - #link runtime static - if(MSVC) - foreach(_bt DEBUG RELEASE RELWITHDEBINFO) - string(REPLACE "/MD" "/MT" CMAKE_CXX_FLAGS_${_bt} ${CMAKE_CXX_FLAGS_${_bt}}) - endforeach(_bt DEBUG RELEASE RELWITHDEBINFO) - endif(MSVC) -endif() - add_library(SnoreToastActions INTERFACE) target_include_directories(SnoreToastActions INTERFACE $ $ ) add_library(SnoreToast::SnoreToastActions ALIAS SnoreToastActions) add_library(libsnoretoast STATIC snoretoasts.cpp toasteventhandler.cpp linkhelper.cpp utils.cpp) target_link_libraries(libsnoretoast PUBLIC runtimeobject shlwapi SnoreToast::SnoreToastActions) target_compile_definitions(libsnoretoast PRIVATE UNICODE _UNICODE __WRL_CLASSIC_COM_STRICT__ WIN32_LEAN_AND_MEAN NOMINMAX) target_compile_definitions(libsnoretoast PRIVATE SNORETOAST_VERSION_MAJOR=${PROJECT_VERSION_MAJOR} SNORETOAST_VERSION_MINOR=${PROJECT_VERSION_MINOR} SNORETOAST_VERSION_PATCH=${PROJECT_VERSION_PATCH} ) target_compile_definitions(libsnoretoast PUBLIC __WRL_CLASSIC_COM_STRICT__) target_include_directories(libsnoretoast PUBLIC $) set_target_properties(libsnoretoast PROPERTIES EXPORT_NAME LibSnoreToast) add_library(SnoreToast::LibSnoreToast ALIAS libsnoretoast) generate_export_header(libsnoretoast) create_icon_rc(${PROJECT_SOURCE_DIR}/data/zzz.ico TOAST_ICON) add_executable(SnoreToast WIN32 main.cpp ${TOAST_ICON}) -target_link_libraries(SnoreToast PRIVATE SnoreToast::LibSnoreToast) +target_link_libraries(SnoreToast PRIVATE SnoreToast::LibSnoreToast snoreretoastsources) target_compile_definitions(SnoreToast PRIVATE UNICODE _UNICODE WIN32_LEAN_AND_MEAN NOMINMAX) # if there are changes to the callback mechanism we need to change the uuid for the activator SNORETOAST_CALLBACK_UUID target_compile_definitions(SnoreToast PRIVATE SNORETOAST_CALLBACK_UUID="{383803B6-AFDA-4220-BFC3-0DBF810106BF}" ) add_executable(SnoreToast::SnoreToast ALIAS SnoreToast) install(TARGETS SnoreToast SnoreToastActions EXPORT LibSnoreToastConfig RUNTIME DESTINATION bin LIBRARY DESTINATION lib ARCHIVE DESTINATION lib) install(FILES snoretoastactions.h DESTINATION include/snoretoast) install(EXPORT LibSnoreToastConfig DESTINATION lib/cmake/libsnoretoast NAMESPACE SnoreToast::) diff --git a/src/main.cpp b/src/main.cpp index e331367..4dbb926 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,319 +1,332 @@ /* SnoreToast is capable to invoke Windows 8 toast notifications. Copyright (C) 2013-2019 Hannah von Reth SnoreToast is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. SnoreToast is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with SnoreToast. If not, see . */ #include "snoretoasts.h" #include "toasteventhandler.h" #include "snoretoastactioncenterintegration.h" #include "linkhelper.h" +#include + #include #include -#include -#include -#include #include #include +#include +#include +#include +#include #include -using namespace Windows::Foundation; +CMRC_DECLARE(SnoreToastResource); void help(const std::wstring &error) { if (!error.empty()) { std::wcerr << error << std::endl; } else { std::wcerr << L"Welcome to SnoreToast " << SnoreToasts::version() << "." << std::endl << L"A command line application capable of creating Windows Toast notifications." << std::endl; } std::wcerr << std::endl << L"---- Usage ----" << std::endl << L"SnoreToast [Options]" << std::endl << std::endl << L"---- Options ----" << std::endl << L"[-t] \t| Displayed on the first line of the toast." << std::endl << L"[-m] <message string>\t| Displayed on the remaining lines, wrapped." << std::endl << L"[-b] <button1;button2 string>| Displayed on the bottom line, can list multiple " L"buttons separated by ;" << std::endl << L"[-tb]\t\t\t| Displayed a textbox on the bottom line, only if buttons are not " L"presented." << std::endl << L"[-p] <image URI>\t| Display toast with an image, local files only." << std::endl << L"[-id] <id>\t\t| sets the id for a notification to be able to close it later." << std::endl << L"[-s] <sound URI> \t| Sets the sound of the notifications, for possible values see " L"http://msdn.microsoft.com/en-us/library/windows/apps/hh761492.aspx." << std::endl << L"[-silent] \t\t| Don't play a sound file when showing the notifications." << std::endl << L"[-appID] <App.ID>\t| Don't create a shortcut but use the provided app id." << std::endl << L"[-pipeName] <\\.\\pipe\\pipeName\\>\t| Provide a name pipe which is used for " L"callbacks." << std::endl << L"[-application] <C:\\foo.exe>\t| Provide a application that might be started if " L"the pipe does not exist." << std::endl << L"-close <id>\t\t| Closes a currently displayed notification." << std::endl << std::endl << L"-install <name> <application> <appID>| Creates a shortcut <name> in the start " L"menu which point to the executable <application>, appID used for the " L"notifications." << std::endl << std::endl << L"-v \t\t\t| Print the version and copying information." << std::endl << L"-h\t\t\t| Print these instructions. Same as no args." << std::endl << L"Exit Status\t: Exit Code" << std::endl << L"Failed\t\t: " << static_cast<int>(SnoreToastActions::Actions::Error) << std::endl << std::endl << "Success\t\t: " << static_cast<int>(SnoreToastActions::Actions::Clicked) << std::endl << "Hidden\t\t: " << static_cast<int>(SnoreToastActions::Actions::Hidden) << std::endl << "Dismissed\t: " << static_cast<int>(SnoreToastActions::Actions::Dismissed) << std::endl << "TimedOut\t: " << static_cast<int>(SnoreToastActions::Actions::Timedout) << std::endl << "ButtonPressed\t: " << static_cast<int>(SnoreToastActions::Actions::ButtonClicked) << std::endl << "TextEntered\t: " << static_cast<int>(SnoreToastActions::Actions::TextEntered) << std::endl << std::endl << L"---- Image Notes ----" << std::endl << L"Images must be .png with:" << std::endl << L"\tmaximum dimensions of 1024x1024" << std::endl << L"\tsize <= 200kb" << std::endl << L"These limitations are due to the Toast notification system." << std::endl; } void version() { std::wcerr << L"SnoreToast version " << SnoreToasts::version() << std::endl << L"Copyright (C) 2019 Hannah von Reth <vonreth@kde.org>" << std::endl << L"SnoreToast is free software: you can redistribute it and/or modify" << std::endl << L"it under the terms of the GNU Lesser General Public License as published by" << std::endl << L"the Free Software Foundation, either version 3 of the License, or" << std::endl << L"(at your option) any later version." << std::endl; } SnoreToastActions::Actions parse(std::vector<wchar_t *> args) { HRESULT hr = S_OK; std::wstring appID; std::filesystem::path pipe; std::filesystem::path application; std::wstring title; std::wstring body; std::filesystem::path image; std::wstring id; std::wstring sound(L"Notification.Default"); std::wstring buttons; bool silent = false; bool closeNotify = false; bool isTextBoxEnabled = false; auto nextArg = [&](std::vector<wchar_t *>::const_iterator &it, const std::wstring &helpText) -> std::wstring { if (it != args.cend()) { return *it++; } else { help(helpText); exit(static_cast<int>(SnoreToastActions::Actions::Error)); } }; auto it = args.begin() + 1; while (it != args.end()) { std::wstring arg(nextArg(it, L"")); std::transform(arg.begin(), arg.end(), arg.begin(), [](int i) -> int { return ::tolower(i); }); if (arg == L"-m") { body = nextArg(it, L"Missing argument to -m.\n" L"Supply argument as -m \"message string\""); } else if (arg == L"-t") { title = nextArg(it, L"Missing argument to -t.\n" L"Supply argument as -t \"bold title string\""); } else if (arg == L"-p") { std::wstring path = nextArg(it, L"Missing argument to -p." L"Supply argument as -p \"image path\""); if (path.substr(0, 8) != L"file:///") { image = L"file:///"; path = _wfullpath(nullptr, path.c_str(), MAX_PATH); } image.append(path); } else if (arg == L"-s") { sound = nextArg(it, L"Missing argument to -s.\n" L"Supply argument as -s \"sound name\""); } else if (arg == L"-id") { id = nextArg(it, L"Missing argument to -id.\n" L"Supply argument as -id \"id\""); } else if (arg == L"-silent") { silent = true; } else if (arg == L"-appid") { appID = nextArg(it, L"Missing argument to -appID.\n" L"Supply argument as -appID \"Your.APP.ID\""); } else if (arg == L"-pipename") { pipe = nextArg(it, L"Missing argument to -pipeName.\n" L"Supply argument as -pipeName \"\\.\\pipe\\foo\\\""); } else if (arg == L"-application") { application = nextArg(it, L"Missing argument to -pipeName.\n" L"Supply argument as -applicatzion \"C:\\foo.exe\""); } else if (arg == L"-b") { buttons = nextArg(it, L"Missing argument to -b.\n" L"Supply argument for buttons as -b \"button1;button2\""); } else if (arg == L"-tb") { isTextBoxEnabled = true; } else if (arg == L"-install") { const std::wstring shortcut( nextArg(it, L"Missing argument to -install.\n" L"Supply argument as -install \"path to your shortcut\" \"path to the " L"application the shortcut should point to\" \"App.ID\"")); const std::wstring exe( nextArg(it, L"Missing argument to -install.\n" L"Supply argument as -install \"path to your shortcut\" \"path to the " L"application the shortcut should point to\" \"App.ID\"")); appID = nextArg(it, L"Missing argument to -install.\n" L"Supply argument as -install \"path to your shortcut\" \"path to the " L"application the shortcut should point to\" \"App.ID\""); return SUCCEEDED(LinkHelper::tryCreateShortcut( shortcut, exe, appID, SnoreToastActionCenterIntegration::uuid())) ? SnoreToastActions::Actions::Clicked : SnoreToastActions::Actions::Error; } else if (arg == L"-close") { id = nextArg(it, L"Missing agument to -close" L"Supply argument as -close \"id\""); closeNotify = true; } else if (arg == L"-v") { version(); return SnoreToastActions::Actions::Clicked; } else if (arg == L"-h") { help(L""); return SnoreToastActions::Actions::Clicked; } else { std::wstringstream ws; ws << L"Unknown argument: " << arg << std::endl; help(ws.str()); return SnoreToastActions::Actions::Error; } } if (appID.empty()) { std::wstringstream _appID; _appID << L"Snore.DesktopToasts." << SnoreToasts::version(); appID = _appID.str(); hr = LinkHelper::tryCreateShortcut(std::filesystem::path(L"SnoreToast") / SnoreToasts::version() / L"SnoreToast", appID, SnoreToastActionCenterIntegration::uuid()); if (!SUCCEEDED(hr)) { return SnoreToastActions::Actions::Error; } } if (closeNotify) { if (!id.empty()) { SnoreToasts app(appID); app.setId(id); if (app.closeNotification()) { return SnoreToastActions::Actions::Clicked; } } else { help(L"Close only works if an -id id was provided."); } } else { hr = (title.length() > 0 && body.length() > 0) ? S_OK : E_FAIL; if (SUCCEEDED(hr)) { if (isTextBoxEnabled) { if (pipe.empty()) { std::wcerr << L"TextBox notifications only work if a pipe for the result " L"was provided" << std::endl; return SnoreToastActions::Actions::Error; } } + if (image.empty()) { + image = std::filesystem::temp_directory_path() / "snoretoast" + / SnoreToasts::version() / "logo.png"; + std::filesystem::create_directories(image.parent_path()); + const auto filesystem = cmrc::SnoreToastResource::get_filesystem(); + const auto img = filesystem.open("256-256-snoretoast.png"); + std::ofstream out(image, std::ios::binary); + out.write(const_cast<char *>(img.begin()), img.size()); + out.close(); + } SnoreToasts app(appID); app.setPipeName(pipe); app.setApplication(application); app.setSilent(silent); app.setSound(sound); app.setId(id); app.setButtons(buttons); app.setTextBoxEnabled(isTextBoxEnabled); app.displayToast(title, body, image); return app.userAction(); } else { help(L""); return SnoreToastActions::Actions::Clicked; } } return SnoreToastActions::Actions::Error; } SnoreToastActions::Actions handleEmbedded() { SnoreToasts::waitForCallbackActivation(); return SnoreToastActions::Actions::Clicked; } int WINAPI wWinMain(HINSTANCE, HINSTANCE, wchar_t *, int) { if (AttachConsole(ATTACH_PARENT_PROCESS)) { FILE *dummy; _wfreopen_s(&dummy, L"CONOUT$", L"w", stdout); setvbuf(stdout, nullptr, _IONBF, 0); _wfreopen_s(&dummy, L"CONOUT$", L"w", stderr); setvbuf(stderr, nullptr, _IONBF, 0); std::ios::sync_with_stdio(); } const auto commandLine = GetCommandLineW(); int argc; wchar_t **argv = CommandLineToArgvW(commandLine, &argc); SnoreToastActions::Actions action = SnoreToastActions::Actions::Clicked; - HRESULT hr = Initialize(RO_INIT_MULTITHREADED); + HRESULT hr = Windows::Foundation::Initialize(RO_INIT_MULTITHREADED); if (SUCCEEDED(hr)) { if (std::wstring(commandLine).find(L"-Embedding") != std::wstring::npos) { action = handleEmbedded(); } else { action = parse(std::vector<wchar_t *>(argv, argv + argc)); } - Uninitialize(); + Windows::Foundation::Uninitialize(); } return static_cast<int>(action); }