diff --git a/.github/template/template_name b/.github/template/template_name new file mode 100644 index 00000000..9f9b489e --- /dev/null +++ b/.github/template/template_name @@ -0,0 +1 @@ +cpp_boilerplate_project diff --git a/.github/template/template_repository b/.github/template/template_repository new file mode 100644 index 00000000..622fb5a7 --- /dev/null +++ b/.github/template/template_repository @@ -0,0 +1 @@ +cpp-best-practices/cpp_boilerplate_project diff --git a/.github/workflows/template-janitor.yml b/.github/workflows/template-janitor.yml index 20f5bb83..b64b0630 100644 --- a/.github/workflows/template-janitor.yml +++ b/.github/workflows/template-janitor.yml @@ -1,11 +1,16 @@ # This workflow should cleanup everything unneeded from the template project name: Template Janitor + on: + pull_request: + release: + types: [published] push: + tags: branches: - - develop - - main # maybe all to run "tests" on every change? + - main + - develop env: TEMPLATES_PATH: ".github/template" @@ -23,7 +28,7 @@ jobs: run: | echo "NEW_ORG=${{ github.repository_owner }}" >> $GITHUB_ENV echo "NEW_PROJECT=${{ github.event.repository.name }}" >> $GITHUB_ENV - echo "NEW_URL=${{ github.repositoryUrl}}" >> $GITHUB_ENV + echo "NEW_URL=${{ github.repositoryUrl }}" >> $GITHUB_ENV - uses: octokit/request-action@v2.x id: get_repo_meta @@ -41,17 +46,17 @@ jobs: sed -i "s/myproject/${{ github.event.repository.name }}/gi" CMakeLists.txt configured_files/config.hpp.in # Update URL placeholders for project - sed -i "s/%%myurl%%/${{ github.event.repositoryUrl }}/gi" CMakeLists.txt + sed -i "s|%%myurl%%|${{ fromJson(steps.get_repo_meta.outputs.data).html_url }}|gi" CMakeLists.txt # fill in placeholders of readme and move it into place sed -i "s/%%myorg%%/${{ env.NEW_ORG }}/g" ${{ env.TEMPLATES_PATH }}/README.md sed -i "s/%%myproject%%/${{ env.NEW_PROJECT }}/g" ${{ env.TEMPLATES_PATH }}/README.md - sed -i "s/%%description%%/${{ fromJson(steps.get_repo_meta.outputs.data).description }}/g" ${{ env.TEMPLATES_PATH }}/README.md + sed -i "s|%%description%%|${{ fromJson(steps.get_repo_meta.outputs.data).description }}|g" ${{ env.TEMPLATES_PATH }}/README.md cp ${{ env.TEMPLATES_PATH }}/README.md README.md - name: Print diff after replacement run: | - # Exclude the README as that is checked seperatly! + # Exclude the README as that is checked separately! git diff ':!README.md' # following should not have any diffs diff ${{ env.TEMPLATES_PATH }}/README.md README.md @@ -63,11 +68,35 @@ jobs: - name: Clean up before commit and push run: | - rm -rf ${{ env.TEMPLATES_PATH }} + rm -r ${{ env.TEMPLATES_PATH }} - # Can we get that from a variabl? + # Can we get that from a variable? # Remove this workflow as it has fulfilled its purpose - rm -rf .github/workflows/template-janitor.yml + rm .github/workflows/template-janitor.yml + rm .github/workflows/template-renamer.yml + + - name: Setup Cpp + uses: aminya/setup-cpp@v1 + with: + compiler: gcc + + cmake: true + ninja: false + conan: true + vcpkg: false + ccache: false + clangtidy: false + + cppcheck: false + + gcovr: false + opencppcoverage: false + + + - name: Test simple configuration to make sure nothing broke (default compiler,cmake,developer_mode OFF) + run: | + cmake -S . -B ./build -DCMAKE_BUILD_TYPE:STRING=DEBUG -DENABLE_DEVELOPER_MODE:BOOL=OFF -DOPT_ENABLE_COVERAGE:BOOL=OFF + - uses: EndBug/add-and-commit@v4 # only commit and push if we are not a template project anymore! diff --git a/.github/workflows/template-renamer.yml b/.github/workflows/template-renamer.yml new file mode 100644 index 00000000..fce3b6fe --- /dev/null +++ b/.github/workflows/template-renamer.yml @@ -0,0 +1,94 @@ +# This workflow should cleanup everything unneeded from the template project + +name: Template Renamer + +on: + pull_request: + release: + types: [published] + push: + tags: + branches: + - main + - develop + +env: + TEMPLATES_PATH: ".github/template" + +jobs: + + template-rename: + name: Renames template when a new name is detected + runs-on: ubuntu-latest + steps: + - name: Fetch Sources + uses: actions/checkout@v2.4.0 + + - name: Get organization and project name + run: | + echo "TEST_RUN=false" >> $GITHUB_ENV + echo "NEW_ORG=${{ github.repository_owner }}" >> $GITHUB_ENV + echo "NEW_PROJECT=${{ github.event.repository.name }}" >> $GITHUB_ENV + echo "NEW_REPOSITORY=${{ github.repository }}" >> $GITHUB_ENV + echo "TEMPLATE_NAME=`cat ${{ env.TEMPLATES_PATH }}/template_name`" >> $GITHUB_ENV + echo "TEMPLATE_REPOSITORY=`cat ${{ env.TEMPLATES_PATH }}/template_repository`" >> $GITHUB_ENV + + - uses: octokit/request-action@v2.x + id: get_repo_meta + with: + route: GET /repos/{owner}/{repo} + owner: ${{ env.NEW_ORG }} + repo: ${{ env.NEW_PROJECT }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup fake test org/project names if project didn't change + if: env.TEMPLATE_NAME == env.NEW_PROJECT + run: | + echo "TEST_RUN=true" >> $GITHUB_ENV + echo "NEW_ORG=${{ github.repository_owner }}" >> $GITHUB_ENV + echo "NEW_PROJECT=TEST_PROJECT" >> $GITHUB_ENV + echo "NEW_REPOSITORY=TEST_REPOSITORY" >> $GITHUB_ENV + + + # Rename all cpp_starter_project occurrences to current repository and remove this workflow + - name: Update repository to match new template information + run: | + # Update the README and template files to match the new org / repository names + sed -i "s|${{ env.TEMPLATE_REPOSITORY }}|${{ env.NEW_REPOSITORY }}|g" README.md ${{ env.TEMPLATES_PATH }}/template_repository + sed -i "s|${{ env.TEMPLATE_NAME }}|${{ env.NEW_PROJECT }}|g" README.md ${{ env.TEMPLATES_PATH }}/template_name + + - name: Print diff after template name replacement + run: | + git diff + + - name: Setup Cpp + uses: aminya/setup-cpp@v1 + with: + compiler: gcc + + cmake: true + ninja: false + conan: true + vcpkg: false + ccache: false + clangtidy: false + + cppcheck: false + + gcovr: false + opencppcoverage: false + + - name: Test simple configuration to make sure nothing broke (default compiler,cmake,developer_mode OFF) + run: | + cmake -S . -B ./build -DCMAKE_BUILD_TYPE:STRING=DEBUG -DENABLE_DEVELOPER_MODE:BOOL=OFF -DOPT_ENABLE_COVERAGE:BOOL=OFF + + - uses: EndBug/add-and-commit@v4 + # only commit and push if we are a template and project name has changed + if: fromJson(steps.get_repo_meta.outputs.data).is_template == true && env.TEST_RUN == 'false' + with: + author_name: Template Janitor + author_email: template.janitor@example.com + message: 'Change Template Name' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/CMakeLists.txt b/CMakeLists.txt index 2600ad09..84ee0ec5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,6 +9,22 @@ set(CMAKE_CXX_STANDARD 20) # when compiling with PCH enabled set(CMAKE_CXX_EXTENSIONS OFF) +include(FetchContent) + +set(FETCHCONTENT_UPDATES_DISCONNECTED TRUE) +FetchContent_Declare(ftxui + GIT_REPOSITORY https://github.com/ArthurSonzogni/ftxui + GIT_TAG v2.0.0 +) + +FetchContent_GetProperties(ftxui) +if(NOT ftxui_POPULATED) + FetchContent_Populate(ftxui) + add_subdirectory(${ftxui_SOURCE_DIR} ${ftxui_BINARY_DIR} EXCLUDE_FROM_ALL) +endif() + + + # Note: by default ENABLE_DEVELOPER_MODE is True # This means that all analysis (sanitizers, static analysis) # is enabled and all warnings are treated as errors @@ -22,7 +38,6 @@ set(OPT_WARNINGS_AS_ERRORS_DEVELOPER_DEFAULT TRUE) # Add project_options v0.17.0 # https://github.com/cpp-best-practices/project_options -include(FetchContent) FetchContent_Declare(_project_options URL https://github.com/cpp-best-practices/project_options/archive/refs/tags/v0.17.0.zip) FetchContent_MakeAvailable(_project_options) @@ -37,7 +52,7 @@ project( myproject VERSION 0.0.1 DESCRIPTION "" - HOMEPAGE_URL "%%url%%" + HOMEPAGE_URL "%%myurl%%" LANGUAGES CXX C) set(GIT_SHA diff --git a/README.md b/README.md index 8f666d88..8e4e5593 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![ci](https://github.com/cpp-best-practices/cpp_boilerplate_project/actions/workflows/ci.yml/badge.svg)](https://github.com/cpp-best-practices/cpp_boilerplate_project/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/cpp-best-practices/cpp_boilerplate_project/branch/main/graph/badge.svg)](https://codecov.io/gh/cpp-best-practices/cpp_boilerplate_project) -[![Language grade: C++](https://img.shields.io/lgtm/grade/cpp/github/cpp-best-practices/cpp_boilerplate_project)](https://lgtm.com/projects/g/cpp-best-practices/cpp_starter_project/context:cpp) +[![Language grade: C++](https://img.shields.io/lgtm/grade/cpp/github/cpp-best-practices/cpp_boilerplate_project)](https://lgtm.com/projects/g/cpp-best-practices/cpp_boilerplate_project/context:cpp) [![CodeQL](https://github.com/cpp-best-practices/cpp_boilerplate_project/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/cpp-best-practices/cpp_boilerplate_project/actions/workflows/codeql-analysis.yml) ## About cpp_boilerplate_project @@ -28,9 +28,9 @@ It requires * conan * a compiler -If you want a more complex example project, check out the [cpp_starter_project](https://github.com/cpp-best-practices/cpp_starter_project). -Ths Boilerplate project will merge new features first, then they will be merged (as appropriate) into cpp_starter_project. +This project gets you started with a simple example of using FTXUI, which happens to also be a game + ## Getting Started diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 23208c22..9f7d9a3c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -12,5 +12,11 @@ target_link_libraries( fmt::fmt spdlog::spdlog) -target_include_directories(intro PRIVATE "${CMAKE_BINARY_DIR}/configured_files/include") +target_link_system_libraries( + intro + PRIVATE + ftxui::screen + ftxui::dom + ftxui::component) +target_include_directories(intro PRIVATE "${CMAKE_BINARY_DIR}/configured_files/include") diff --git a/src/main.cpp b/src/main.cpp index de9b1564..57e5df34 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,32 +1,104 @@ #include #include +#include #include +#include // for ftxui +#include // for Slider +#include // for ScreenInteractive #include -// This file will be generated automatically when you run the CMake configuration step. -// It creates a namespace called `myproject`. -// You can modify the source template at `configured_files/config.hpp.in`. +// This file will be generated automatically when you run the CMake +// configuration step. It creates a namespace called `myproject`. You can modify +// the source template at `configured_files/config.hpp.in`. #include static constexpr auto USAGE = - R"(Naval Fate. + R"(intro Usage: - naval_fate ship new ... - naval_fate ship move [--speed=] - naval_fate ship shoot - naval_fate mine (set|remove) [--moored | --drifting] - naval_fate (-h | --help) - naval_fate --version + intro + intro (-h | --help) + intro --version Options: -h --help Show this screen. --version Show version. - --speed= Speed in knots [default: 10]. - --moored Moored (anchored) mine. - --drifting Drifting mine. )"; + +template struct GameBoard +{ + static constexpr std::size_t width = Width; + static constexpr std::size_t height = Height; + + std::array, width> strings; + std::array, width> values{}; + + std::size_t move_count{0}; + + std::string &get_string(std::size_t x, std::size_t y) { + return strings.at(x).at(y); + } + + + void set(std::size_t x, std::size_t y, bool new_value) + { + get(x,y) = new_value; + + if (new_value) { + get_string(x,y) = " ON"; + } else { + get_string(x,y) = "OFF"; + } + } + + void visit(auto visitor) + { + for (std::size_t x = 0; x < width; ++x) { + for (std::size_t y = 0; y < height; ++y) { visitor(x, y, *this); } + } + } + + [[nodiscard]] bool get(std::size_t x, std::size_t y) const { return values.at(x).at(y); } + + [[nodiscard]] bool &get(std::size_t x, std::size_t y) { return values.at(x).at(y); } + + GameBoard() + { + visit([](const auto x, const auto y, auto &gameboard) { gameboard.set(x, y, true); }); + } + + void update_strings() + { + for (std::size_t x = 0; x < width; ++x) { + for (std::size_t y = 0; y < height; ++y) { set(x, y, get(x, y)); } + } + } + + void toggle(std::size_t x, std::size_t y) { set(x, y, !get(x, y)); } + + void press(std::size_t x, std::size_t y) + { + ++move_count; + toggle(x, y); + if (x > 0) { toggle(x - 1, y); } + if (y > 0) { toggle(x, y - 1); } + if (x < width - 1) { toggle(x + 1, y); } + if (y < height - 1) { toggle(x, y + 1); } + } + + [[nodiscard]] bool solved() const + { + for (std::size_t x = 0; x < width; ++x) { + for (std::size_t y = 0; y < height; ++y) { + if (!get(x, y)) { return false; } + } + } + + return true; + } +}; + int main(int argc, const char **argv) { try { @@ -35,15 +107,77 @@ int main(int argc, const char **argv) true,// show help if requested fmt::format("{} {}", myproject::cmake::project_name, - myproject::cmake::project_version));// version string, acquired from config.hpp via CMake + myproject::cmake::project_version));// version string, acquired + // from config.hpp via CMake + + auto screen = ftxui::ScreenInteractive::TerminalOutput(); + + GameBoard<3, 3> gb; + + std::string quit_text; + + const auto update_quit_text = [&quit_text](const auto &game_board) { + quit_text = fmt::format("Quit ({} moves)", game_board.move_count); + if (game_board.solved()) { quit_text += " Solved!"; } + }; + + const auto make_buttons = [&] { + std::vector buttons; + for (std::size_t x = 0; x < gb.width; ++x) { + for (std::size_t y = 0; y < gb.height; ++y) { + buttons.push_back(ftxui::Button(&gb.get_string(x,y), [=, &gb] { + if (!gb.solved()) { gb.press(x, y); } + update_quit_text(gb); + })); + } + } + return buttons; + }; + + auto buttons = make_buttons(); + + auto quit_button = ftxui::Button(&quit_text, screen.ExitLoopClosure()); + + auto make_layout = [&] { + std::vector rows; + + std::size_t idx = 0; + + for (std::size_t x = 0; x < gb.width; ++x) { + std::vector row; + for (std::size_t y = 0; y < gb.height; ++y) { + row.push_back(buttons[idx]->Render()); + ++idx; + } + rows.push_back(ftxui::hbox(std::move(row))); + } + + rows.push_back(ftxui::hbox({ quit_button->Render() })); + + return ftxui::vbox(std::move(rows)); + }; + + + static constexpr int randomization_iterations = 100; + static constexpr int random_seed = 42; + + std::mt19937 gen32{random_seed}; // NOLINT + std::uniform_int_distribution x(static_cast(0), gb.width - 1); + std::uniform_int_distribution y(static_cast(0), gb.height - 1); + + for (int i = 0; i < randomization_iterations; ++i) { gb.press(x(gen32), y(gen32)); } + gb.move_count = 0; + update_quit_text(gb); + + auto all_buttons = buttons; + all_buttons.push_back(quit_button); + auto container = ftxui::Container::Horizontal(all_buttons); - for (auto const &arg : args) { std::cout << arg.first << "=" << arg.second << '\n'; } + auto renderer = ftxui::Renderer(container, make_layout); + screen.Loop(renderer); - // Use the default logger (stdout, multi-threaded, colored) - spdlog::info("Hello, {}!", "World"); - fmt::print("Hello, from {}\n", "{fmt}"); } catch (const std::exception &e) { fmt::print("Unhandled exception in main: {}", e.what()); }