diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..ef13133 --- /dev/null +++ b/.clang-format @@ -0,0 +1,67 @@ +# Generated from CLion C/C++ Code Style settings +BasedOnStyle: LLVM +Language: Cpp +AccessModifierOffset: -4 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: Consecutive +AlignOperands: Align +AllowAllArgumentsOnNextLine: false +AllowAllConstructorInitializersOnNextLine: false +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: Always +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: All +AllowShortIfStatementsOnASingleLine: Always +AllowShortLambdasOnASingleLine: All +AllowShortLoopsOnASingleLine: true +AlwaysBreakAfterReturnType: None +AlwaysBreakTemplateDeclarations: Yes +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterControlStatement: Never + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterUnion: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: true +BreakBeforeBinaryOperators: None +BreakBeforeTernaryOperators: true +BreakConstructorInitializers: BeforeColon +BreakInheritanceList: BeforeColon +ColumnLimit: 0 +CompactNamespaces: false +ContinuationIndentWidth: 8 +IndentCaseLabels: true +IndentPPDirectives: None +IndentWidth: 4 +KeepEmptyLinesAtTheStartOfBlocks: true +MaxEmptyLinesToKeep: 2 +NamespaceIndentation: All +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PointerAlignment: Right +ReflowComments: false +SpaceAfterCStyleCast: true +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 0 +SpacesInAngles: false +SpacesInCStyleCastParentheses: false +SpacesInContainerLiterals: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +TabWidth: 4 +UseTab: Never diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..8ae450e --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,81 @@ +--- +Checks: 'clang-diagnostic-*,clang-analyzer-*,bugprone-*,concurrency-*,cppcoreguidelines-*,modernize-*,performance-*,readability-*,-bugprone-easily-swappable-parameters,-cppcoreguidelines-owning-memory,-cppcoreguidelines-special-member-functions' +WarningsAsErrors: true +HeaderFilterRegex: '' +AnalyzeTemporaryDtors: false +FormatStyle: file +CheckOptions: + - key: llvm-else-after-return.WarnOnConditionVariables + value: 'false' + - key: modernize-loop-convert.MinConfidence + value: reasonable + - key: modernize-replace-auto-ptr.IncludeStyle + value: llvm + - key: cert-str34-c.DiagnoseSignedUnsignedCharComparisons + value: 'false' + - key: google-readability-namespace-comments.ShortNamespaceLines + value: '10' + - key: cert-err33-c.CheckedFunctions + value: '::aligned_alloc;::asctime_s;::at_quick_exit;::atexit;::bsearch;::bsearch_s;::btowc;::c16rtomb;::c32rtomb;::calloc;::clock;::cnd_broadcast;::cnd_init;::cnd_signal;::cnd_timedwait;::cnd_wait;::ctime_s;::fclose;::fflush;::fgetc;::fgetpos;::fgets;::fgetwc;::fopen;::fopen_s;::fprintf;::fprintf_s;::fputc;::fputs;::fputwc;::fputws;::fread;::freopen;::freopen_s;::fscanf;::fscanf_s;::fseek;::fsetpos;::ftell;::fwprintf;::fwprintf_s;::fwrite;::fwscanf;::fwscanf_s;::getc;::getchar;::getenv;::getenv_s;::gets_s;::getwc;::getwchar;::gmtime;::gmtime_s;::localtime;::localtime_s;::malloc;::mbrtoc16;::mbrtoc32;::mbsrtowcs;::mbsrtowcs_s;::mbstowcs;::mbstowcs_s;::memchr;::mktime;::mtx_init;::mtx_lock;::mtx_timedlock;::mtx_trylock;::mtx_unlock;::printf_s;::putc;::putwc;::raise;::realloc;::remove;::rename;::scanf;::scanf_s;::setlocale;::setvbuf;::signal;::snprintf;::snprintf_s;::sprintf;::sprintf_s;::sscanf;::sscanf_s;::strchr;::strerror_s;::strftime;::strpbrk;::strrchr;::strstr;::strtod;::strtof;::strtoimax;::strtok;::strtok_s;::strtol;::strtold;::strtoll;::strtoul;::strtoull;::strtoumax;::strxfrm;::swprintf;::swprintf_s;::swscanf;::swscanf_s;::thrd_create;::thrd_detach;::thrd_join;::thrd_sleep;::time;::timespec_get;::tmpfile;::tmpfile_s;::tmpnam;::tmpnam_s;::tss_create;::tss_get;::tss_set;::ungetc;::ungetwc;::vfprintf;::vfprintf_s;::vfscanf;::vfscanf_s;::vfwprintf;::vfwprintf_s;::vfwscanf;::vfwscanf_s;::vprintf_s;::vscanf;::vscanf_s;::vsnprintf;::vsnprintf_s;::vsprintf;::vsprintf_s;::vsscanf;::vsscanf_s;::vswprintf;::vswprintf_s;::vswscanf;::vswscanf_s;::vwprintf_s;::vwscanf;::vwscanf_s;::wcrtomb;::wcschr;::wcsftime;::wcspbrk;::wcsrchr;::wcsrtombs;::wcsrtombs_s;::wcsstr;::wcstod;::wcstof;::wcstoimax;::wcstok;::wcstok_s;::wcstol;::wcstold;::wcstoll;::wcstombs;::wcstombs_s;::wcstoul;::wcstoull;::wcstoumax;::wcsxfrm;::wctob;::wctrans;::wctype;::wmemchr;::wprintf_s;::wscanf;::wscanf_s;' + - key: cert-oop54-cpp.WarnOnlyIfThisHasSuspiciousField + value: 'false' + - key: cert-dcl16-c.NewSuffixes + value: 'L;LL;LU;LLU' + - key: google-readability-braces-around-statements.ShortStatementLines + value: '1' + - key: cppcoreguidelines-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic + value: 'true' + - key: google-readability-namespace-comments.SpacesBeforeComments + value: '2' + - key: modernize-loop-convert.MaxCopySize + value: '16' + - key: modernize-pass-by-value.IncludeStyle + value: llvm + - key: modernize-use-nullptr.NullMacros + value: 'NULL' + - key: llvm-qualified-auto.AddConstToQualified + value: 'false' + - key: modernize-loop-convert.NamingStyle + value: CamelCase + - key: llvm-else-after-return.WarnOnUnfixable + value: 'false' + - key: google-readability-function-size.StatementThreshold + value: '800' + - key: readability-identifier-naming.AbstractClassCase + value: CamelCase + - key: readability-identifier-naming.AggressiveDependentMemberLookup + value: true + - key: readability-identifier-naming.ClassCase + value: CamelCase + - key: readability-identifier-naming.ClassConstantCase + value: UPPER_CASE + - key: readability-identifier-naming.ClassMemberCase + value: lower_case + - key: readability-identifier-naming.ClassMethodCase + value: camelBack + - key: readability-identifier-naming.ConstantCase + value: UPPER_CASE + - key: readability-identifier-naming.EnumCase + value: CamelCase + - key: readability-identifier-naming.EnumConstantCase + value: UPPER_CASE + - key: readability-identifier-naming.FunctionCase + value: camelBack + - key: readability-identifier-naming.LocalVariableCase + value: lower_case + - key: readability-identifier-naming.MemberCase + value: lower_case + - key: readability-identifier-naming.MemberPrefix + value: _ + - key: readability-identifier-naming.MethodCase + value: camelBack + - key: readability-identifier-naming.NamespaceCase + value: lower_case + - key: readability-identifier-naming.ParameterCase + value: lower_case + - key: readability-identifier-naming.StructCase + value: CamelCase + - key: readability-identifier-naming.VariableCase + value: lower_case +... + diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 544481b..79a7e32 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,42 +4,28 @@ on: push: paths: - "include/**" + - "src/**" - "tests/**" - ".github/workflows/test.yml" pull_request: paths: - "include/**" + - "src/**" - "tests/**" - ".github/workflows/test.yml" jobs: - test-ubuntu: - runs-on: ubuntu-latest + unit-tests: + strategy: + fail-fast: false + matrix: + os: [ ubuntu-latest, macos-latest ] + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 - name: Configure CMake - run: cmake -B ${{github.workspace }}/build -DCMAKE_BUILD_TYPE=Release + run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTING=ON - name: Build - run: | - cd build - make test + run: cmake --build ${{github.workspace}}/build --target unit-tests - name: Run tests - run: | - cd build - ./test - - test-macos: - runs-on: macos-latest - steps: - - uses: actions/checkout@v2 - - name: Configure CMake - run: cmake -B ${{github.workspace }}/build -DCMAKE_BUILD_TYPE=Release - - name: Build - run: | - cd build - make test - - name: Run tests - run: | - cd build - ./test - + run: cd ${{github.workspace}}/build && ctest --output-on-failure diff --git a/.gitignore b/.gitignore index 4afcf19..a3300e9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .idea -build \ No newline at end of file +build +cmake-build-release \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index f09c920..05716e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## v1.0.2 + +- Update CMakeLists.txt to make it usable with FetchContent +- Update source code with modern c++ + ## v1.0.1 *2023-07-14* diff --git a/CMakeLists.txt b/CMakeLists.txt index 81facf0..f6cb54c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,15 +1,47 @@ -cmake_minimum_required(VERSION 3.10) -project(Enquirer) +cmake_minimum_required(VERSION 3.22 FATAL_ERROR) +project( + enquirer + VERSION 1.0.2 + LANGUAGES CXX +) -set(CMAKE_CXX_STANDARD 14) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +# Don't use e.g. GNU extension (like -std=gnu++11) for portability +set(CMAKE_CXX_EXTENSIONS OFF) -add_executable(demo - tests/demo.cpp - include/enquirer/enquirer.hpp) -target_include_directories(demo PUBLIC include/enquirer) +add_library(${PROJECT_NAME}) +add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) +target_sources(${PROJECT_NAME} + PRIVATE + src/enquirer.cpp +) +target_include_directories(${PROJECT_NAME} + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src + PUBLIC + $ + $ +) -add_executable(test - tests/test.cpp - include/enquirer/enquirer.hpp - tests/test.hpp) -target_include_directories(test PUBLIC include/enquirer) \ No newline at end of file +set(public_headers + include/enquirer.h +) +set_target_properties(${PROJECT_NAME} PROPERTIES PUBLIC_HEADER "${public_headers}") + +set_target_properties(${PROJECT_NAME} PROPERTIES DEBUG_POSTFIX "d") + +option(BUILD_TESTING "Build the testing tree." OFF) +if (BUILD_TESTING AND (PROJECT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR)) + target_compile_options(${PROJECT_NAME} PUBLIC + -Wall + -O0 + -g + ) + enable_testing() + add_subdirectory(tests) +else() + target_compile_options(${PROJECT_NAME} PUBLIC + -O3 + ) +endif () diff --git a/include/enquirer.h b/include/enquirer.h new file mode 100644 index 0000000..facc229 --- /dev/null +++ b/include/enquirer.h @@ -0,0 +1,319 @@ +/** + * MIT License + * + * Copyright (c) 2023 Kevin Traini + * + * 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. + */ +#ifndef ENQUIRER_HPP +#define ENQUIRER_HPP + +#define ENQUIRER_VERSION "1.0.2" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace enquirer { + + // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. + // Utilities + + namespace color { + const std::string black = "\033[30m"; + const std::string red = "\033[31m"; + const std::string green = "\033[32m"; + const std::string yellow = "\033[33m"; + const std::string blue = "\033[34m"; + const std::string magenta = "\033[35m"; + const std::string cyan = "\033[36m"; + const std::string white = "\033[37m"; + const std::string grey = "\033[90m"; + + const std::string reset = "\033[0m"; + const std::string bold = "\033[1m"; + const std::string underline = "\033[4m"; + const std::string blink = "\033[5m"; + const std::string inverse = "\033[7m"; + }// namespace color + + namespace utils { + auto move_up(unsigned int n = 1) -> std::string; + + auto move_left(unsigned int n = 1) -> std::string; + + typedef enum { + EOL = 0, + BOL = 1, + LINE = 2 + } clear_mode; + + auto clear_line(clear_mode mode) -> std::string; + + auto hide_cursor() -> std::string; + + auto show_cursor() -> std::string; + + auto print_question(const std::string &question, + const std::string &symbol = color::cyan + color::bold + "? ", + const std::string &input = color::grey + color::bold + "› ") -> void; + + auto print_answer(const std::string &question) -> void; + + auto enable_raw_mode() -> void; + + auto disable_raw_mode() -> void; + }// namespace utils + + // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. + // Auth + + auto auth(const std::string &id_prompt = "Username", + const std::string &pw_prompt = "Password", + char mask = '*') -> std::pair; + + auto auth(const std::function &)> &predicate, + const std::string &id_prompt = "Username", + const std::string &pw_prompt = "Password", + char mask = '*') -> bool; + + // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. + // Autocomplete + + auto autocomplete(const std::string &question, + const std::vector &choices = {}, + unsigned int limit = 10) -> std::string; + + // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. + // Confirm + + auto confirm(const std::string &question, + bool default_value = false) -> bool; + + // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. + // Form + + auto form(const std::string &question, + const std::vector &inputs) -> std::map; + + // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. + // Input + + auto input(const std::string &question, + const std::string &default_value = "") -> std::string; + + // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. + // Invisible + + auto invisible(const std::string &question) -> std::string; + + // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. + // List + + auto list(const std::string &question) -> std::vector; + + // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. + // MultiSelect + + auto multi_select(const std::string &question, + const std::vector &choices) -> std::vector; + + // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. + // Number + + template::value>::type> + auto number(const std::string &question) -> N { + // Print question + utils::print_question(question); + + // Get answer + std::string answer; + char current; + utils::enable_raw_mode(); + while (std::cin.get(current)) { + if (iscntrl(current)) { + if (current == 10) {// Enter + std::cout << std::endl; + break; + } else if (current == 127) {// Backspace + if (!answer.empty()) { + answer.pop_back(); + std::cout << utils::move_left(1); + std::cout << utils::clear_line(utils::EOL); + } + } else if (current == 27) {// Escape + std::cin.get(current); + if (current == 91) { + std::cin.get(current); + // Ignore arrow keys + } + } + } else if (isdigit(current) || + (current == '.' && answer.find('.') == std::string::npos) || + ((current == '+' || current == '-') && answer.empty())) {// 'Normal' character + answer += current; + std::cout << current; + } + } + utils::disable_raw_mode(); + + // Print resume + std::cout << utils::move_up() + << utils::move_left(1000); + utils::print_answer(question); + std::cout << color::cyan << answer << color::reset << std::endl; + + // Convert answer to number type N + std::stringstream ss(answer); + N number; + ss >> number; + + return number; + } + + // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. + // Password + + auto password(const std::string &question, + char mask = '*') -> std::string; + + // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. + // Quiz + + auto quiz(const std::string &question, + const std::vector &choices, + const std::string &correct) -> bool; + + // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. + // Slider + + template::value>::type> + auto slider(const std::string &question, + N min_value, + N max_value, + N step, + N initial_value) -> N { + // Print question + utils::print_question(question); + + struct winsize w {}; + ioctl(STDOUT_FILENO, TIOCGWINSZ, &w); + const unsigned int width = std::min((unsigned int) ((max_value - min_value) / step), + (unsigned int) w.ws_col - 7);// 7 is for < > # and 2 spaces each side + const N swidth = (max_value - min_value) / ((N) width); + + // Print value + N value = initial_value; + std::cout << std::endl + << " " + << std::string((width / 2) - (std::to_string(value).length() / 2), ' ') + << color::bold << value << color::reset + << std::endl; + + // Print slider + std::cout << " " << color::cyan << color::bold << "<" << color::reset; + for (N i = 0; i <= (N) width; i++) { + N l = min_value + (i * swidth); + N r = min_value + ((i + 1) * swidth); + if (value >= l && value < r) { + std::cout << color::cyan << color::bold << "#" << color::reset; + } else { + std::cout << color::grey << "-" << color::reset; + } + } + std::cout << color::cyan << color::bold << ">" << color::reset; + + // Get answer + char current; + utils::enable_raw_mode(); + std::cout << utils::hide_cursor(); + while (std::cin.get(current)) { + if (iscntrl(current)) { + if (current == 10) {// Enter + break; + } else if (current == 27) {// Escape + std::cin.get(current); + if (current == 91) { + std::cin.get(current); + if (current == 67) {// Right + value = std::min(value + step, max_value); + } else if (current == 68) {// Left + value = std::max(value - step, min_value); + } + } + } + } + + // Redraw slider + std::cout << utils::clear_line(utils::LINE) + << utils::move_up() << utils::clear_line(utils::LINE) + << utils::move_left(1000); + std::cout << " " + << std::string((width / 2) - (std::to_string(value).length() / 2), ' ') + << color::bold << value << color::reset << std::endl; + std::cout << " " + << color::cyan << color::bold << "<" << color::reset; + for (unsigned int i = 0; i <= width; i++) { + N l = min_value + (i * swidth); + N r = min_value + ((i + 1) * swidth); + if (value >= l && value < r) { + std::cout << color::cyan << color::bold << "#" << color::reset; + } else { + std::cout << color::grey << "-" << color::reset; + } + } + std::cout << color::cyan << color::bold << ">" << color::reset; + } + std::cout << utils::show_cursor(); + utils::disable_raw_mode(); + + // Print resume + std::cout << utils::clear_line(utils::LINE) + << utils::move_up() << utils::clear_line(utils::LINE) + << utils::move_up() << utils::clear_line(utils::LINE) + << utils::move_left(1000); + utils::print_answer(question); + std::cout << color::cyan << value << color::reset << std::endl; + + return value; + } + + // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. + // Select + + auto select(const std::string &question, + const std::vector &choices) -> std::string; + + // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. + // Toggle + + auto toggle(const std::string &question, + const std::string &enable, + const std::string &disable, + bool default_value = false) -> bool; +}// namespace enquirer + +#endif//ENQUIRER_HPP diff --git a/include/enquirer/enquirer.hpp b/include/enquirer/enquirer.hpp deleted file mode 100644 index 7b61b4d..0000000 --- a/include/enquirer/enquirer.hpp +++ /dev/null @@ -1,1219 +0,0 @@ -/** - * MIT License - * - * Copyright (c) 2023 Kevin Traini - * - * 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. - */ -#ifndef ENQUIRER_HPP -#define ENQUIRER_HPP - -#define ENQUIRER_VERSION "1.0.1" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace enquirer { - - // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. - // Utilities - - namespace color { - const std::string black = "\033[30m"; - const std::string red = "\033[31m"; - const std::string green = "\033[32m"; - const std::string yellow = "\033[33m"; - const std::string blue = "\033[34m"; - const std::string magenta = "\033[35m"; - const std::string cyan = "\033[36m"; - const std::string white = "\033[37m"; - const std::string grey = "\033[90m"; - - const std::string reset = "\033[0m"; - const std::string bold = "\033[1m"; - const std::string underline = "\033[4m"; - const std::string blink = "\033[5m"; - const std::string inverse = "\033[7m"; - } - - namespace utils { - /** - * Move the cursor n times upward - */ - inline std::string move_up(uint n = 1) { - return "\033[" + std::to_string(n) + "A"; - } - - /** - * Move the cursor n times downward - */ - inline std::string move_down(uint n = 1) { - return "\033[" + std::to_string(n) + "B"; - } - - /** - * Move the cursor n times to the left - */ - inline std::string move_left(uint n = 1) { - return "\033[" + std::to_string(n) + "D"; - } - - /** - * Move the cursor n times to the right - */ - inline std::string move_right(uint n = 1) { - return "\033[" + std::to_string(n) + "C"; - } - - typedef enum { - EOL = 0, - BOL = 1, - LINE = 2 - } clear_mode; - - /** - * Clear entire line - */ - inline std::string clear_line(clear_mode mode) { - return "\033[" + std::to_string(mode) + "K"; - } - - /** - * Hide the cursor - */ - inline std::string hide_cursor() { - return "\033[?25l"; - } - - /** - * Show the cursor - */ - inline std::string show_cursor() { - return "\033[?25h"; - } - - /** - * Clear line then print the question - */ - inline void print_question(const std::string &question, - const std::string &symbol = color::cyan + color::bold + "? ", - const std::string &input = color::grey + color::bold + "› ") { - std::cout << clear_line(LINE); - std::cout << symbol - << color::reset << question - << " " << input - << color::reset; - } - - /** - * Print the question as it's answered - */ - inline void print_answer(const std::string &question) { - print_question(question, color::green + color::bold + "✔ ", color::grey + color::bold + "· "); - } - - inline void enable_raw_mode() { - struct termios term{}; - tcgetattr(STDIN_FILENO, &term); - term.c_lflag &= ~(ECHO | ICANON); - tcsetattr(STDIN_FILENO, TCSAFLUSH, &term); - } - - inline void disable_raw_mode() { - struct termios term{}; - tcgetattr(STDIN_FILENO, &term); - term.c_lflag |= (ECHO | ICANON); - tcsetattr(STDIN_FILENO, TCSAFLUSH, &term); - } - - inline std::string ltrim(const std::string &str) { - size_t start = str.find_first_not_of(' '); - return (start == std::string::npos) ? "" : str.substr(start); - } - - inline std::string rtrim(const std::string &str) { - size_t end = str.find_last_not_of(' '); - return (end == std::string::npos) ? "" : str.substr(0, end + 1); - } - - /** - * Trim whitespace from both sides of a string - */ - inline std::string trim(const std::string &str) { - return rtrim(ltrim(str)); - } - - /** - * Split a string around a delimiter - */ - inline std::vector split(const std::string &str, const char delim) { - std::vector result; - std::stringstream ss(str); - std::string item; - while (std::getline(ss, item, delim)) { - result.push_back(trim(item)); - } - - return result; - } - - /** - * Return src filtered by the predicate - */ - inline std::vector filter(const std::vector &src, - const std::function &predicate) { - std::vector result; - - for (const auto &item: src) { - if (predicate(item)) { - result.push_back(item); - } - } - - return result; - } - - /** - * Check if src begin with prefix - */ - inline bool begin_with(const std::string &src, const std::string &prefix) { - return src.find(prefix) == 0; - } - - /** - * Complete src with fill until it reaches the length - */ - inline std::string lfill(const std::string &src, const size_t width, const char fill = ' ') { - if (src.length() >= width) { - return src; - } - - return std::string(width - src.length(), fill) + src; - } - - /** - * Returns the size of longest string in the vector - */ - inline uint max_size(const std::vector &strs) { - uint max = 0; - for (const auto &str: strs) - if (str.length() > max) - max = str.length(); - - return max; - } - } - - // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. - // Auth - - std::pair auth(const std::string &id_prompt = "Username", - const std::string &pw_prompt = "Password", - char mask = '*') { - // Print inputs - std::vector inputs = {id_prompt, pw_prompt}; - uint width = std::max(id_prompt.length(), pw_prompt.length()); - uint line = 0; - std::pair answers = std::make_pair("", ""); - utils::print_question(color::cyan + utils::lfill(id_prompt, width), color::grey + "⊙ "); - std::cout << std::endl; - utils::print_question(utils::lfill(pw_prompt, width), color::grey + "⊙ "); - std::cout << std::endl; - std::cout << utils::move_up(2) << utils::move_right(width + 5); - - // Get answers - char current; - utils::enable_raw_mode(); - while (std::cin.get(current)) { - uint previous = line; - if (iscntrl(current)) { - if (current == 10) { // Enter - if (!answers.first.empty() && !answers.second.empty()) { - break; - } else { - line = answers.first.empty() ? 0 : 1; - } - } else if (current == 127) { // Backspace - if (line == 0 && !answers.first.empty()) - answers.first.pop_back(); - else if (line == 1 && !answers.second.empty()) - answers.second.pop_back(); - - } else if (current == 27) { // Escape - std::cin.get(current); - if (current == 91) { - std::cin.get(current); - if (current == 65) { // Up - line = (line == 0) ? 1 : 0; - } else if (current == 66) { // Down - line = (line == 1) ? 0 : 1; - } - } - } - } else { // 'Normal' character - if (line == 0) - answers.first += current; - else - answers.second += current; - - } - - // Redraw inputs - std::cout << utils::move_left(1000) << (previous == 0 ? "" : utils::move_up()); - std::cout << utils::clear_line(utils::EOL); - if (line == 0) { - utils::print_question(color::cyan + utils::lfill(id_prompt, width), (answers.first.empty() ? - color::grey + "⊙ " : - color::green + "⦿ ")); - std::cout << answers.first << std::endl; - utils::print_question(utils::lfill(pw_prompt, width), (answers.second.empty() ? - color::grey + "⊙ " : - color::green + "⦿ ")); - std::cout << std::string(answers.second.length(), '*') << std::endl; - } else { - utils::print_question(utils::lfill(id_prompt, width), (answers.first.empty() ? - color::grey + "⊙ " : - color::green + "⦿ ")); - std::cout << answers.first << std::endl; - utils::print_question(color::cyan + utils::lfill(pw_prompt, width), (answers.second.empty() ? - color::grey + "⊙ " : - color::green + "⦿ ")); - std::cout << std::string(answers.second.length(), '*') << std::endl; - } - std::cout << utils::move_up(inputs.size() - line) - << utils::move_right(width + 5 + (line == 0 ? - answers.first.length() : - answers.second.length())); - } - - // Print resume - std::cout << utils::move_left(1000) << (line == 0 ? "" : utils::move_up()); - std::cout << utils::clear_line(utils::EOL); - utils::print_question(utils::lfill(id_prompt, width), (answers.first.empty() ? - color::grey + "⊙ " : - color::green + "⦿ ")); - std::cout << answers.first << std::endl; - utils::print_question(utils::lfill(pw_prompt, width), (answers.second.empty() ? - color::grey + "⊙ " : - color::green + "⦿ ")); - std::cout << std::string(answers.second.length(), '*') << std::endl; - - return {answers.first, answers.second}; - } - - bool auth(const std::function &)> &predicate, - const std::string &id_prompt = "Username", - const std::string &pw_prompt = "Password", - char mask = '*') { - return predicate(auth(id_prompt, pw_prompt, mask)); - } - - // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. - // Autocomplete - - std::string autocomplete(const std::string &question, - const std::vector &choices = {}, - uint limit = 10) { - // Print question - utils::print_question(question); - - std::vector current_choices; - int choice = -1; - - // Get answer - std::string answer; - char current; - utils::enable_raw_mode(); - while (std::cin.get(current)) { - if (iscntrl(current)) { - if (current == 10) { // Enter - break; - } else if (current == 127) { // Backspace - if (!answer.empty()) { - answer.pop_back(); - } - } else if (current == 9 && choice != -1) { // Tab - if (answer == current_choices[choice].substr(0, answer.length())) { - answer = std::string(current_choices[choice]); - } - } else if (current == 27) { // Escape - std::cin.get(current); - if (current == 91) { - std::cin.get(current); - if (current == 65) { // Up - choice = (choice == 0) ? (int) current_choices.size() - 1 : choice - 1; - } else if (current == 66) { // Down - choice = (choice == std::min(limit, (uint) current_choices.size()) - 1) ? 0 : choice + 1; - } - } - } - } else { // 'Normal' character - answer += current; - } - - // Erase previous choices - std::cout << utils::move_down(std::min(limit, (uint) current_choices.size()) + 1) - << utils::move_left(1000); - for (uint i = 0; i < current_choices.size() && i < limit; i++) { - std::cout << utils::clear_line(utils::EOL) - << utils::move_up(); - } - std::cout << utils::move_up(); - // Draw completion - current_choices = utils::filter(choices, [=](const std::string &item) { - return utils::begin_with(item, answer); - }); - choice = std::max(0, std::min(choice, (int) current_choices.size() - 1)); - std::cout << utils::move_left(1000) << utils::clear_line(utils::LINE); - utils::print_question(question); - std::cout << answer; - if (!current_choices.empty()) { - std::cout << color::grey << current_choices[choice].substr(answer.length()) << color::reset; - } - std::cout << std::endl; - for (uint i = 0; i < current_choices.size() && i < limit; i++) { - std::cout << utils::clear_line(utils::EOL); - if (i == choice) { - std::cout << color::cyan << color::underline << current_choices[i] << color::reset << std::endl; - } else { - std::cout << current_choices[i] << std::endl; - } - } - std::cout << utils::move_up(std::min(limit, (uint) current_choices.size()) + 1) - << utils::move_left(1000) << utils::move_right(question.length() + answer.length() + 5); - } - utils::disable_raw_mode(); - - // Print resume - std::cout << utils::move_down(std::min(limit, (uint) current_choices.size()) + 1) - << utils::move_left(1000); - for (uint i = 0; i < current_choices.size() && i < limit; i++) { - std::cout << utils::clear_line(utils::EOL) - << utils::move_up(); - } - std::cout << utils::move_up() - << utils::move_left(1000) << utils::clear_line(utils::LINE); - utils::print_answer(question); - std::cout << color::cyan << answer << color::reset << std::endl; - - return answer; - } - - // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. - // Confirm - - bool confirm(const std::string &question, - bool default_value = false) { - // Print question - utils::print_question(question); - - // Print choices - bool confirmed = default_value; - std::cout << (confirmed ? "Yes" : "No"); - - // Get answer - char current; - utils::enable_raw_mode(); - std::cout << utils::hide_cursor(); - while (std::cin.get(current)) { - bool previous = confirmed; - if (iscntrl(current)) { - if (current == 10) { // Enter - break; - } else if (current == 27) { // Escape - std::cin.get(current); - if (current == 91) { - std::cin.get(current); - if (current == 68) { // Left - confirmed = true; - } else if (current == 67) { // Right - confirmed = false; - } - } - } - } - - // Redraw choices - std::cout << utils::move_left(previous ? 3 : 2) - << utils::clear_line(utils::EOL) - << (confirmed ? "Yes" : "No"); - } - std::cout << utils::show_cursor(); - utils::disable_raw_mode(); - - // Print resume - std::cout << utils::move_left(1000); - utils::print_answer(question); - std::cout << (confirmed ? color::green : color::red) << (confirmed ? "Yes" : "No") << color::reset << std::endl; - - return confirmed; - } - - // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. - // Form - - std::map form(const std::string &question, - const std::vector &inputs) { - if (inputs.empty()) { - return {}; - } - - // Print question - utils::print_question(question); - std::cout << std::endl; - - // Print inputs - uint width = utils::max_size(inputs); - uint line = 0; - std::map answers; - for (uint i = 0; i < inputs.size(); i++) { - answers[inputs[i]] = ""; - if (i == line) { - utils::print_question(color::cyan + utils::lfill(inputs[i], width), color::grey + "⊙ "); - } else { - utils::print_question(utils::lfill(inputs[i], width), color::grey + "⊙ "); - } - std::cout << std::endl; - } - std::cout << utils::move_up(inputs.size()) << utils::move_right(width + 5); - - // Get answers - char current; - utils::enable_raw_mode(); - while (std::cin.get(current)) { - uint previous = line; - if (iscntrl(current)) { - if (current == 10) { // Enter - if (std::all_of(answers.begin(), answers.end(), - [](const std::pair &item) { - return !item.second.empty(); - })) { - break; - } else { - line = std::distance(answers.begin(), - std::find_if(answers.begin(), answers.end(), - [](const std::pair &item) { - return item.second.empty(); - })); - } - } else if (current == 127) { // Backspace - if (!answers[inputs[line]].empty()) { - answers[inputs[line]].pop_back(); - } - } else if (current == 27) { // Escape - std::cin.get(current); - if (current == 91) { - std::cin.get(current); - if (current == 65) { // Up - line = (line == 0) ? inputs.size() - 1 : line - 1; - } else if (current == 66) { // Down - line = (line == inputs.size() - 1) ? 0 : line + 1; - } - } - } - } else { // 'Normal' character - answers[inputs[line]] += current; - } - - // Redraw inputs - std::cout << utils::move_left(1000) << (previous == 0 ? "" : utils::move_up(previous)); - for (uint i = 0; i < inputs.size(); i++) { - std::cout << utils::clear_line(utils::EOL); - std::string indicator = (answers[inputs[i]].empty() ? - color::grey + "⊙ " : - color::green + "⦿ "); - if (i == line) { - utils::print_question(color::cyan + utils::lfill(inputs[i], width), indicator); - } else { - utils::print_question(utils::lfill(inputs[i], width), indicator); - } - std::cout << answers[inputs[i]] << std::endl; - } - std::cout << utils::move_up(inputs.size() - line) - << utils::move_right(width + 5 + answers[inputs[line]].length()); - } - - // Print resume - std::cout << utils::move_left(1000) << (line == 0 ? "" : utils::move_up(line)) - << utils::move_up() - << utils::clear_line(utils::EOL); - utils::print_answer(question); - std::cout << std::endl; - for (const auto &input: inputs) { - std::cout << utils::clear_line(utils::EOL); - utils::print_question(utils::lfill(input, width), color::green + "⦿ "); - std::cout << answers[input] << std::endl; - } - - return answers; - } - - // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. - // Input - - std::string input(const std::string &question, - const std::string &default_value = "") { - // Print question - utils::print_question(question); - - // Print default value - std::cout << color::grey << default_value << color::reset; - std::cout << utils::move_left(default_value.size()); - - // Get answer - std::string answer; - char current; - utils::enable_raw_mode(); - while (std::cin.get(current)) { - if (iscntrl(current)) { - if (current == 10) { // Enter - std::cout << std::endl; - break; - } else if (current == 127) { // Backspace - if (!answer.empty()) { - answer.pop_back(); - std::cout << utils::move_left(1); - std::cout << utils::clear_line(utils::EOL); - } - } else if (current == 9) { // Tab - if (answer == default_value.substr(0, answer.length())) { - std::cout << default_value.substr(answer.length()); - answer = default_value; - } - } else if (current == 27) { // Escape - std::cin.get(current); - if (current == 91) { - std::cin.get(current); - // Ignore arrow keys - } - } - } else { // 'Normal' character - answer += current; - std::cout << current; - } - - // Check default_value - if (answer == default_value.substr(0, answer.length())) { - std::cout << color::grey << default_value.substr(answer.length()) << color::reset; - if (answer != default_value) { - std::cout << utils::move_left(default_value.size() - answer.size()); - } - } else if (answer != default_value) { - std::cout << utils::clear_line(utils::EOL); - } - } - utils::disable_raw_mode(); - - // Print resume - std::cout << utils::move_up() - << utils::move_left(1000); - utils::print_answer(question); - std::cout << color::cyan << answer << color::reset << std::endl; - - return answer; - } - - // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. - // Invisible - - std::string invisible(const std::string &question) { - // Print question - utils::print_question(question); - - // Get answer - std::string answer; - char current; - utils::enable_raw_mode(); - while (std::cin.get(current)) { - if (iscntrl(current)) { - if (current == 10) { // Enter - std::cout << std::endl; - break; - } else if (current == 127) { // Backspace - if (!answer.empty()) { - answer.pop_back(); - } - } else if (current == 27) { // Escape - std::cin.get(current); - if (current == 91) { - std::cin.get(current); - // Ignore arrow keys - } - } - } else { // 'Normal' character - answer += current; - } - } - utils::disable_raw_mode(); - - // Print resume - std::cout << utils::move_up() - << utils::move_left(1000); - utils::print_answer(question); - std::cout << std::endl; - - return answer; - } - - // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. - // List - - std::vector list(const std::string &question) { - // Print question - utils::print_question(question); - - // Get answer - std::string answer; - char current; - utils::enable_raw_mode(); - while (std::cin.get(current)) { - if (iscntrl(current)) { - if (current == 10) { // Enter - std::cout << std::endl; - break; - } else if (current == 127) { // Backspace - if (!answer.empty()) { - answer.pop_back(); - std::cout << utils::move_left(1); - std::cout << utils::clear_line(utils::EOL); - } - } else if (current == 27) { // Escape - std::cin.get(current); - if (current == 91) { - std::cin.get(current); - // Ignore arrow keys - } - } - } else { // 'Normal' character - answer += current; - std::cout << current; - } - } - utils::disable_raw_mode(); - - // Print resume - std::cout << utils::move_up() - << utils::move_left(1000); - utils::print_answer(question); - auto items = utils::split(answer, ','); - for (auto it = items.begin(); it != items.end(); it++) { - std::cout << color::cyan << *it << color::reset; - if (it + 1 != items.end()) { - std::cout << ", "; - } - } - std::cout << std::endl; - - return items; - } - - // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. - // MultiSelect - - std::vector multi_select(const std::string &question, - const std::vector &choices) { - // Print question - utils::print_question(question); - std::cout << std::endl; - - uint selected = 0; - bool choice[choices.size()]; - for (uint i = 0; i < choices.size(); i++) { - choice[i] = false; - } - - // Print choices - for (uint i = 0; i < choices.size(); i++) { - if (choice[i]) { - std::cout << color::bold << color::green << "✔ " << color::reset; - } else { - std::cout << color::grey << "✔ " << color::reset; - } - if (i == selected) { - std::cout << color::cyan << color::underline << choices[i] << color::reset << std::endl; - } else { - std::cout << choices[i] << std::endl; - } - } - - // Get answer - char current; - utils::enable_raw_mode(); - std::cout << utils::hide_cursor(); - while (std::cin.get(current)) { - if (iscntrl(current)) { - if (current == 10) { // Enter - break; - } else if (current == 27) { // Escape - std::cin.get(current); - if (current == 91) { - std::cin.get(current); - if (current == 65) { // Up - selected = (selected == 0) ? choices.size() - 1 : selected - 1; - } else if (current == 66) { // Down - selected = (selected == choices.size() - 1) ? 0 : selected + 1; - } else if (current == 67) { // Right - choice[selected] = true; - } else if (current == 68) { // Left - choice[selected] = false; - } - } - } - } - - // Redraw choices - std::cout << utils::move_up(choices.size()) - << utils::move_left(1000); - for (uint i = 0; i < choices.size(); i++) { - if (choice[i]) { - std::cout << color::bold << color::green << "✔ " << color::reset; - } else { - std::cout << color::grey << "✔ " << color::reset; - } - if (i == selected) { - std::cout << color::cyan << color::underline << choices[i] << color::reset << std::endl; - } else { - std::cout << choices[i] << std::endl; - } - } - } - std::cout << utils::show_cursor(); - utils::disable_raw_mode(); - - // Print resume - for (uint i = 0; i < choices.size(); i++) { // Clear choices - std::cout << utils::move_up() << utils::clear_line(utils::EOL); - } - std::cout << utils::move_up() - << utils::move_left(1000); - utils::print_answer(question); - std::vector items; - for (uint i = 0; i < choices.size(); i++) { - if (choice[i]) { - items.push_back(choices[i]); - } - } - for (auto it = items.begin(); it != items.end(); it++) { - std::cout << color::cyan << *it << color::reset; - if (it + 1 != items.end()) { - std::cout << ", "; - } - } - std::cout << std::endl; - - return items; - } - - // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. - // Number - - template::value>::type> - N number(const std::string &question) { - // Print question - utils::print_question(question); - - // Get answer - std::string answer; - char current; - utils::enable_raw_mode(); - while (std::cin.get(current)) { - if (iscntrl(current)) { - if (current == 10) { // Enter - std::cout << std::endl; - break; - } else if (current == 127) { // Backspace - if (!answer.empty()) { - answer.pop_back(); - std::cout << utils::move_left(1); - std::cout << utils::clear_line(utils::EOL); - } - } else if (current == 27) { // Escape - std::cin.get(current); - if (current == 91) { - std::cin.get(current); - // Ignore arrow keys - } - } - } else if (std::isdigit(current) || - (current == '.' && answer.find('.') == std::string::npos) || - ((current == '+' || current == '-') && answer.empty())) { // 'Normal' character - answer += current; - std::cout << current; - } - } - utils::disable_raw_mode(); - - // Print resume - std::cout << utils::move_up() - << utils::move_left(1000); - utils::print_answer(question); - std::cout << color::cyan << answer << color::reset << std::endl; - - // Convert answer to number type N - std::stringstream ss(answer); - N number; - ss >> number; - - return number; - } - - // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. - // Password - - std::string password(const std::string &question, - char mask = '*') { - // Print question - utils::print_question(question); - - // Get answer - std::string answer; - char current; - utils::enable_raw_mode(); - while (std::cin.get(current)) { - if (iscntrl(current)) { - if (current == 10) { // Enter - std::cout << std::endl; - break; - } else if (current == 127) { // Backspace - if (!answer.empty()) { - answer.pop_back(); - std::cout << utils::move_left(1); - std::cout << utils::clear_line(utils::EOL); - } - } else if (current == 27) { // Escape - std::cin.get(current); - if (current == 91) { - std::cin.get(current); - // Ignore arrow keys - } - } - } else { // 'Normal' character - answer += current; - std::cout << mask; - } - } - utils::disable_raw_mode(); - - // Print resume - std::cout << utils::move_up() - << utils::move_left(1000); - utils::print_answer(question); - std::cout << color::cyan << std::string(answer.size(), mask) << color::reset << std::endl; - - return answer; - } - - // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. - // Quiz - - bool quiz(const std::string &question, - const std::vector &choices, - const std::string &correct) { - // Print question - utils::print_question(question); - std::cout << std::endl; - - uint choice = 0; - - // Print choices - for (uint i = 0; i < choices.size(); i++) { - if (i == choice) { - std::cout << color::cyan << color::bold << "> " << color::reset - << color::cyan << color::underline << choices[i] << color::reset << std::endl; - } else { - std::cout << " " << choices[i] << std::endl; - } - } - - // Get answer - char current; - utils::enable_raw_mode(); - std::cout << utils::hide_cursor(); - while (std::cin.get(current)) { - if (iscntrl(current)) { - if (current == 10) { // Enter - break; - } else if (current == 27) { // Escape - std::cin.get(current); - if (current == 91) { - std::cin.get(current); - if (current == 65) { // Up - choice = (choice == 0) ? choices.size() - 1 : choice - 1; - } else if (current == 66) { // Down - choice = (choice == choices.size() - 1) ? 0 : choice + 1; - } - } - } - } - - // Redraw choices - std::cout << utils::move_up(choices.size()) - << utils::move_left(1000); - for (uint i = 0; i < choices.size(); i++) { - if (i == choice) { - std::cout << color::cyan << color::bold << "> " << color::reset - << color::cyan << color::underline << choices[i] << color::reset << std::endl; - } else { - std::cout << " " << choices[i] << std::endl; - } - } - } - std::cout << utils::show_cursor(); - utils::disable_raw_mode(); - - // Print resume - for (uint i = 0; i < choices.size(); i++) { // Clear choices - std::cout << utils::move_up() << utils::clear_line(utils::EOL); - } - std::cout << utils::move_up() - << utils::move_left(1000); - utils::print_answer(question); - bool result = (choices[choice] == correct); - std::cout << (result ? color::green : color::red) << choices[choice] << color::reset << std::endl; - - return result; - } - - // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. - // Slider - - template::value>::type> - N slider(const std::string &question, - N min_value, - N max_value, - N step, - N initial_value) { - // Print question - utils::print_question(question); - - struct winsize w{}; - ioctl(STDOUT_FILENO, TIOCGWINSZ, &w); - const uint width = std::min((uint) ((max_value - min_value) / step), - (uint) w.ws_col - 7); // 7 is for < > # and 2 spaces each side - const N swidth = (max_value - min_value) / ((N) width); - - // Print value - N value = initial_value; - std::cout << std::endl << " " - << std::string((width / 2) - (std::to_string(value).length() / 2), ' ') - << color::bold << value << color::reset - << std::endl; - - // Print slider - std::cout << " " << color::cyan << color::bold << "<" << color::reset; - for (N i = 0; i <= width; i++) { - N l = min_value + (i * swidth); - N r = min_value + ((i + 1) * swidth); - if (value >= l && value < r) { - std::cout << color::cyan << color::bold << "#" << color::reset; - } else { - std::cout << color::grey << "-" << color::reset; - } - } - std::cout << color::cyan << color::bold << ">" << color::reset; - - // Get answer - char current; - utils::enable_raw_mode(); - std::cout << utils::hide_cursor(); - while (std::cin.get(current)) { - if (iscntrl(current)) { - if (current == 10) { // Enter - break; - } else if (current == 27) { // Escape - std::cin.get(current); - if (current == 91) { - std::cin.get(current); - if (current == 67) { // Right - value = std::min(value + step, max_value); - } else if (current == 68) { // Left - value = std::max(value - step, min_value); - } - } - } - } - - // Redraw slider - std::cout << utils::clear_line(utils::LINE) - << utils::move_up() << utils::clear_line(utils::LINE) - << utils::move_left(1000); - std::cout << " " - << std::string((width / 2) - (std::to_string(value).length() / 2), ' ') - << color::bold << value << color::reset << std::endl; - std::cout << " " - << color::cyan << color::bold << "<" << color::reset; - for (uint i = 0; i <= width; i++) { - N l = min_value + (i * swidth); - N r = min_value + ((i + 1) * swidth); - if (value >= l && value < r) { - std::cout << color::cyan << color::bold << "#" << color::reset; - } else { - std::cout << color::grey << "-" << color::reset; - } - } - std::cout << color::cyan << color::bold << ">" << color::reset; - } - std::cout << utils::show_cursor(); - utils::disable_raw_mode(); - - // Print resume - std::cout << utils::clear_line(utils::LINE) - << utils::move_up() << utils::clear_line(utils::LINE) - << utils::move_up() << utils::clear_line(utils::LINE) - << utils::move_left(1000); - utils::print_answer(question); - std::cout << color::cyan << value << color::reset << std::endl; - - return value; - } - - // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. - // Select - - std::string select(const std::string &question, - const std::vector &choices) { - // Print question - utils::print_question(question); - std::cout << std::endl; - - uint choice = 0; - - // Print choices - for (uint i = 0; i < choices.size(); i++) { - if (i == choice) { - std::cout << color::cyan << color::bold << "> " << color::reset - << color::cyan << color::underline << choices[i] << color::reset << std::endl; - } else { - std::cout << " " << choices[i] << std::endl; - } - } - - // Get answer - char current; - utils::enable_raw_mode(); - std::cout << utils::hide_cursor(); - while (std::cin.get(current)) { - if (iscntrl(current)) { - if (current == 10) { // Enter - break; - } else if (current == 27) { // Escape - std::cin.get(current); - if (current == 91) { - std::cin.get(current); - if (current == 65) { // Up - choice = (choice == 0) ? choices.size() - 1 : choice - 1; - } else if (current == 66) { // Down - choice = (choice == choices.size() - 1) ? 0 : choice + 1; - } - } - } - } - - // Redraw choices - std::cout << utils::move_up(choices.size()) - << utils::move_left(1000); - for (uint i = 0; i < choices.size(); i++) { - if (i == choice) { - std::cout << color::cyan << color::bold << "> " << color::reset - << color::cyan << color::underline << choices[i] << color::reset << std::endl; - } else { - std::cout << " " << choices[i] << std::endl; - } - } - } - std::cout << utils::show_cursor(); - utils::disable_raw_mode(); - - // Print resume - for (uint i = 0; i < choices.size(); i++) { // Clear choices - std::cout << utils::move_up() << utils::clear_line(utils::EOL); - } - std::cout << utils::move_up() - << utils::move_left(1000); - utils::print_answer(question); - std::cout << color::cyan << choices[choice] << color::reset << std::endl; - - return choices[choice]; - } - - // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. - // Toggle - - bool toggle(const std::string &question, - const std::string &enable, - const std::string &disable, - bool default_value = false) { - // Print question - utils::print_question(question); - - // Print choices - bool toggled = default_value; - std::cout << (toggled ? color::cyan + color::underline : "") << enable << color::reset << "/" - << (toggled ? "" : color::cyan + color::underline) << disable << color::reset; - - // Get answer - char current; - utils::enable_raw_mode(); - std::cout << utils::hide_cursor(); - while (std::cin.get(current)) { - if (iscntrl(current)) { - if (current == 10) { // Enter - break; - } else if (current == 27) { // Escape - std::cin.get(current); - if (current == 91) { - std::cin.get(current); - if (current == 68) { // Left - toggled = true; - } else if (current == 67) { // Right - toggled = false; - } - } - } - } - - // Redraw choices - std::cout << utils::move_left(enable.length() + disable.length() + 1) - << utils::clear_line(utils::EOL) - << (toggled ? color::cyan + color::underline : "") << enable << color::reset << "/" - << (toggled ? "" : color::cyan + color::underline) << disable << color::reset; - } - std::cout << utils::show_cursor(); - utils::disable_raw_mode(); - - // Print resume - std::cout << utils::move_left(1000); - utils::print_answer(question); - std::cout << (toggled ? color::green : color::red) << (toggled ? enable : disable) << color::reset << std::endl; - - return toggled; - } -} - -#endif //ENQUIRER_HPP diff --git a/src/enquirer.cpp b/src/enquirer.cpp new file mode 100644 index 0000000..285570a --- /dev/null +++ b/src/enquirer.cpp @@ -0,0 +1,1002 @@ +/** + * MIT License + * + * Copyright (c) 2024-Present Kevin Traini + * + * 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. + */ +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + +namespace enquirer { + + // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. + // Utilities + + namespace color { + const string black = "\033[30m"; + const string red = "\033[31m"; + const string green = "\033[32m"; + const string yellow = "\033[33m"; + const string blue = "\033[34m"; + const string magenta = "\033[35m"; + const string cyan = "\033[36m"; + const string white = "\033[37m"; + const string grey = "\033[90m"; + + const string reset = "\033[0m"; + const string bold = "\033[1m"; + const string underline = "\033[4m"; + const string blink = "\033[5m"; + const string inverse = "\033[7m"; + }// namespace color + + namespace utils { + auto move_up(unsigned int n = 1) -> string { + return "\033[" + to_string(n) + "A"; + } + + auto move_down(unsigned int n = 1) -> string { + return "\033[" + to_string(n) + "B"; + } + + auto move_left(unsigned int n = 1) -> string { + return "\033[" + to_string(n) + "D"; + } + + auto move_right(unsigned int n = 1) -> string { + return "\033[" + to_string(n) + "C"; + } + + typedef enum { + EOL = 0, + BOL = 1, + LINE = 2 + } clear_mode; + + auto clear_line(clear_mode mode) -> string { + return "\033[" + to_string(mode) + "K"; + } + + auto hide_cursor() -> string { + return "\033[?25l"; + } + + auto show_cursor() -> string { + return "\033[?25h"; + } + + auto print_question(const string &question, + const string &symbol = color::cyan + color::bold + "? ", + const string &input = color::grey + color::bold + "› ") -> void { + cout << clear_line(LINE); + cout << symbol + << color::reset << question + << " " << input + << color::reset; + } + + auto print_answer(const string &question) -> void { + print_question(question, color::green + color::bold + "✔ ", color::grey + color::bold + "· "); + } + + auto enable_raw_mode() -> void { + struct termios term {}; + tcgetattr(STDIN_FILENO, &term); + term.c_lflag &= ~(ECHO | ICANON); + tcsetattr(STDIN_FILENO, TCSAFLUSH, &term); + } + + auto disable_raw_mode() -> void { + struct termios term {}; + tcgetattr(STDIN_FILENO, &term); + term.c_lflag |= (ECHO | ICANON); + tcsetattr(STDIN_FILENO, TCSAFLUSH, &term); + } + + auto ltrim(const string &str) -> string { + auto start = str.find_first_not_of(' '); + return (start == string::npos) ? "" : str.substr(start); + } + + auto rtrim(const string &str) -> string { + auto end = str.find_last_not_of(' '); + return (end == string::npos) ? "" : str.substr(0, end + 1); + } + + auto trim(const string &str) -> string { + return rtrim(ltrim(str)); + } + + auto split(const string &str, const char delim) -> vector { + vector result; + stringstream ss(str); + string item; + while (getline(ss, item, delim)) { + result.push_back(trim(item)); + } + + return result; + } + + auto filter(const vector &src, + const function &predicate) -> vector { + vector result; + + for (const auto &item: src) { + if (predicate(item)) { + result.push_back(item); + } + } + + return result; + } + + auto begin_with(const string &src, const string &prefix) -> bool { + return src.find(prefix) == 0; + } + + auto lfill(const string &src, const size_t width, const char fill = ' ') -> string { + if (src.length() >= width) { + return src; + } + + return string(width - src.length(), fill) + src; + } + + auto max_size(const vector &strs) -> unsigned int { + unsigned int max = 0; + for (const auto &str: strs) + if (str.length() > max) + max = str.length(); + + return max; + } + }// namespace utils + + // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. + // Auth + + auto auth(const string &id_prompt = "Username", + const string &pw_prompt = "Password", + char mask = '*') -> pair { + // Print inputs + vector inputs = {id_prompt, pw_prompt}; + unsigned int width = max(id_prompt.length(), pw_prompt.length()); + unsigned int line = 0; + pair answers = make_pair("", ""); + utils::print_question(color::cyan + utils::lfill(id_prompt, width), color::grey + "⊙ "); + cout << endl; + utils::print_question(utils::lfill(pw_prompt, width), color::grey + "⊙ "); + cout << endl; + cout << utils::move_up(2) << utils::move_right(width + 5); + + // Get answers + char current; + utils::enable_raw_mode(); + while (cin.get(current)) { + unsigned int previous = line; + if (iscntrl(current)) { + if (current == 10) {// Enter + if (!answers.first.empty() && !answers.second.empty()) { + break; + } else { + line = answers.first.empty() ? 0 : 1; + } + } else if (current == 127) {// Backspace + if (line == 0 && !answers.first.empty()) + answers.first.pop_back(); + else if (line == 1 && !answers.second.empty()) + answers.second.pop_back(); + + } else if (current == 27) {// Escape + cin.get(current); + if (current == 91) { + cin.get(current); + if (current == 65) {// Up + line = (line == 0) ? 1 : 0; + } else if (current == 66) {// Down + line = (line == 1) ? 0 : 1; + } + } + } + } else {// 'Normal' character + if (line == 0) + answers.first += current; + else + answers.second += current; + } + + // Redraw inputs + cout << utils::move_left(1000) << (previous == 0 ? "" : utils::move_up()); + cout << utils::clear_line(utils::EOL); + if (line == 0) { + utils::print_question(color::cyan + utils::lfill(id_prompt, width), (answers.first.empty() ? color::grey + "⊙ " : color::green + "⦿ ")); + cout << answers.first << endl; + utils::print_question(utils::lfill(pw_prompt, width), (answers.second.empty() ? color::grey + "⊙ " : color::green + "⦿ ")); + cout << string(answers.second.length(), mask) << endl; + } else { + utils::print_question(utils::lfill(id_prompt, width), (answers.first.empty() ? color::grey + "⊙ " : color::green + "⦿ ")); + cout << answers.first << endl; + utils::print_question(color::cyan + utils::lfill(pw_prompt, width), (answers.second.empty() ? color::grey + "⊙ " : color::green + "⦿ ")); + cout << string(answers.second.length(), mask) << endl; + } + cout << utils::move_up(inputs.size() - line) + << utils::move_right(width + 5 + (line == 0 ? answers.first.length() : answers.second.length())); + } + + // Print resume + cout << utils::move_left(1000) << (line == 0 ? "" : utils::move_up()); + cout << utils::clear_line(utils::EOL); + utils::print_question(utils::lfill(id_prompt, width), (answers.first.empty() ? color::grey + "⊙ " : color::green + "⦿ ")); + cout << answers.first << endl; + utils::print_question(utils::lfill(pw_prompt, width), (answers.second.empty() ? color::grey + "⊙ " : color::green + "⦿ ")); + cout << string(answers.second.length(), mask) << endl; + + return {answers.first, answers.second}; + } + + auto auth(const function &)> &predicate, + const string &id_prompt = "Username", + const string &pw_prompt = "Password", + char mask = '*') -> bool { + return predicate(auth(id_prompt, pw_prompt, mask)); + } + + // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. + // Autocomplete + + auto autocomplete(const string &question, + const vector &choices = {}, + unsigned int limit = 10) -> string { + // Print question + utils::print_question(question); + + vector current_choices; + int choice = -1; + + // Get answer + string answer; + char current; + utils::enable_raw_mode(); + while (cin.get(current)) { + if (iscntrl(current)) { + if (current == 10) {// Enter + break; + } else if (current == 127) {// Backspace + if (!answer.empty()) { + answer.pop_back(); + } + } else if (current == 9 && choice != -1) {// Tab + if (answer == current_choices[choice].substr(0, answer.length())) { + answer = string(current_choices[choice]); + } + } else if (current == 27) {// Escape + cin.get(current); + if (current == 91) { + cin.get(current); + if (current == 65) {// Up + choice = (choice == 0) ? (int) current_choices.size() - 1 : choice - 1; + } else if (current == 66) {// Down + choice = (choice == (int) min(limit, (unsigned int) current_choices.size()) - 1) ? 0 : choice + 1; + } + } + } + } else {// 'Normal' character + answer += current; + } + + // Erase previous choices + cout << utils::move_down(min(limit, (unsigned int) current_choices.size()) + 1) + << utils::move_left(1000); + for (unsigned int i = 0; i < current_choices.size() && i < limit; i++) { + cout << utils::clear_line(utils::EOL) + << utils::move_up(); + } + cout << utils::move_up(); + // Draw completion + current_choices = utils::filter(choices, [=](const string &item) { + return utils::begin_with(item, answer); + }); + choice = max(0, min(choice, (int) current_choices.size() - 1)); + cout << utils::move_left(1000) << utils::clear_line(utils::LINE); + utils::print_question(question); + cout << answer; + if (!current_choices.empty()) { + cout << color::grey << current_choices[choice].substr(answer.length()) << color::reset; + } + cout << endl; + for (unsigned int i = 0; i < current_choices.size() && i < limit; i++) { + cout << utils::clear_line(utils::EOL); + if ((int) i == choice) { + cout << color::cyan << color::underline << current_choices[i] << color::reset << endl; + } else { + cout << current_choices[i] << endl; + } + } + cout << utils::move_up(min(limit, (unsigned int) current_choices.size()) + 1) + << utils::move_left(1000) << utils::move_right(question.length() + answer.length() + 5); + } + utils::disable_raw_mode(); + + // Print resume + cout << utils::move_down(min(limit, (unsigned int) current_choices.size()) + 1) + << utils::move_left(1000); + for (unsigned int i = 0; i < current_choices.size() && i < limit; i++) { + cout << utils::clear_line(utils::EOL) + << utils::move_up(); + } + cout << utils::move_up() + << utils::move_left(1000) << utils::clear_line(utils::LINE); + utils::print_answer(question); + cout << color::cyan << answer << color::reset << endl; + + return answer; + } + + // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. + // Confirm + + auto confirm(const string &question, + bool default_value = false) -> bool { + // Print question + utils::print_question(question); + + // Print choices + bool confirmed = default_value; + cout << (confirmed ? "Yes" : "No"); + + // Get answer + char current; + utils::enable_raw_mode(); + cout << utils::hide_cursor(); + while (cin.get(current)) { + bool previous = confirmed; + if (iscntrl(current)) { + if (current == 10) {// Enter + break; + } else if (current == 27) {// Escape + cin.get(current); + if (current == 91) { + cin.get(current); + if (current == 68) {// Left + confirmed = true; + } else if (current == 67) {// Right + confirmed = false; + } + } + } + } + + // Redraw choices + cout << utils::move_left(previous ? 3 : 2) + << utils::clear_line(utils::EOL) + << (confirmed ? "Yes" : "No"); + } + cout << utils::show_cursor(); + utils::disable_raw_mode(); + + // Print resume + cout << utils::move_left(1000); + utils::print_answer(question); + cout << (confirmed ? color::green : color::red) << (confirmed ? "Yes" : "No") << color::reset << endl; + + return confirmed; + } + + // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. + // Form + + auto form(const string &question, + const vector &inputs) -> map { + if (inputs.empty()) { + return {}; + } + + // Print question + utils::print_question(question); + cout << endl; + + // Print inputs + unsigned int width = utils::max_size(inputs); + unsigned int line = 0; + map answers; + for (unsigned int i = 0; i < inputs.size(); i++) { + answers[inputs[i]] = ""; + if (i == line) { + utils::print_question(color::cyan + utils::lfill(inputs[i], width), color::grey + "⊙ "); + } else { + utils::print_question(utils::lfill(inputs[i], width), color::grey + "⊙ "); + } + cout << endl; + } + cout << utils::move_up(inputs.size()) << utils::move_right(width + 5); + + // Get answers + char current; + utils::enable_raw_mode(); + while (cin.get(current)) { + unsigned int previous = line; + if (iscntrl(current)) { + if (current == 10) {// Enter + if (all_of(answers.begin(), answers.end(), + [](const pair &item) { + return !item.second.empty(); + })) { + break; + } else { + line = distance(answers.begin(), + find_if(answers.begin(), answers.end(), + [](const pair &item) { + return item.second.empty(); + })); + } + } else if (current == 127) {// Backspace + if (!answers[inputs[line]].empty()) { + answers[inputs[line]].pop_back(); + } + } else if (current == 27) {// Escape + cin.get(current); + if (current == 91) { + cin.get(current); + if (current == 65) {// Up + line = (line == 0) ? inputs.size() - 1 : line - 1; + } else if (current == 66) {// Down + line = (line == inputs.size() - 1) ? 0 : line + 1; + } + } + } + } else {// 'Normal' character + answers[inputs[line]] += current; + } + + // Redraw inputs + cout << utils::move_left(1000) << (previous == 0 ? "" : utils::move_up(previous)); + for (unsigned int i = 0; i < inputs.size(); i++) { + cout << utils::clear_line(utils::EOL); + string indicator = (answers[inputs[i]].empty() ? color::grey + "⊙ " : color::green + "⦿ "); + if (i == line) { + utils::print_question(color::cyan + utils::lfill(inputs[i], width), indicator); + } else { + utils::print_question(utils::lfill(inputs[i], width), indicator); + } + cout << answers[inputs[i]] << endl; + } + cout << utils::move_up(inputs.size() - line) + << utils::move_right(width + 5 + answers[inputs[line]].length()); + } + + // Print resume + cout << utils::move_left(1000) << (line == 0 ? "" : utils::move_up(line)) + << utils::move_up() + << utils::clear_line(utils::EOL); + utils::print_answer(question); + cout << endl; + for (const auto &input: inputs) { + cout << utils::clear_line(utils::EOL); + utils::print_question(utils::lfill(input, width), color::green + "⦿ "); + cout << answers[input] << endl; + } + + return answers; + } + + // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. + // Input + + auto input(const string &question, + const string &default_value = "") -> string { + // Print question + utils::print_question(question); + + // Print default value + cout << color::grey << default_value << color::reset; + cout << utils::move_left(default_value.size()); + + // Get answer + string answer; + char current; + utils::enable_raw_mode(); + while (cin.get(current)) { + if (iscntrl(current)) { + if (current == 10) {// Enter + cout << endl; + break; + } else if (current == 127) {// Backspace + if (!answer.empty()) { + answer.pop_back(); + cout << utils::move_left(1); + cout << utils::clear_line(utils::EOL); + } + } else if (current == 9) {// Tab + if (answer == default_value.substr(0, answer.length())) { + cout << default_value.substr(answer.length()); + answer = default_value; + } + } else if (current == 27) {// Escape + cin.get(current); + if (current == 91) { + cin.get(current); + // Ignore arrow keys + } + } + } else {// 'Normal' character + answer += current; + cout << current; + } + + // Check default_value + if (answer == default_value.substr(0, answer.length())) { + cout << color::grey << default_value.substr(answer.length()) << color::reset; + if (answer != default_value) { + cout << utils::move_left(default_value.size() - answer.size()); + } + } else if (answer != default_value) { + cout << utils::clear_line(utils::EOL); + } + } + utils::disable_raw_mode(); + + // Print resume + cout << utils::move_up() + << utils::move_left(1000); + utils::print_answer(question); + cout << color::cyan << answer << color::reset << endl; + + return answer; + } + + // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. + // Invisible + + auto invisible(const string &question) -> string { + // Print question + utils::print_question(question); + + // Get answer + string answer; + char current; + utils::enable_raw_mode(); + while (cin.get(current)) { + if (iscntrl(current)) { + if (current == 10) {// Enter + cout << endl; + break; + } else if (current == 127) {// Backspace + if (!answer.empty()) { + answer.pop_back(); + } + } else if (current == 27) {// Escape + cin.get(current); + if (current == 91) { + cin.get(current); + // Ignore arrow keys + } + } + } else {// 'Normal' character + answer += current; + } + } + utils::disable_raw_mode(); + + // Print resume + cout << utils::move_up() + << utils::move_left(1000); + utils::print_answer(question); + cout << endl; + + return answer; + } + + // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. + // List + + auto list(const string &question) -> vector { + // Print question + utils::print_question(question); + + // Get answer + string answer; + char current; + utils::enable_raw_mode(); + while (cin.get(current)) { + if (iscntrl(current)) { + if (current == 10) {// Enter + cout << endl; + break; + } else if (current == 127) {// Backspace + if (!answer.empty()) { + answer.pop_back(); + cout << utils::move_left(1); + cout << utils::clear_line(utils::EOL); + } + } else if (current == 27) {// Escape + cin.get(current); + if (current == 91) { + cin.get(current); + // Ignore arrow keys + } + } + } else {// 'Normal' character + answer += current; + cout << current; + } + } + utils::disable_raw_mode(); + + // Print resume + cout << utils::move_up() + << utils::move_left(1000); + utils::print_answer(question); + auto items = utils::split(answer, ','); + for (auto it = items.begin(); it != items.end(); it++) { + cout << color::cyan << *it << color::reset; + if (it + 1 != items.end()) { + cout << ", "; + } + } + cout << endl; + + return items; + } + + // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. + // MultiSelect + + auto multi_select(const string &question, + const vector &choices) -> vector { + // Print question + utils::print_question(question); + cout << endl; + + unsigned int selected = 0; + bool choice[choices.size()]; + for (unsigned int i = 0; i < choices.size(); i++) { + choice[i] = false; + } + + // Print choices + for (unsigned int i = 0; i < choices.size(); i++) { + if (choice[i]) { + cout << color::bold << color::green << "✔ " << color::reset; + } else { + cout << color::grey << "✔ " << color::reset; + } + if (i == selected) { + cout << color::cyan << color::underline << choices[i] << color::reset << endl; + } else { + cout << choices[i] << endl; + } + } + + // Get answer + char current; + utils::enable_raw_mode(); + cout << utils::hide_cursor(); + while (cin.get(current)) { + if (iscntrl(current)) { + if (current == 10) {// Enter + break; + } else if (current == 27) {// Escape + cin.get(current); + if (current == 91) { + cin.get(current); + if (current == 65) {// Up + selected = (selected == 0) ? choices.size() - 1 : selected - 1; + } else if (current == 66) {// Down + selected = (selected == choices.size() - 1) ? 0 : selected + 1; + } else if (current == 67) {// Right + choice[selected] = true; + } else if (current == 68) {// Left + choice[selected] = false; + } + } + } + } + + // Redraw choices + cout << utils::move_up(choices.size()) + << utils::move_left(1000); + for (unsigned int i = 0; i < choices.size(); i++) { + if (choice[i]) { + cout << color::bold << color::green << "✔ " << color::reset; + } else { + cout << color::grey << "✔ " << color::reset; + } + if (i == selected) { + cout << color::cyan << color::underline << choices[i] << color::reset << endl; + } else { + cout << choices[i] << endl; + } + } + } + cout << utils::show_cursor(); + utils::disable_raw_mode(); + + // Print resume + for (unsigned int i = 0; i < choices.size(); i++) {// Clear choices + cout << utils::move_up() << utils::clear_line(utils::EOL); + } + cout << utils::move_up() + << utils::move_left(1000); + utils::print_answer(question); + vector items; + for (unsigned int i = 0; i < choices.size(); i++) { + if (choice[i]) { + items.push_back(choices[i]); + } + } + for (auto it = items.begin(); it != items.end(); it++) { + cout << color::cyan << *it << color::reset; + if (it + 1 != items.end()) { + cout << ", "; + } + } + cout << endl; + + return items; + } + + // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. + // Password + + auto password(const string &question, + char mask = '*') -> string { + // Print question + utils::print_question(question); + + // Get answer + string answer; + char current; + utils::enable_raw_mode(); + while (cin.get(current)) { + if (iscntrl(current)) { + if (current == 10) {// Enter + cout << endl; + break; + } else if (current == 127) {// Backspace + if (!answer.empty()) { + answer.pop_back(); + cout << utils::move_left(1); + cout << utils::clear_line(utils::EOL); + } + } else if (current == 27) {// Escape + cin.get(current); + if (current == 91) { + cin.get(current); + // Ignore arrow keys + } + } + } else {// 'Normal' character + answer += current; + cout << mask; + } + } + utils::disable_raw_mode(); + + // Print resume + cout << utils::move_up() + << utils::move_left(1000); + utils::print_answer(question); + cout << color::cyan << string(answer.size(), mask) << color::reset << endl; + + return answer; + } + + // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. + // Quiz + + auto quiz(const string &question, + const vector &choices, + const string &correct) -> bool { + // Print question + utils::print_question(question); + cout << endl; + + unsigned int choice = 0; + + // Print choices + for (unsigned int i = 0; i < choices.size(); i++) { + if (i == choice) { + cout << color::cyan << color::bold << "> " << color::reset + << color::cyan << color::underline << choices[i] << color::reset << endl; + } else { + cout << " " << choices[i] << endl; + } + } + + // Get answer + char current; + utils::enable_raw_mode(); + cout << utils::hide_cursor(); + while (cin.get(current)) { + if (iscntrl(current)) { + if (current == 10) {// Enter + break; + } else if (current == 27) {// Escape + cin.get(current); + if (current == 91) { + cin.get(current); + if (current == 65) {// Up + choice = (choice == 0) ? choices.size() - 1 : choice - 1; + } else if (current == 66) {// Down + choice = (choice == choices.size() - 1) ? 0 : choice + 1; + } + } + } + } + + // Redraw choices + cout << utils::move_up(choices.size()) + << utils::move_left(1000); + for (unsigned int i = 0; i < choices.size(); i++) { + if (i == choice) { + cout << color::cyan << color::bold << "> " << color::reset + << color::cyan << color::underline << choices[i] << color::reset << endl; + } else { + cout << " " << choices[i] << endl; + } + } + } + cout << utils::show_cursor(); + utils::disable_raw_mode(); + + // Print resume + for (unsigned int i = 0; i < choices.size(); i++) {// Clear choices + cout << utils::move_up() << utils::clear_line(utils::EOL); + } + cout << utils::move_up() + << utils::move_left(1000); + utils::print_answer(question); + bool result = (choices[choice] == correct); + cout << (result ? color::green : color::red) << choices[choice] << color::reset << endl; + + return result; + } + + // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. + // Select + + auto select(const string &question, + const vector &choices) -> string { + // Print question + utils::print_question(question); + cout << endl; + + unsigned int choice = 0; + + // Print choices + for (unsigned int i = 0; i < choices.size(); i++) { + if (i == choice) { + cout << color::cyan << color::bold << "> " << color::reset + << color::cyan << color::underline << choices[i] << color::reset << endl; + } else { + cout << " " << choices[i] << endl; + } + } + + // Get answer + char current; + utils::enable_raw_mode(); + cout << utils::hide_cursor(); + while (cin.get(current)) { + if (iscntrl(current)) { + if (current == 10) {// Enter + break; + } else if (current == 27) {// Escape + cin.get(current); + if (current == 91) { + cin.get(current); + if (current == 65) {// Up + choice = (choice == 0) ? choices.size() - 1 : choice - 1; + } else if (current == 66) {// Down + choice = (choice == choices.size() - 1) ? 0 : choice + 1; + } + } + } + } + + // Redraw choices + cout << utils::move_up(choices.size()) + << utils::move_left(1000); + for (unsigned int i = 0; i < choices.size(); i++) { + if (i == choice) { + cout << color::cyan << color::bold << "> " << color::reset + << color::cyan << color::underline << choices[i] << color::reset << endl; + } else { + cout << " " << choices[i] << endl; + } + } + } + cout << utils::show_cursor(); + utils::disable_raw_mode(); + + // Print resume + for (unsigned int i = 0; i < choices.size(); i++) {// Clear choices + cout << utils::move_up() << utils::clear_line(utils::EOL); + } + cout << utils::move_up() + << utils::move_left(1000); + utils::print_answer(question); + cout << color::cyan << choices[choice] << color::reset << endl; + + return choices[choice]; + } + + // _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. + // Toggle + + auto toggle(const string &question, + const string &enable, + const string &disable, + bool default_value = false) -> bool { + // Print question + utils::print_question(question); + + // Print choices + bool toggled = default_value; + cout << (toggled ? color::cyan + color::underline : "") << enable << color::reset << "/" + << (toggled ? "" : color::cyan + color::underline) << disable << color::reset; + + // Get answer + char current; + utils::enable_raw_mode(); + cout << utils::hide_cursor(); + while (cin.get(current)) { + if (iscntrl(current)) { + if (current == 10) {// Enter + break; + } else if (current == 27) {// Escape + cin.get(current); + if (current == 91) { + cin.get(current); + if (current == 68) {// Left + toggled = true; + } else if (current == 67) {// Right + toggled = false; + } + } + } + } + + // Redraw choices + cout << utils::move_left(enable.length() + disable.length() + 1) + << utils::clear_line(utils::EOL) + << (toggled ? color::cyan + color::underline : "") << enable << color::reset << "/" + << (toggled ? "" : color::cyan + color::underline) << disable << color::reset; + } + cout << utils::show_cursor(); + utils::disable_raw_mode(); + + // Print resume + cout << utils::move_left(1000); + utils::print_answer(question); + cout << (toggled ? color::green : color::red) << (toggled ? enable : disable) << color::reset << endl; + + return toggled; + } +}// namespace enquirer diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..14741b8 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,20 @@ +include(FetchContent) +FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/refs/tags/v1.14.0.zip +) +# For Windows: Prevent overriding the parent project's compiler/linker settings +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +FetchContent_MakeAvailable(googletest) + +include(GoogleTest) + +add_executable(demo + demo.cpp +) +target_link_libraries(demo PRIVATE enquirer) + +add_executable(unit-tests + tests.cpp +) +target_link_libraries(unit-tests PRIVATE enquirer gtest_main gtest gmock) \ No newline at end of file diff --git a/tests/demo.cpp b/tests/demo.cpp index 583d377..3c461a0 100644 --- a/tests/demo.cpp +++ b/tests/demo.cpp @@ -1,39 +1,55 @@ /** - * === Enquirer === - * Created by Kevin Traini - * Under GPL-3.0 - * ----------------------- + * MIT License + * + * Copyright (c) 2023 Kevin Traini + * + * 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. */ +#include #include -#include "enquirer.hpp" using namespace std; -int main() { +auto main() -> int { cout << "Enquirer demo, version " << ENQUIRER_VERSION << endl; do { - string choice = enquirer::select("Choose a function to test:", { - "auth", - "autocomplete", - "confirm", - "form", - "input", - "invisible", - "list", - "multiselect", - "number", - "password", - "quiz", - "slider", - "select", - "toggle" - }); + string choice = enquirer::select( + "Choose a function to test:", + {"auth", + "autocomplete", + "confirm", + "form", + "input", + "invisible", + "list", + "multiselect", + "number", + "password", + "quiz", + "slider", + "select", + "toggle"}); if (choice == "auth") { bool is_valid = enquirer::auth([](const std::pair &credentials) { - return credentials.first == "admin" - && credentials.second == "admin"; + return credentials.first == "admin" && credentials.second == "admin"; }); if (is_valid) { cout << "You are successfully authenticated!" << endl; @@ -41,16 +57,16 @@ int main() { cout << "Invalid credentials!" << endl; } } else if (choice == "autocomplete") { - string answer = enquirer::autocomplete("What is you favorite fruit", { - "Apple", - "Banana", - "Blueberry", - "Cherry", - "Orange", - "Pear", - "Raspberry", - "Strawberry" - }); + string answer = enquirer::autocomplete( + "What is you favorite fruit", + {"Apple", + "Banana", + "Blueberry", + "Cherry", + "Orange", + "Pear", + "Raspberry", + "Strawberry"}); cout << "Your favorite fruit is " << answer << endl; } else if (choice == "confirm") { bool quit = false; @@ -58,11 +74,9 @@ int main() { quit = enquirer::confirm("Do you want to quit?"); } } else if (choice == "form") { - auto answers = enquirer::form("Please provide some informations:", { - "Firstname", - "Lastname", - "Username" - }); + auto answers = enquirer::form("Please provide some informations:", {"Firstname", + "Lastname", + "Username"}); cout << "Hi " << answers["Firstname"] << " " << answers["Lastname"] << ", also called " << answers["Username"] << "!" << endl; } else if (choice == "input") { @@ -79,16 +93,16 @@ int main() { } cout << endl; } else if (choice == "multiselect") { - auto choices = enquirer::multi_select("Choose some colors", { - "Red", - "Green", - "Blue", - "Yellow", - "Magenta", - "Cyan", - "White", - "Black" - }); + auto choices = enquirer::multi_select( + "Choose some colors", + {"Red", + "Green", + "Blue", + "Yellow", + "Magenta", + "Cyan", + "White", + "Black"}); cout << "You chose " << choices.size() << " colors:" << endl; for (const auto &c: choices) { cout << "'" << c << "' "; @@ -109,11 +123,9 @@ int main() { int value = enquirer::slider("How much do you want?", 0, 10, 1, 1); cout << "You want " << value << " potatoes" << endl; } else if (choice == "select") { - auto language = enquirer::select("Which is the best one?", { - "c++", - "python", - "java" - }); + auto language = enquirer::select("Which is the best one?", {"c++", + "python", + "java"}); if (language == "c++") cout << "You are right!" << endl; else diff --git a/tests/test.cpp b/tests/test.cpp deleted file mode 100644 index d1e7be2..0000000 --- a/tests/test.cpp +++ /dev/null @@ -1,758 +0,0 @@ -/** - * === Enquirer === - * Created by Kevin Traini - * Under GPL-3.0 - * ----------------------- - */ -#include -#include "enquirer.hpp" -#include "test.hpp" - -using namespace std; -using namespace test; - -namespace utils_char { - const string arrow_up = "\e[A"; - const string arrow_down = "\e[B"; - const string arrow_right = "\e[C"; - const string arrow_left = "\e[D"; - - const string tab = "\t"; - const string del = "\x7F"; -} - -void construct_test(Test &test) { - test.describe("Auth 1", []() { - it_pass_fail("return username and password", []() { - stringstream ss; - streambuf *old = cin.rdbuf(ss.rdbuf()); - - ss << "username" << utils_char::arrow_down << "password" << endl; - auto result = enquirer::auth(); - - cin.rdbuf(old); - - assert_equal("username", result.first); - assert_equal("password", result.second); - - return PASS; - }) - - it_pass_skip("type password then username", []() { - stringstream ss; - streambuf *old = cin.rdbuf(ss.rdbuf()); - - ss << utils_char::arrow_up << "password" << utils_char::arrow_up << "username" << endl; - auto result = enquirer::auth(); - - cin.rdbuf(old); - - assert_equal("username", result.first); - assert_equal("password", result.second); - - return PASS; - }) - - return PASS; - }); - - test.describe("Auth 2", []() { - it_pass_fail("correct credentials", []() { - stringstream ss; - streambuf *old = cin.rdbuf(ss.rdbuf()); - - ss << "admin" << utils_char::arrow_down << "admin" << endl; - bool result = enquirer::auth([](const pair &crd) { - return crd.first == "admin" && crd.second == "admin"; - }); - - cin.rdbuf(old); - - assert_true(result); - - return PASS; - }) - - it_pass_fail("wrong credentials", []() { - stringstream ss; - streambuf *old = cin.rdbuf(ss.rdbuf()); - - ss << "abcde" << utils_char::arrow_down << "abcde" << endl; - bool result = enquirer::auth([](const pair &crd) { - return crd.first == "admin" && crd.second == "admin"; - }); - - cin.rdbuf(old); - - assert_false(result); - - return PASS; - }) - - return PASS; - }); - - test.describe("Autocomplete", []() { - it_pass_fail("can complete first", []() { - stringstream ss; - streambuf *old = cin.rdbuf(ss.rdbuf()); - - ss << "a" << utils_char::tab << endl; - string result = enquirer::autocomplete("", {"abcde"}); - - cin.rdbuf(old); - - assert_equal("abcde", result); - - return PASS; - }) - - it_pass_fail("can complete second", []() { - stringstream ss; - streambuf *old = cin.rdbuf(ss.rdbuf()); - - ss << "a" << utils_char::arrow_down << utils_char::tab << endl; - string result = enquirer::autocomplete("", {"abcde", "abcde2"}); - - cin.rdbuf(old); - - assert_equal("abcde2", result); - - return PASS; - }) - - it_pass_fail("can take other input", []() { - stringstream ss; - streambuf *old = cin.rdbuf(ss.rdbuf()); - - ss << "a" << utils_char::del << "bonjour" << endl; - string result = enquirer::autocomplete("", {"abcde", "abcde2"}); - - cin.rdbuf(old); - - assert_equal("bonjour", result); - - return PASS; - }) - - it_pass_fail("can complete first passing by second", []() { - stringstream ss; - streambuf *old = cin.rdbuf(ss.rdbuf()); - - ss << "a" << utils_char::arrow_down << utils_char::arrow_down << utils_char::tab << endl; - string result = enquirer::autocomplete("", {"abcde", "abcdef"}); - - cin.rdbuf(old); - - assert_equal("abcde", result); - - return PASS; - }) - - return PASS; - }); - - test.describe("Confirm", []() { - it_pass_fail("default false", []() { - stringstream ss; - streambuf *old = cin.rdbuf(ss.rdbuf()); - - ss << endl; - bool result = enquirer::confirm("test", false); - - cin.rdbuf(old); - - assert_false(result); - - return PASS; - }) - - it_pass_fail("default true", []() { - stringstream ss; - streambuf *old = cin.rdbuf(ss.rdbuf()); - - ss << endl; - bool result = enquirer::confirm("test", true); - - cin.rdbuf(old); - - assert_true(result); - - return PASS; - }) - - it_pass_fail("can confirm true from false", []() { - stringstream ss; - streambuf *old = cin.rdbuf(ss.rdbuf()); - - ss << utils_char::arrow_left << endl; - bool result = enquirer::confirm("test", false); - - cin.rdbuf(old); - - assert_true(result); - - return PASS; - }) - - it_pass_fail("can confirm false from true", []() { - stringstream ss; - streambuf *old = cin.rdbuf(ss.rdbuf()); - - ss << utils_char::arrow_right << endl; - bool result = enquirer::confirm("test", true); - - cin.rdbuf(old); - - assert_false(result); - - return PASS; - }) - - return PASS; - }); - - test.describe("Form", []() { - it_pass_fail("can take input", []() { - stringstream ss; - streambuf *old = cin.rdbuf(ss.rdbuf()); - - ss << "test" << utils_char::arrow_down - << "test2" << utils_char::arrow_down - << "test3" << endl; - auto answers = enquirer::form("Please provide some informations:", { - "Firstname", - "Lastname", - "Username" - }); - - cin.rdbuf(old); - - assert_equal("test", answers["Firstname"]); - assert_equal("test2", answers["Lastname"]); - assert_equal("test3", answers["Username"]); - - return PASS; - }) - - it_pass_fail("can take input in reverse order", []() { - stringstream ss; - streambuf *old = cin.rdbuf(ss.rdbuf()); - - ss << utils_char::arrow_up << "test3" - << utils_char::arrow_up << "test2" - << utils_char::arrow_up << "test" << endl; - auto answers = enquirer::form("Please provide some informations:", { - "Firstname", - "Lastname", - "Username" - }); - - cin.rdbuf(old); - - assert_equal("test", answers["Firstname"]); - assert_equal("test2", answers["Lastname"]); - assert_equal("test3", answers["Username"]); - - return PASS; - }) - - it_pass_fail("fill first and last, then enter and fill second", []() { - stringstream ss; - streambuf *old = cin.rdbuf(ss.rdbuf()); - - ss << "test" << utils_char::arrow_down << utils_char::arrow_down - << "test3" << endl - << "test2" << endl; - auto answers = enquirer::form("Please provide some informations:", { - "Firstname", - "Lastname", - "Username" - }); - - cin.rdbuf(old); - - assert_equal("test", answers["Firstname"]); - assert_equal("test2", answers["Lastname"]); - assert_equal("test3", answers["Username"]); - - return PASS; - }) - - it_pass_fail("can complete only with enter", []() { - stringstream ss; - streambuf *old = cin.rdbuf(ss.rdbuf()); - - ss << "test" << endl - << "test2" << endl - << "test3" << endl; - auto answers = enquirer::form("Please provide some informations:", { - "Firstname", - "Lastname", - "Username" - }); - - cin.rdbuf(old); - - assert_equal("test", answers["Firstname"]); - assert_equal("test2", answers["Lastname"]); - assert_equal("test3", answers["Username"]); - - return PASS; - }) - - return PASS; - }); - - test.describe("Input", []() { - it_pass_fail("return the input", []() { - stringstream ss; - streambuf *old = cin.rdbuf(ss.rdbuf()); - - ss << "Hello world!" << endl; - string res = enquirer::input("Type something"); - - cin.rdbuf(old); - - assert_equal(res, "Hello world!") - - return PASS; - }) - - it_pass_fail("complete default value", []() { - stringstream ss; - streambuf *old = cin.rdbuf(ss.rdbuf()); - - ss << "J" << utils_char::tab << endl; - string res = enquirer::input("Type something", "John"); - - cin.rdbuf(old); - - assert_equal(res, "John") - - return PASS; - }) - - return PASS; - }); - - test.describe("Invisible", []() { - it_pass_fail("return the input", []() { - stringstream ss; - streambuf *old = cin.rdbuf(ss.rdbuf()); - - ss << "Hello world!" << endl; - string res = enquirer::invisible("Type something"); - - cin.rdbuf(old); - - assert_equal(res, "Hello world!") - - return PASS; - }) - - return PASS; - }); - - test.describe("List", []() { - it_pass_fail("can take simple input", []() { - stringstream ss; - streambuf *old = cin.rdbuf(ss.rdbuf()); - - ss << "a" << endl; - auto res = enquirer::list("Type something"); - - cin.rdbuf(old); - - assert_equal(1, res.size()); - assert_equal("a", res[0]); - - return PASS; - }) - - it_pass_fail("can take multiple input", []() { - stringstream ss; - streambuf *old = cin.rdbuf(ss.rdbuf()); - - ss << "a, b, c" << endl; - auto res = enquirer::list("Type something"); - - cin.rdbuf(old); - - assert_equal(3, res.size()); - assert_equal("a", res[0]); - assert_equal("b", res[1]); - assert_equal("c", res[2]); - - return PASS; - }) - - it_pass_fail("can take input with more spaces", []() { - stringstream ss; - streambuf *old = cin.rdbuf(ss.rdbuf()); - - ss << "a , b, c ,d,e,f" << endl; - auto res = enquirer::list("Type something"); - - cin.rdbuf(old); - - assert_equal(6, res.size()); - assert_equal("a", res[0]); - assert_equal("b", res[1]); - assert_equal("c", res[2]); - assert_equal("d", res[3]); - assert_equal("e", res[4]); - assert_equal("f", res[5]); - - return PASS; - }) - - it_pass_fail("can take empty input at begin", []() { - stringstream ss; - streambuf *old = cin.rdbuf(ss.rdbuf()); - - ss << ", b, c" << endl; - auto res = enquirer::list("Type something"); - - cin.rdbuf(old); - - assert_equal(3, res.size()); - assert_empty(res[0]); - assert_equal("b", res[1]); - assert_equal("c", res[2]); - - return PASS; - }) - - it_pass_fail("can take empty input at middle", []() { - stringstream ss; - streambuf *old = cin.rdbuf(ss.rdbuf()); - - ss << "a, , c" << endl; - auto res = enquirer::list("Type something"); - - cin.rdbuf(old); - - assert_equal(3, res.size()); - assert_equal("a", res[0]); - assert_empty(res[1]); - assert_equal("c", res[2]); - - return PASS; - }) - - it_pass_fail("can take empty input at end", []() { - stringstream ss; - streambuf *old = cin.rdbuf(ss.rdbuf()); - - ss << "a, b, " << endl; - auto res = enquirer::list("Type something"); - - cin.rdbuf(old); - - assert_equal(3, res.size()); - assert_equal("a", res[0]); - assert_equal("b", res[1]); - assert_empty(res[2]); - - return PASS; - }) - - return PASS; - }); - - test.describe("MultiSelect", []() { - it_pass_fail("can take no input", []() { - stringstream ss; - streambuf *old = cin.rdbuf(ss.rdbuf()); - - ss << endl; - auto res = enquirer::multi_select("Choose", { - "a", - "b", - "c" - }); - - cin.rdbuf(old); - - assert_empty(res); - - return PASS; - }) - - it_pass_fail("can take several input", []() { - stringstream ss; - streambuf *old = cin.rdbuf(ss.rdbuf()); - - ss << utils_char::arrow_right << utils_char::arrow_up - << utils_char::arrow_right << endl; - auto res = enquirer::multi_select("Choose", { - "a", - "b", - "c" - }); - - cin.rdbuf(old); - - assert_equal(2, res.size()); - assert_equal("a", res[0]); - assert_equal("c", res[1]); - - return PASS; - }) - - it_pass_fail("can take several input in reverse order", []() { - stringstream ss; - streambuf *old = cin.rdbuf(ss.rdbuf()); - - ss << utils_char::arrow_up << utils_char::arrow_right - << utils_char::arrow_up << utils_char::arrow_right - << utils_char::arrow_up << utils_char::arrow_right - << endl; - auto res = enquirer::multi_select("Choose", { - "a", - "b", - "c" - }); - - cin.rdbuf(old); - - assert_equal(3, res.size()); - assert_equal("a", res[0]); - assert_equal("b", res[1]); - assert_equal("c", res[2]); - - return PASS; - }) - - return PASS; - }); - - test.describe("Number", []() { - it_pass_fail("can take simple input", []() { - stringstream ss; - streambuf *old = cin.rdbuf(ss.rdbuf()); - - ss << "1" << endl; - auto res = enquirer::number("Type something"); - - cin.rdbuf(old); - - assert_equal(1, res); - - return PASS; - }) - - return PASS; - }); - - test.describe("Password", []() { - it_pass_fail("can take simple input", []() { - stringstream ss; - streambuf *old = cin.rdbuf(ss.rdbuf()); - - ss << "p@ssw0rd" << endl; - auto res = enquirer::password("Type something"); - - cin.rdbuf(old); - - assert_equal("p@ssw0rd", res); - - return PASS; - }) - - return PASS; - }); - - test.describe("Quiz", []() { - it_pass_fail("good answer", []() { - stringstream ss; - streambuf *old = cin.rdbuf(ss.rdbuf()); - - ss << endl; - bool res = enquirer::quiz("Which is yellow?", {"Banana", "Coconut", "Strawberry"}, "Banana"); - - cin.rdbuf(old); - - assert_true(res); - - return PASS; - }) - - it_pass_fail("wrong answer", []() { - stringstream ss; - streambuf *old = cin.rdbuf(ss.rdbuf()); - - ss << utils_char::arrow_up << endl; - bool res = enquirer::quiz("Which is yellow?", {"Banana", "Coconut", "Strawberry"}, "Banana"); - - cin.rdbuf(old); - - assert_false(res); - - return PASS; - }) - - return PASS; - }); - - test.describe("Slider", []() { - it_pass_fail("return initial value", []() { - stringstream ss; - streambuf *old = cin.rdbuf(ss.rdbuf()); - - ss << endl; - int res = enquirer::slider("Choose", 1, 10, 1, 5); - - cin.rdbuf(old); - - assert_equal(5, res); - - return PASS; - }) - - it_pass_fail("return min value", []() { - stringstream ss; - streambuf *old = cin.rdbuf(ss.rdbuf()); - - ss << utils_char::arrow_left - << utils_char::arrow_left - << utils_char::arrow_left - << utils_char::arrow_left - << endl; - int res = enquirer::slider("Choose", 1, 10, 1, 5); - - cin.rdbuf(old); - - assert_equal(1, res); - - return PASS; - }) - - it_pass_fail("return max value", []() { - stringstream ss; - streambuf *old = cin.rdbuf(ss.rdbuf()); - - ss << utils_char::arrow_right - << utils_char::arrow_right - << utils_char::arrow_right - << utils_char::arrow_right - << utils_char::arrow_right - << endl; - int res = enquirer::slider("Choose", 1, 10, 1, 5); - - cin.rdbuf(old); - - assert_equal(10, res); - - return PASS; - }) - - return PASS; - }); - - test.describe("Select", []() { - it_pass_fail("return first value", []() { - stringstream ss; - streambuf *old = cin.rdbuf(ss.rdbuf()); - - ss << endl; - string res = enquirer::select("Choose", {"a", "b", "c"}); - - cin.rdbuf(old); - - assert_equal("a", res); - - return PASS; - }) - - it_pass_fail("return last value", []() { - stringstream ss; - streambuf *old = cin.rdbuf(ss.rdbuf()); - - ss << utils_char::arrow_up - << endl; - string res = enquirer::select("Choose", {"a", "b", "c"}); - - cin.rdbuf(old); - - assert_equal("c", res); - - return PASS; - }) - - return PASS; - }); - - test.describe("Toggle", []() { - it_pass_fail("return default true", []() { - stringstream ss; - streambuf *old = cin.rdbuf(ss.rdbuf()); - - ss << endl; - bool res = enquirer::toggle("Choose", "", "", true); - - cin.rdbuf(old); - - assert_true(res); - - return PASS; - }) - - it_pass_fail("return default false", []() { - stringstream ss; - streambuf *old = cin.rdbuf(ss.rdbuf()); - - ss << endl; - bool res = enquirer::toggle("Choose", "", "", false); - - cin.rdbuf(old); - - assert_false(res); - - return PASS; - }) - - it_pass_fail("return true", []() { - stringstream ss; - streambuf *old = cin.rdbuf(ss.rdbuf()); - - ss << utils_char::arrow_left - << endl; - bool res = enquirer::toggle("Choose", "", "", false); - - cin.rdbuf(old); - - assert_true(res); - - return PASS; - }) - - it_pass_fail("return false", []() { - stringstream ss; - streambuf *old = cin.rdbuf(ss.rdbuf()); - - ss << utils_char::arrow_right - << endl; - bool res = enquirer::toggle("Choose", "", "", true); - - cin.rdbuf(old); - - assert_false(res); - - return PASS; - }) - - return PASS; - }); -} - -int main() { - cout << "Enquirer test, version " << ENQUIRER_VERSION << endl << endl; - - Test test; - - construct_test(test); - - return test.run() ? 0 : 1; -} \ No newline at end of file diff --git a/tests/test.hpp b/tests/test.hpp deleted file mode 100644 index 770ccbb..0000000 --- a/tests/test.hpp +++ /dev/null @@ -1,387 +0,0 @@ -#ifndef TEST_HPP -#define TEST_HPP - -#include -#include -#include -#include -#include -#include - -/* - * Simple c++ test framework - * - * Usage: - * // Instantiate a test object - * Test test; - * // Describe some tests - * test.describe("Test 1", []() { - * // Test some things - * Test::Result res = it("1 is equal to 1", []() { - * assert_equal(1, 1); - * - * return Test::PASS; - * }); - * - * // You can use assert (or should directly) - * should_not_equal(1, 2); - * - * return res; - * }); - * // Run tests - * bool success = test.run(); - * - * // It will display that (with colors) - * Run 1 test - * Test 1 - * ✔ 1 is equal to 1 - * PASS - * - * All tests passed - * - * // describe() - * method describe takes 2 arguments : - * - name of the test - * - function of the test. This function should return a test::Result - * - * // it() - * macro it takes 2 arguments : - * - name of the test - * - function of the test. This function should return a test::Result - * - * // assert_* and should_* - * For each assert, there is an equivalent should (and vice versa) - * If assert failed, the test fail. If should failed, the test is skipped - * List of assert_* and should_* - * - equal - * - not_equal - * - less - * - great - * - less_equal - * - great_equal - * - true - * - false - * - empty - * - not_empty - * - * // it__() - * These macros are some helpers for tests, it runs a test and return b if not a. - * In fact, it's just a shortcut : it__fail() is assert_equal(, it()) and - * it__skip() is should_equal(, it()) - * List of it__() - * - it_pass_fail - * - it_fail_fail - * - it_skip_fail - * - it_pass_skip - * - it_fail_skip - * - it_skip_skip - */ - -// _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. - -/** - * Colors used for displaying tests - */ -namespace color { - const std::string black = "\033[30m"; - const std::string red = "\033[31m"; - const std::string green = "\033[32m"; - const std::string yellow = "\033[33m"; - const std::string blue = "\033[34m"; - const std::string cyan = "\033[36m"; - const std::string grey = "\033[90m"; - - const std::string bg_red = "\033[41m"; - const std::string bg_green = "\033[42m"; - const std::string bg_yellow = "\033[43m"; - - const std::string reset = "\033[0m"; - const std::string bold = "\033[1m"; -} - -/** - * Utility functions for tests - */ -namespace utils { - inline std::string move_left(uint n = 1) { - return "\033[" + std::to_string(n) + "D"; - } - - inline std::string clear_line() { - return "\033[0K"; - } - - inline std::string hide_cursor() { - return "\033[?25l"; - } - - inline std::string show_cursor() { - return "\033[?25h"; - } - - inline void enable_raw_mode() { - struct termios term{}; - tcgetattr(STDIN_FILENO, &term); - term.c_lflag &= ~(ECHO | ICANON); - tcsetattr(STDIN_FILENO, TCSAFLUSH, &term); - } - - inline void disable_raw_mode() { - struct termios term{}; - tcgetattr(STDIN_FILENO, &term); - term.c_lflag |= (ECHO | ICANON); - tcsetattr(STDIN_FILENO, TCSAFLUSH, &term); - } -} - -// _.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-. - -namespace test { - /** - * Result of a test - */ - typedef enum { - PASS = 0, - FAIL = 1, - SKIP = 2 - } Result; - - /** - * Class Test used to describe and run tests - */ - class Test { - public: - Test() = default; - - /** - * Describe a new test - * - * @param name Name of the test - * @param func The test itself - */ - void describe(const std::string &name, const std::function &func) { - _tests[name] = func; - } - - /** - * Run all described tests - * - * @return Result of tests - */ - bool run() { - utils::enable_raw_mode(); - std::cout << utils::hide_cursor(); - - std::cout << color::bold << color::blue << " Run " << _tests.size() << " test" - << (_tests.size() > 1 ? "s" : "") << color::reset - << std::endl; - - int passed = 0; - before_all(); - for (const auto &test: _tests) { - std::cout << color::cyan << test.first << color::reset << std::endl; - - before_each(); - auto res = test.second(); - after_each(); - - switch (res) { - case PASS: - std::cout << " " << color::bg_green << color::black << " PASS " << color::reset << std::endl; - passed++; - break; - case FAIL: - std::cout << " " << color::bg_red << color::black << " FAIL " << color::reset << std::endl; - break; - case SKIP: - std::cout << " " << color::bg_yellow << color::black << " SKIP " << color::reset << std::endl; - passed++; - break; - } - std::cout << std::endl; - } - after_all(); - - // Resume - bool result = passed == _tests.size(); - if (result) { - std::cout << color::bold << color::green << " All tests passed " << color::reset << std::endl; - } else { - std::cout << color::bold << color::red << " " << _tests.size() - passed << " tests failed " - << color::reset << std::endl; - } - - std::cout << utils::show_cursor(); - utils::disable_raw_mode(); - - return result; - } - - /** - * Function called before each tests - */ - std::function before_each = []() {}; - /** - * Function called after each tests - */ - std::function after_each = []() {}; - /** - * Function called before all tests - */ - std::function before_all = []() {}; - /** - * Function called after all tests - */ - std::function after_all = []() {}; - - private: - std::map> _tests; - }; - -// ==================== - -/** - * Run a simple test and return the result - */ -#define it(name, func) ([]() { \ - std::cout << color::grey << "⏲ " << name << color::reset; \ - std::cout.flush(); \ - std::streambuf *old = std::cout.rdbuf(nullptr); \ - test::Result res = func(); \ - std::cout.rdbuf(old); \ - std::cout << utils::move_left(1000) << utils::clear_line(); \ - switch (res) { \ - case test::PASS: \ - std::cout << color::green << "✔ " << name << color::reset << std::endl; \ - break; \ - case test::SKIP: \ - std::cout << color::yellow << "⚠ " << name << color::reset << std::endl; \ - break; \ - case test::FAIL: \ - std::cout << color::red << "✘ " << name << color::reset << std::endl; \ - break; \ - } \ - return res; \ - })() - -/** - * Derivation of it() that fail if the test is FAIL or SKIP - */ -#define it_pass_fail(name, func) assert_equal(test::PASS, it(name, func)) - -/** - * Derivation of it() that fail if the test is PASS or SKIP - */ -#define it_fail_fail(name, func) assert_equal(test::FAIL, it(name, func)) - -/** - * Derivation of it() that fail if the test is PASS or FAIL - */ -#define it_skip_fail(name, func) assert_equal(test::SKIP, it(name, func)) - -/** - * Derivation of it() that skip if the test is FAIL or SKIP - */ -#define it_pass_skip(name, func) should_equal(test::PASS, it(name, func)) - -/** - * Derivation of it() that skip if the test is PASS or SKIP - */ -#define it_fail_skip(name, func) should_equal(test::FAIL, it(name, func)) - -/** - * Derivation of it() that skip if the test is PASS or FAIL - */ -#define it_skip_skip(name, func) should_equal(test::SKIP, it(name, func)) - -// ==================== -// Asserts (FAIL if false) - -/** - * Assert that a is equal to b - */ -#define assert_equal(a, b) if (a != b) { return test::FAIL; } -/** - * Assert that a is not equal to b - */ -#define assert_not_equal(a, b) if (a == b) { return test::FAIL; } -/** - * Assert that a is less than b - */ -#define assert_less(a, b) if (a >= b) { return test::FAIL; } -/** - * Assert that a is greater than b - */ -#define assert_great(a, b) if (a <= b) { return test::FAIL; } -/** - * Assert that a is less or equal to b - */ -#define assert_less_equal(a, b) if (a > b) { return test::FAIL; } -/** - * Assert that a is greater or equal to b - */ -#define assert_great_equal(a, b) if (a < b) { return test::FAIL; } -/** - * Assert that a is true - */ -#define assert_true(a) if (!a) { return test::FAIL; } -/** - * Assert that a is false - */ -#define assert_false(a) if (a) { return test::FAIL; } -/** - * Assert that a is empty - */ -#define assert_empty(a) if (!a.empty()) { return test::FAIL; } -/** - * Assert that a is not empty - */ -#define assert_not_empty(a) if (!a.empty()) { return test::FAIL; } - -// ==================== -// Should (SKIP if false) - -/** - * a should be equal to b - */ -#define should_equal(a, b) if (a != b) { return test::SKIP; } -/** - * a should not be equal to b - */ -#define should_not_equal(a, b) if (a == b) { return test::SKIP; } -/** - * a should be less than b - */ -#define should_less(a, b) if (a >= b) { return test::SKIP; } -/** - * a should be greater than b - */ -#define should_great(a, b) if (a <= b) { return test::SKIP; } -/** - * a should be less or equal to b - */ -#define should_less_equal(a, b) if (a > b) { return test::SKIP; } -/** - * a should be greater or equal to b - */ -#define should_great_equal(a, b) if (a < b) { return test::SKIP; } -/** - * a should be true - */ -#define should_true(a) if (!a) { return test::SKIP; } -/** - * a should be false - */ -#define should_false(a) if (a) { return test::SKIP; } -/** - * a should be empty - */ -#define should_empty(a) if (!a.empty()) { return test::SKIP; } -/** - * a should be not empty - */ -#define should_not_empty(a) if (a.empty()) { return test::SKIP; } - -} - -#endif // TEST_HPP diff --git a/tests/tests.cpp b/tests/tests.cpp new file mode 100644 index 0000000..0c025f0 --- /dev/null +++ b/tests/tests.cpp @@ -0,0 +1,30 @@ +/** + * MIT License + * + * Copyright (c) 2024-Present Kevin Traini + * + * 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. + */ +#include +#include +#include + +TEST(enquirer, auth) { + GTEST_FATAL_FAILURE_("Not implemented yet"); +}