├── .gitignore ├── src ├── inquirer.cpp ├── main.cpp └── inquirer.h ├── assets ├── int-input.png ├── password.png ├── regex-input.png ├── text-input.png ├── yesNo-input.png ├── confirm-input.png └── options-input.png ├── CMakeLists.txt ├── LICENSE ├── .clang-format ├── readme.md └── .clang-tidy /.gitignore: -------------------------------------------------------------------------------- 1 | /CMakeFiles 2 | /cmake-build-debug 3 | .idea -------------------------------------------------------------------------------- /src/inquirer.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Created by aelliixx on 2023-05-29. 3 | // 4 | -------------------------------------------------------------------------------- /assets/int-input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aelliixx/cpp-inquirer/HEAD/assets/int-input.png -------------------------------------------------------------------------------- /assets/password.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aelliixx/cpp-inquirer/HEAD/assets/password.png -------------------------------------------------------------------------------- /assets/regex-input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aelliixx/cpp-inquirer/HEAD/assets/regex-input.png -------------------------------------------------------------------------------- /assets/text-input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aelliixx/cpp-inquirer/HEAD/assets/text-input.png -------------------------------------------------------------------------------- /assets/yesNo-input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aelliixx/cpp-inquirer/HEAD/assets/yesNo-input.png -------------------------------------------------------------------------------- /assets/confirm-input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aelliixx/cpp-inquirer/HEAD/assets/confirm-input.png -------------------------------------------------------------------------------- /assets/options-input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aelliixx/cpp-inquirer/HEAD/assets/options-input.png -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.25) 2 | project(cpp_inquirer) 3 | set(CMAKE_CXX_STANDARD 11) 4 | 5 | add_library(Inquirer src/inquirer.cpp) 6 | 7 | add_executable(cpp_inquirer src/main.cpp) 8 | 9 | target_link_libraries(${PROJECT_NAME} Inquirer) 10 | target_include_directories(${PROJECT_NAME} PRIVATE src) 11 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "inquirer.h" 3 | #include 4 | 5 | int main() 6 | { 7 | auto inquirer = alx::Inquirer("cpp-inquirer example"); 8 | inquirer.add_question({ "query", "What do you want to do?" }); 9 | inquirer.add_question({ "birthday", "Is this for a birthday?", alx::Type::yesNo }); 10 | inquirer.add_question({ "candles", "How many candles do you want?", alx::Type::integer }); 11 | inquirer.add_question({ "type", "What kind of a cake would you like?", 12 | std::vector{ "Chocolate", "Ice-cream", "Cheesecake", "Red velvet" }}); 13 | inquirer.add_question({ "delivery", "Is this for delivery?", alx::Type::confirm }); 14 | inquirer.add_question({ "number", "Enter your contact details", "\\d{9}" }); 15 | inquirer.add_question({"password", "Enter your password", alx::Type::password}); 16 | inquirer.ask(); 17 | std::cout << "------------\n"; 18 | inquirer.print_questions(); 19 | inquirer.print_answers(); 20 | return 0; 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 aelliixx 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | # Generated from CLion C/C++ Code Style settings 2 | BasedOnStyle: Mozilla 3 | AccessModifierOffset: -4 4 | AlignAfterOpenBracket: Align 5 | AlignConsecutiveAssignments: None 6 | AlignOperands: DontAlign 7 | AllowAllArgumentsOnNextLine: true 8 | AllowAllParametersOfDeclarationOnNextLine: true 9 | AllowShortFunctionsOnASingleLine: All 10 | AllowShortBlocksOnASingleLine: Always 11 | AllowShortCaseLabelsOnASingleLine: false 12 | AllowShortIfStatementsOnASingleLine: Never 13 | AllowShortLambdasOnASingleLine: All 14 | AllowShortLoopsOnASingleLine: true 15 | PenaltyReturnTypeOnItsOwnLine: 100000000 16 | AlwaysBreakAfterDefinitionReturnType: None 17 | AlwaysBreakAfterReturnType: None 18 | AlwaysBreakTemplateDeclarations: Yes 19 | BreakBeforeBraces: Custom 20 | BreakConstructorInitializersBeforeComma: false 21 | BraceWrapping: 22 | AfterCaseLabel: false 23 | AfterClass: true 24 | AfterControlStatement: MultiLine 25 | AfterEnum: true 26 | AfterFunction: true 27 | AfterNamespace: false 28 | AfterUnion: true 29 | BeforeCatch: true 30 | BeforeElse: true 31 | IndentBraces: false 32 | SplitEmptyFunction: false 33 | SplitEmptyRecord: true 34 | BreakBeforeBinaryOperators: NonAssignment 35 | BreakBeforeTernaryOperators: false 36 | BreakConstructorInitializers: BeforeColon 37 | BreakInheritanceList: BeforeColon 38 | ColumnLimit: 120 39 | CompactNamespaces: false 40 | ContinuationIndentWidth: 4 41 | IndentCaseLabels: false 42 | IndentPPDirectives: None 43 | IndentWidth: 4 44 | KeepEmptyLinesAtTheStartOfBlocks: true 45 | MaxEmptyLinesToKeep: 2 46 | LambdaBodyIndentation: OuterScope 47 | NamespaceIndentation: None 48 | ObjCSpaceAfterProperty: false 49 | ObjCSpaceBeforeProtocolList: true 50 | PackConstructorInitializers: CurrentLine 51 | PointerAlignment: Left 52 | QualifierAlignment: Left 53 | ReferenceAlignment: Left 54 | SortUsingDeclarations: LexicographicNumeric 55 | AlignTrailingComments: Always 56 | ReflowComments: true 57 | SpacesBeforeTrailingComments: 1 58 | SpacesInLineCommentPrefix: 59 | Minimum: 1 60 | Maximum: 1 61 | SpaceAfterCStyleCast: false 62 | SpaceAfterLogicalNot: false 63 | SpaceAfterTemplateKeyword: false 64 | SpaceBeforeAssignmentOperators: true 65 | SpaceBeforeCpp11BracedList: false 66 | SpaceBeforeCtorInitializerColon: true 67 | SpaceBeforeInheritanceColon: true 68 | SpaceBeforeParens: ControlStatements 69 | SpaceBeforeRangeBasedForLoopColon: true 70 | SpaceInEmptyParentheses: false 71 | SpacesInAngles: false 72 | SpacesInCStyleCastParentheses: false 73 | SpacesInContainerLiterals: false 74 | SpacesInParentheses: false 75 | SpacesInSquareBrackets: false 76 | IncludeBlocks: Preserve 77 | SortIncludes: Never 78 | TabWidth: 4 79 | UseTab: ForContinuationAndIndentation 80 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # C++ Inquirer 2 | 3 | A single header C++11 library for interactive command line prompts. 4 | 5 | # Use 6 | 7 | Simply include the header: 8 | ```cpp 9 | #include "inquirer.h" 10 | ``` 11 | And initialise the inquirer object (with optional title): 12 | ```cpp 13 | auto inquirer = alx::Inquirer("cpp-inquirer example"); 14 | ``` 15 | There are two ways to get answers back: 16 | 17 | ```cpp 18 | // Adds a question. 19 | inquirer.add_question({"query", "What do you want to do?" }); 20 | // Prompts the user with all questions at once. 21 | inquirer.ask(); 22 | /* With the key of your question. You will get a std::string as a value, so you will need to convert 23 | it to an appropriate type yourself. */ 24 | inquirer.answer("query"); 25 | ``` 26 | or 27 | 28 | ```cpp 29 | // Adds a question and immediately prompts the user for an answer. 30 | std::string answer = inquirer.add_question({ "query", "What do you want to do?" }).ask(); 31 | ``` 32 | 33 | If you mix both of these approaches the question already answered will not be asked again. 34 | If you wish to ask the same question more than once call: 35 | 36 | ```cpp 37 | inquirer.ask(true); 38 | ``` 39 | 40 | Alternatively, if you have unrelated question or prefer to manage them individually you can use 41 | the `alx::Question` class directly: 42 | 43 | ```cpp 44 | std::string answer = alx::Question{"key", "What do you want to do?"}.ask(); // Key is not used in this case, so any value will do. 45 | // Or 46 | alx::Question question{"key", "What do you want to do?"}; 47 | std::string answer = question.ask(); 48 | ``` 49 | 50 | # Prompts 51 | 52 | ## Text 53 | 54 | ![](assets/text-input.png) 55 | 56 | ```cpp 57 | inquirer.add_question({ "query", "What do you want to do?" }); 58 | ``` 59 | 60 | ## Yes/No 61 | 62 | ![](assets/yesNo-input.png) 63 | 64 | ```cpp 65 | inquirer.add_question({ "birthday", "Is this for a birthday?", alx::Type::yesNo }); 66 | ``` 67 | ## Integer or decimal 68 | 69 | ![](assets/int-input.png) 70 | 71 | ```cpp 72 | inquirer.add_question({ "candles", "How many candles do you want?", alx::Type::integer }); 73 | // Or 74 | inquirer.add_question({ "candles", "How many candles do you want?", alx::Type::decimal }); 75 | ``` 76 | 77 | ## Select 78 | 79 | ![](assets/options-input.png) 80 | 81 | ```cpp 82 | inquirer.add_question({ "type", "What kind of a cake would you like?", 83 | std::vector{ "Chocolate", "Ice-cream", "Cheesecake", "Red velvet" }}); 84 | ``` 85 | 86 | ## Confirm 87 | 88 | ![](assets/confirm-input.png) 89 | 90 | ```cpp 91 | inquirer.add_question({ "delivery", "Is this for delivery?", alx::Type::confirm }); 92 | ``` 93 | 94 | ## Regex validated input 95 | 96 | ![](assets/regex-input.png) 97 | 98 | ```cpp 99 | inquirer.add_question({ "number", "Enter your contact details:", "\\d{9}" }); 100 | ``` 101 | 102 | ## Password 103 | 104 | Password questions hide user input. 105 | 106 | ![](assets/password.png) 107 | ```cpp 108 | inquirer.add_question({ "password", "Enter your password:", alx::Type::password }); 109 | ``` 110 | You can keep asking for a password if it doesn't meet your criteria like this: 111 | 112 | ```cpp 113 | std::string pass; 114 | alx::Question question{ "pass", "Password", alx::Type::password }; 115 | while (pass.empty()) // Your criteria here 116 | pass = question.ask(true); 117 | ``` 118 | This works for any other question type as well but would probably be most commonly used with passwords. 119 | 120 | # Contributing 121 | 122 | I'm more than happy to accept pull requests with some minor requirements: 123 | 124 | - Prefix your commit messages with a relevant change type. If the commit relates to a GitHub issue, include the issue number in the message. Example: 125 | > Types: added a new question type 'yes/no' 126 | > 127 | > This addresses issue #123 128 | 129 | or 130 | 131 | > Answers: added a way to return an answer with the correct type 132 | 133 | - Please follow the naming convention already established in the code. There is no clangformat, but I'm sure you can deduce it yourself. 134 | -------------------------------------------------------------------------------- /.clang-tidy: -------------------------------------------------------------------------------- 1 | # Generated from CLion Inspection settings 2 | --- 3 | Checks: '-*, 4 | bugprone-argument-comment, 5 | bugprone-assert-side-effect, 6 | bugprone-bad-signal-to-kill-thread, 7 | bugprone-branch-clone, 8 | bugprone-copy-constructor-init, 9 | bugprone-dangling-handle, 10 | bugprone-dynamic-static-initializers, 11 | bugprone-fold-init-type, 12 | bugprone-forward-declaration-namespace, 13 | bugprone-forwarding-reference-overload, 14 | bugprone-inaccurate-erase, 15 | bugprone-incorrect-roundings, 16 | bugprone-integer-division, 17 | bugprone-lambda-function-name, 18 | bugprone-macro-parentheses, 19 | bugprone-macro-repeated-side-effects, 20 | bugprone-misplaced-operator-in-strlen-in-alloc, 21 | bugprone-misplaced-pointer-arithmetic-in-alloc, 22 | bugprone-misplaced-widening-cast, 23 | bugprone-move-forwarding-reference, 24 | bugprone-multiple-statement-macro, 25 | bugprone-no-escape, 26 | bugprone-not-null-terminated-result, 27 | bugprone-parent-virtual-call, 28 | bugprone-posix-return, 29 | bugprone-reserved-identifier, 30 | bugprone-sizeof-container, 31 | bugprone-sizeof-expression, 32 | bugprone-spuriously-wake-up-functions, 33 | bugprone-string-constructor, 34 | bugprone-string-integer-assignment, 35 | bugprone-string-literal-with-embedded-nul, 36 | bugprone-suspicious-enum-usage, 37 | bugprone-suspicious-include, 38 | bugprone-suspicious-memset-usage, 39 | bugprone-suspicious-missing-comma, 40 | bugprone-suspicious-semicolon, 41 | bugprone-suspicious-string-compare, 42 | bugprone-suspicious-memory-comparison, 43 | bugprone-suspicious-realloc-usage, 44 | bugprone-swapped-arguments, 45 | bugprone-terminating-continue, 46 | bugprone-throw-keyword-missing, 47 | bugprone-too-small-loop-variable, 48 | bugprone-undefined-memory-manipulation, 49 | bugprone-undelegated-constructor, 50 | bugprone-unhandled-self-assignment, 51 | bugprone-unused-raii, 52 | bugprone-unused-return-value, 53 | bugprone-use-after-move, 54 | bugprone-virtual-near-miss, 55 | cert-dcl21-cpp, 56 | cert-dcl58-cpp, 57 | cert-err34-c, 58 | cert-err52-cpp, 59 | cert-err60-cpp, 60 | cert-flp30-c, 61 | cert-msc50-cpp, 62 | cert-msc51-cpp, 63 | cert-str34-c, 64 | cppcoreguidelines-interfaces-global-init, 65 | cppcoreguidelines-narrowing-conversions, 66 | cppcoreguidelines-pro-type-member-init, 67 | cppcoreguidelines-pro-type-static-cast-downcast, 68 | cppcoreguidelines-slicing, 69 | google-default-arguments, 70 | google-explicit-constructor, 71 | google-runtime-operator, 72 | hicpp-exception-baseclass, 73 | hicpp-multiway-paths-covered, 74 | misc-misplaced-const, 75 | misc-new-delete-overloads, 76 | misc-no-recursion, 77 | misc-non-copyable-objects, 78 | misc-throw-by-value-catch-by-reference, 79 | misc-unconventional-assign-operator, 80 | misc-uniqueptr-reset-release, 81 | modernize-avoid-bind, 82 | modernize-concat-nested-namespaces, 83 | modernize-deprecated-headers, 84 | modernize-deprecated-ios-base-aliases, 85 | modernize-loop-convert, 86 | modernize-make-shared, 87 | modernize-make-unique, 88 | modernize-pass-by-value, 89 | modernize-raw-string-literal, 90 | modernize-redundant-void-arg, 91 | modernize-replace-auto-ptr, 92 | modernize-replace-disallow-copy-and-assign-macro, 93 | modernize-replace-random-shuffle, 94 | modernize-return-braced-init-list, 95 | modernize-shrink-to-fit, 96 | modernize-unary-static-assert, 97 | modernize-use-auto, 98 | modernize-use-bool-literals, 99 | modernize-use-emplace, 100 | modernize-use-equals-default, 101 | modernize-use-equals-delete, 102 | modernize-use-nodiscard, 103 | modernize-use-noexcept, 104 | modernize-use-nullptr, 105 | modernize-use-override, 106 | modernize-use-transparent-functors, 107 | modernize-use-uncaught-exceptions, 108 | mpi-buffer-deref, 109 | mpi-type-mismatch, 110 | openmp-use-default-none, 111 | performance-faster-string-find, 112 | performance-for-range-copy, 113 | performance-implicit-conversion-in-loop, 114 | performance-inefficient-algorithm, 115 | performance-inefficient-string-concatenation, 116 | performance-inefficient-vector-operation, 117 | performance-move-const-arg, 118 | performance-move-constructor-init, 119 | performance-no-automatic-move, 120 | performance-noexcept-move-constructor, 121 | performance-trivially-destructible, 122 | performance-type-promotion-in-math-fn, 123 | performance-unnecessary-copy-initialization, 124 | performance-unnecessary-value-param, 125 | portability-simd-intrinsics, 126 | readability-avoid-const-params-in-decls, 127 | readability-const-return-type, 128 | readability-container-size-empty, 129 | readability-convert-member-functions-to-static, 130 | readability-delete-null-pointer, 131 | readability-deleted-default, 132 | readability-inconsistent-declaration-parameter-name, 133 | readability-make-member-function-const, 134 | readability-misleading-indentation, 135 | readability-misplaced-array-index, 136 | readability-non-const-parameter, 137 | readability-redundant-control-flow, 138 | readability-redundant-declaration, 139 | readability-redundant-function-ptr-dereference, 140 | readability-redundant-smartptr-get, 141 | readability-redundant-string-cstr, 142 | readability-redundant-string-init, 143 | readability-simplify-subscript-expr, 144 | readability-static-accessed-through-instance, 145 | readability-static-definition-in-anonymous-namespace, 146 | readability-string-compare, 147 | readability-uniqueptr-delete-release, 148 | readability-use-anyofallof' -------------------------------------------------------------------------------- /src/inquirer.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2023-2024 Donatas Mockus 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 4 | // documentation files (the “Software”), to deal in the Software without restriction, including without limitation 5 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 6 | // to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | // 8 | // The above copyright notice and this permission notice shall be included in all copies or substantial portions of 9 | // the Software. 10 | // 11 | // THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 12 | // THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 13 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 14 | // TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | 17 | #ifndef CPP_INQUIRER_SRC_INQUIRER_H 18 | #define CPP_INQUIRER_SRC_INQUIRER_H 19 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | #ifdef _WIN32 28 | #include 29 | #endif 30 | 31 | namespace alx { 32 | 33 | #define CTRL_KEYPRESS(k) ((k) & 0x1f) 34 | 35 | #ifdef _WIN32 36 | static int const keyDw = 80; 37 | static int const keyUp = 72; 38 | static int const keySx = 75; 39 | static int const keyDx = 77; 40 | static int const keyEnter = 13; 41 | #else 42 | static int const keyUp = 65; 43 | static int const keyDw = 66; 44 | static int const keySx = 68; 45 | static int const keyDx = 67; 46 | static int const keyEnter = 13; 47 | #endif 48 | static int const keyBackspace = 127; // TODO: is this also correct on Windows and other platforms? 49 | 50 | class Inquirer; 51 | 52 | enum class Type 53 | { 54 | text, 55 | integer, 56 | decimal, 57 | yesNo, 58 | confirm, 59 | options, 60 | regex, 61 | password 62 | }; 63 | 64 | class Question 65 | { 66 | public: 67 | Question(const Question& q) 68 | { 69 | m_key = q.m_key; 70 | m_question = q.m_question; 71 | m_answer = q.m_answer; 72 | m_type = q.m_type; 73 | if (m_type == Type::options) 74 | m_options = q.m_options; 75 | else if (m_type == Type::regex) 76 | m_regex = q.m_regex; 77 | } 78 | 79 | Question(std::string key, std::string question, const Type type = Type::text) 80 | : m_key(std::move(key)), 81 | m_question(std::move(question)), 82 | m_type(type) {} 83 | 84 | Question(std::string key, std::string question, std::vector options) 85 | : m_key(std::move(key)), 86 | m_question(std::move(question)), 87 | m_type(Type::options), 88 | m_options(std::move(options)) 89 | { 90 | if (m_options.empty()) 91 | throw std::runtime_error("Must have one or more options"); 92 | } 93 | 94 | Question(std::string key, std::string question, std::string regex) 95 | : m_key(std::move(key)), 96 | m_question(std::move(question)), 97 | m_type(Type::regex), 98 | m_regex(std::move(regex)) {} 99 | 100 | [[nodiscard]] std::string ask(const bool askAgainIfAnswered = false) 101 | { 102 | if (m_asked && !askAgainIfAnswered) return m_answer; 103 | auto printQuestion = [&](const std::string& append = "") 104 | { 105 | std::cout << "\033[1m\033[34m?\033[0m \033[1m" << m_question << "\033[0m " << append; 106 | }; 107 | 108 | auto takeInput = [](std::string& destination) 109 | { 110 | std::cout << "\033[34m"; 111 | if (!std::getline(std::cin, destination)) 112 | exit(0); 113 | std::cout << "\033[0m"; 114 | }; 115 | 116 | switch (m_type) 117 | { 118 | case Type::confirm: 119 | { 120 | printQuestion("(y/N) "); 121 | std::string answer; 122 | takeInput(answer); 123 | while (!(answer == "y" || answer == "Y" || answer == "n" || answer == "N")) 124 | { 125 | erase_lines(2); 126 | printQuestion("(y/N) "); 127 | takeInput(answer); 128 | } 129 | m_answer = answer; 130 | break; 131 | } 132 | case Type::text: 133 | printQuestion(); 134 | takeInput(m_answer); 135 | break; 136 | case Type::integer: 137 | { 138 | printQuestion(); 139 | std::string answer; 140 | takeInput(answer); 141 | while (!is_integer(answer)) 142 | { 143 | erase_lines(2); 144 | printQuestion(); 145 | takeInput(answer); 146 | } 147 | m_answer = answer; 148 | break; 149 | } 150 | case Type::decimal: 151 | { 152 | printQuestion(); 153 | std::string answer; 154 | takeInput(answer); 155 | while (!is_decimal(answer)) 156 | { 157 | erase_lines(2); 158 | printQuestion(); 159 | takeInput(answer); 160 | } 161 | m_answer = answer; 162 | break; 163 | } 164 | case Type::yesNo: 165 | { 166 | const std::string yes = "\033[34myes\033[0m no\n"; 167 | const std::string no = "yes \033[34mno\033[0m\n"; 168 | std::cout << std::flush; 169 | printQuestion(yes); 170 | bool position = true; 171 | while (true) 172 | { 173 | const int key = getch(); 174 | if (key == keySx) 175 | { 176 | position = true; 177 | erase_lines(2); 178 | printQuestion(yes); 179 | } 180 | else if (key == keyDx) 181 | { 182 | position = false; 183 | erase_lines(2); 184 | printQuestion(no); 185 | } 186 | if (key == keyEnter) 187 | { 188 | m_answer = position ? "yes" : "no"; 189 | break; 190 | } 191 | } 192 | break; 193 | } 194 | case Type::options: 195 | { 196 | unsigned int selectedIndex = 0; 197 | auto printOptions = [&]() 198 | { 199 | std::cout << '\n'; 200 | for (int i = 0; i < m_options.size(); ++i) 201 | { 202 | if (i == selectedIndex) 203 | std::cout << "\033[34m> " << m_options.at(i) << "\033[0m\n"; 204 | else 205 | std::cout << " " << m_options.at(i) << "\n"; 206 | } 207 | }; 208 | printQuestion(); 209 | printOptions(); 210 | 211 | while (true) 212 | { 213 | const int key = getch(); 214 | if (key == keyDw) 215 | { 216 | selectedIndex = wrap_int(selectedIndex + 1, 0, m_options.size() - 1); 217 | erase_lines(m_options.size() + 2); 218 | printQuestion(); 219 | printOptions(); 220 | } 221 | else if (key == keyUp) 222 | { 223 | selectedIndex = wrap_int(selectedIndex - 1, 0, m_options.size() - 1); 224 | erase_lines(m_options.size() + 2); 225 | printQuestion(); 226 | printOptions(); 227 | } 228 | if (key == keyEnter) 229 | { 230 | m_answer = m_options.at(selectedIndex); 231 | erase_lines(m_options.size() + 2); 232 | printQuestion("\033[34m" + m_options.at(selectedIndex) + "\033[0m\n"); 233 | break; 234 | } 235 | } 236 | break; 237 | } 238 | case Type::password: 239 | { 240 | printQuestion(); 241 | std::string answer; 242 | int c; 243 | while (true) 244 | { 245 | c = getch(); 246 | if (c == keyEnter) 247 | break; 248 | char character = static_cast(c); 249 | if (c == keyBackspace) 250 | { 251 | if (!answer.empty()) 252 | answer = answer.substr(0, answer.length() - 1); 253 | continue; 254 | } 255 | answer += character; 256 | } 257 | std::cout << '\n'; 258 | m_answer = answer; 259 | break; 260 | } 261 | case Type::regex: 262 | { 263 | printQuestion(); 264 | std::string answer; 265 | takeInput(answer); 266 | while (!std::regex_match(answer, std::regex(m_regex))) 267 | { 268 | erase_lines(2); 269 | printQuestion(); 270 | takeInput(answer); 271 | } 272 | m_answer = answer; 273 | break; 274 | } 275 | } 276 | m_asked = true; 277 | return m_answer; 278 | } 279 | 280 | [[nodiscard]] std::string get_answer() const { return m_answer; } 281 | 282 | ~Question() {} 283 | 284 | private: 285 | friend Inquirer; 286 | std::string m_key; 287 | std::string m_question; 288 | std::string m_answer; 289 | Type m_type; 290 | std::vector m_options; 291 | std::string m_regex; 292 | bool m_asked = false; 293 | 294 | static void erase_lines(const unsigned count = 1) 295 | { 296 | if (count == 0) 297 | return; 298 | 299 | std::cout << "\x1b[2K"; // Delete current line 300 | for (int i = 1; i < count; ++i) 301 | { 302 | std::cout << "\x1b[1A" // Move cursor one line up 303 | << "\x1b[2K"; // Delete current line 304 | } 305 | std::cout << '\r'; 306 | } 307 | 308 | static bool is_integer(const std::string& string) 309 | { 310 | if (string.empty() || ((!isdigit(string[0])) && (string[0] != '-') && (string[0] != '+'))) return false; 311 | char* p; 312 | strtol(string.c_str(), &p, 10); 313 | return (*p == 0); 314 | } 315 | 316 | static bool is_decimal(const std::string& string) 317 | { 318 | if (string.empty() || ((!isdigit(string[0])) && (string[0] != '-') && (string[0] != '+'))) return false; 319 | char* p; 320 | strtod(string.c_str(), &p); 321 | return (*p == 0); 322 | } 323 | 324 | static unsigned wrap_int(unsigned int k, const unsigned lowerBound, const unsigned upperBound) 325 | { 326 | const unsigned range_size = upperBound - lowerBound + 1; 327 | 328 | if (k < lowerBound) 329 | k += range_size * ((lowerBound - k) / range_size + 1); 330 | 331 | return lowerBound + (k - lowerBound) % range_size; 332 | } 333 | 334 | static int getch() 335 | { 336 | int c; // This function should return the keystroke without allowing it to echo on screen 337 | 338 | #ifdef _WIN32 339 | c = _getch(); 340 | #else 341 | system("stty raw"); // Raw input - wait for only a single keystroke 342 | system("stty -echo"); // Echo off 343 | c = getchar(); 344 | system("stty cooked"); // Cooked input - reset 345 | system("stty echo"); // Echo on - Reset 346 | #endif 347 | if (c == CTRL_KEYPRESS('c') || c == CTRL_KEYPRESS('d')) 348 | exit(0); 349 | return c; 350 | } 351 | }; 352 | 353 | class Inquirer 354 | { 355 | std::vector> m_questions; 356 | std::string m_title; 357 | 358 | public: 359 | explicit Inquirer(std::string title) 360 | : m_title(std::move(title)) {} 361 | 362 | Question& add_question(const Question& question) 363 | { 364 | m_questions.emplace_back(question.m_key, question); 365 | return m_questions.at(m_questions.size() - 1).second; 366 | } 367 | 368 | Question& get_question(const std::string key) 369 | { 370 | auto found = std::find_if(m_questions.begin(), m_questions.end(), [&](std::pair, 371 | Question>& question){ 372 | if (question.second.m_key == key) 373 | return true; 374 | return false; 375 | }); 376 | if (found != m_questions.end()) 377 | return found->second; 378 | throw std::runtime_error("No question with key: " + key); 379 | } 380 | 381 | void ask(const bool askAgainIfAnswered = false) 382 | { 383 | if (!m_title.empty()) 384 | std::cout << "\033[34m>\033[0m " << m_title << '\n'; 385 | for (auto& question : m_questions) 386 | { 387 | (void)question.second.ask(askAgainIfAnswered); 388 | } 389 | } 390 | 391 | void print_questions() const 392 | { 393 | for (const auto& q : m_questions) 394 | { 395 | std::cout << "Key: " << q.second.m_key << ", question: " << q.second.m_question << ", type: "; 396 | switch (q.second.m_type) 397 | { 398 | case Type::text: 399 | std::cout << "Text\n"; 400 | break; 401 | case Type::integer: 402 | std::cout << "Integer\n"; 403 | break; 404 | case Type::decimal: 405 | std::cout << "Decimal\n"; 406 | break; 407 | case Type::yesNo: 408 | std::cout << "Yes/No\n"; 409 | break; 410 | case Type::confirm: 411 | std::cout << "Confirm\n"; 412 | break; 413 | case Type::options: 414 | std::cout << "Options\n"; 415 | break; 416 | case Type::regex: 417 | std::cout << "Regex\n"; 418 | break; 419 | case Type::password: 420 | std::cout << "Password\n"; 421 | break; 422 | } 423 | } 424 | } 425 | 426 | void print_answers() const 427 | { 428 | for (const auto& q : m_questions) 429 | std::cout << q.second.m_question << ": " << q.second.m_answer << '\n'; 430 | } 431 | 432 | std::string answer(const std::string& key) const 433 | { 434 | for (const auto& question : m_questions) 435 | if (question.first == key) 436 | return question.second.m_answer; 437 | return ""; 438 | } 439 | }; 440 | } 441 | #endif //CPP_INQUIRER_SRC_INQUIRER_H 442 | --------------------------------------------------------------------------------