├── .editorconfig ├── .gitignore ├── ConsoleMenu.sln ├── ConsoleMenu ├── CloseTrigger.cs ├── ConsoleMenu.cs ├── ConsoleMenu.csproj ├── ConsoleMenuDisplay.cs ├── Extensions.cs ├── IConsole.cs ├── ItemsCollection.cs ├── MenuConfig.cs ├── MenuItem.cs ├── Properties │ └── PublishProfiles │ │ └── FolderProfile.pubxml ├── PublicAPI.Shipped.txt ├── PublicAPI.Unshipped.txt ├── SystemConsole.cs ├── VisibilityManager.cs └── stylecop.json ├── ConsoleMenuSampleApp ├── ConsoleMenuSampleApp.csproj └── Program.cs ├── ConsoleMenuTests ├── BreadcrumbsScenarioTest.cs ├── ConsoleMenuTests.csproj ├── ExtensionsTest.cs ├── FilteringScenarioTest.cs ├── PreSelectionScenarioTest.cs ├── ReentrySubmenuScenarioTest.cs ├── SimpleScenarioTest.cs ├── TestConsole.cs └── TestHelpers │ ├── AssertHelper.cs │ └── ConfigHelper.cs ├── LICENSE.txt ├── README.md └── preview.gif /.editorconfig: -------------------------------------------------------------------------------- 1 | # To learn more about .editorconfig see https://aka.ms/editorconfigdocs 2 | ############################### 3 | # Core EditorConfig Options # 4 | ############################### 5 | root = true 6 | # All files 7 | [*] 8 | indent_style = space 9 | 10 | # XML project files 11 | [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] 12 | indent_size = 2 13 | 14 | # XML config files 15 | [*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] 16 | indent_size = 2 17 | 18 | # Code files 19 | [*.{cs,csx,vb,vbx}] 20 | indent_size = 2 21 | insert_final_newline = true 22 | charset = utf-8-bom 23 | ############################### 24 | # .NET Coding Conventions # 25 | ############################### 26 | [*.{cs,vb}] 27 | # Organize usings 28 | dotnet_sort_system_directives_first = true 29 | # this. preferences 30 | dotnet_style_qualification_for_field = false:silent 31 | dotnet_style_qualification_for_property = false:silent 32 | dotnet_style_qualification_for_method = false:silent 33 | dotnet_style_qualification_for_event = false:silent 34 | # Language keywords vs BCL types preferences 35 | dotnet_style_predefined_type_for_locals_parameters_members = true:silent 36 | dotnet_style_predefined_type_for_member_access = true:silent 37 | # Parentheses preferences 38 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent 39 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent 40 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent 41 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent 42 | # Modifier preferences 43 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent 44 | dotnet_style_readonly_field = true:suggestion 45 | # Expression-level preferences 46 | dotnet_style_object_initializer = true:suggestion 47 | dotnet_style_collection_initializer = true:suggestion 48 | dotnet_style_explicit_tuple_names = true:suggestion 49 | dotnet_style_null_propagation = true:suggestion 50 | dotnet_style_coalesce_expression = true:suggestion 51 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent 52 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 53 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion 54 | dotnet_style_prefer_auto_properties = true:silent 55 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent 56 | dotnet_style_prefer_conditional_expression_over_return = true:silent 57 | ############################### 58 | # Naming Conventions # 59 | ############################### 60 | # Style Definitions 61 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 62 | # Use PascalCase for constant fields 63 | dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion 64 | dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields 65 | dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style 66 | dotnet_naming_symbols.constant_fields.applicable_kinds = field 67 | dotnet_naming_symbols.constant_fields.applicable_accessibilities = * 68 | dotnet_naming_symbols.constant_fields.required_modifiers = const 69 | tab_width= 2 70 | dotnet_style_operator_placement_when_wrapping = beginning_of_line 71 | end_of_line = lf 72 | dotnet_style_prefer_simplified_boolean_expressions = true:suggestion 73 | ############################### 74 | # C# Coding Conventions # 75 | ############################### 76 | [*.cs] 77 | # var preferences 78 | csharp_style_var_for_built_in_types = true:silent 79 | csharp_style_var_when_type_is_apparent = true:silent 80 | csharp_style_var_elsewhere = true:silent 81 | # Expression-bodied members 82 | csharp_style_expression_bodied_methods = false:silent 83 | csharp_style_expression_bodied_constructors = false:silent 84 | csharp_style_expression_bodied_operators = false:silent 85 | csharp_style_expression_bodied_properties = true:silent 86 | csharp_style_expression_bodied_indexers = true:silent 87 | csharp_style_expression_bodied_accessors = true:silent 88 | # Pattern matching preferences 89 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 90 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 91 | # Null-checking preferences 92 | csharp_style_throw_expression = true:suggestion 93 | csharp_style_conditional_delegate_call = true:suggestion 94 | # Modifier preferences 95 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion 96 | # Expression-level preferences 97 | csharp_prefer_braces = true:silent 98 | csharp_style_deconstructed_variable_declaration = true:suggestion 99 | csharp_prefer_simple_default_expression = true:suggestion 100 | csharp_style_pattern_local_over_anonymous_function = true:suggestion 101 | csharp_style_inlined_variable_declaration = true:suggestion 102 | ############################### 103 | # C# Formatting Rules # 104 | ############################### 105 | # New line preferences 106 | csharp_new_line_before_open_brace = all 107 | csharp_new_line_before_else = true 108 | csharp_new_line_before_catch = true 109 | csharp_new_line_before_finally = true 110 | csharp_new_line_before_members_in_object_initializers = true 111 | csharp_new_line_before_members_in_anonymous_types = true 112 | csharp_new_line_between_query_expression_clauses = true 113 | # Indentation preferences 114 | csharp_indent_case_contents = true 115 | csharp_indent_switch_labels = true 116 | csharp_indent_labels = flush_left 117 | # Space preferences 118 | csharp_space_after_cast = false 119 | csharp_space_after_keywords_in_control_flow_statements = true 120 | csharp_space_between_method_call_parameter_list_parentheses = false 121 | csharp_space_between_method_declaration_parameter_list_parentheses = false 122 | csharp_space_between_parentheses = false 123 | csharp_space_before_colon_in_inheritance_clause = true 124 | csharp_space_after_colon_in_inheritance_clause = true 125 | csharp_space_around_binary_operators = before_and_after 126 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 127 | csharp_space_between_method_call_name_and_opening_parenthesis = false 128 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 129 | # Wrapping preferences 130 | csharp_preserve_single_line_statements = true 131 | csharp_preserve_single_line_blocks = true 132 | csharp_style_namespace_declarations= file_scoped:warning 133 | csharp_using_directive_placement = outside_namespace:silent 134 | csharp_prefer_simple_using_statement = true:suggestion 135 | csharp_style_expression_bodied_lambdas = true:silent 136 | csharp_style_expression_bodied_local_functions = false:silent 137 | ############################### 138 | # VB Coding Conventions # 139 | ############################### 140 | 141 | # SA1633: File should have header 142 | dotnet_diagnostic.SA1633.severity = none 143 | 144 | # SA1122: Use string.Empty for empty strings 145 | dotnet_diagnostic.SA1122.severity = none 146 | 147 | # SA1405: Debug.Assert should provide message text 148 | dotnet_diagnostic.SA1405.severity = none 149 | csharp_style_prefer_method_group_conversion = true:silent 150 | csharp_style_prefer_top_level_statements = true:silent 151 | 152 | [*.vb] 153 | # Modifier preferences 154 | visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:suggestion 155 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/csharp,visualstudio,visualstudiocode 3 | 4 | ### Csharp ### 5 | ## Ignore Visual Studio temporary files, build results, and 6 | ## files generated by popular Visual Studio add-ons. 7 | ## 8 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 9 | 10 | # User-specific files 11 | *.suo 12 | *.user 13 | *.userosscache 14 | *.sln.docstates 15 | 16 | # User-specific files (MonoDevelop/Xamarin Studio) 17 | *.userprefs 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | bld/ 27 | [Bb]in/ 28 | [Oo]bj/ 29 | [Ll]og/ 30 | 31 | # Visual Studio 2015 cache/options directory 32 | .vs/ 33 | # Uncomment if you have tasks that create the project's static files in wwwroot 34 | #wwwroot/ 35 | 36 | # MSTest test Results 37 | [Tt]est[Rr]esult*/ 38 | [Bb]uild[Ll]og.* 39 | 40 | # NUNIT 41 | *.VisualState.xml 42 | TestResult.xml 43 | 44 | # Build Results of an ATL Project 45 | [Dd]ebugPS/ 46 | [Rr]eleasePS/ 47 | dlldata.c 48 | 49 | # .NET Core 50 | project.lock.json 51 | project.fragment.lock.json 52 | artifacts/ 53 | **/Properties/launchSettings.json 54 | 55 | *_i.c 56 | *_p.c 57 | *_i.h 58 | *.ilk 59 | *.meta 60 | *.obj 61 | *.pch 62 | *.pdb 63 | *.pgc 64 | *.pgd 65 | *.rsp 66 | *.sbr 67 | *.tlb 68 | *.tli 69 | *.tlh 70 | *.tmp 71 | *.tmp_proj 72 | *.log 73 | *.vspscc 74 | *.vssscc 75 | .builds 76 | *.pidb 77 | *.svclog 78 | *.scc 79 | 80 | # Chutzpah Test files 81 | _Chutzpah* 82 | 83 | # Visual C++ cache files 84 | ipch/ 85 | *.aps 86 | *.ncb 87 | *.opendb 88 | *.opensdf 89 | *.sdf 90 | *.cachefile 91 | *.VC.db 92 | *.VC.VC.opendb 93 | 94 | # Visual Studio profiler 95 | *.psess 96 | *.vsp 97 | *.vspx 98 | *.sap 99 | 100 | # TFS 2012 Local Workspace 101 | $tf/ 102 | 103 | # Guidance Automation Toolkit 104 | *.gpState 105 | 106 | # ReSharper is a .NET coding add-in 107 | _ReSharper*/ 108 | *.[Rr]e[Ss]harper 109 | *.DotSettings.user 110 | 111 | # JustCode is a .NET coding add-in 112 | .JustCode 113 | 114 | # TeamCity is a build add-in 115 | _TeamCity* 116 | 117 | # DotCover is a Code Coverage Tool 118 | *.dotCover 119 | 120 | # Visual Studio code coverage results 121 | *.coverage 122 | *.coveragexml 123 | 124 | # NCrunch 125 | _NCrunch_* 126 | .*crunch*.local.xml 127 | nCrunchTemp_* 128 | 129 | # MightyMoose 130 | *.mm.* 131 | AutoTest.Net/ 132 | 133 | # Web workbench (sass) 134 | .sass-cache/ 135 | 136 | # Installshield output folder 137 | [Ee]xpress/ 138 | 139 | # DocProject is a documentation generator add-in 140 | DocProject/buildhelp/ 141 | DocProject/Help/*.HxT 142 | DocProject/Help/*.HxC 143 | DocProject/Help/*.hhc 144 | DocProject/Help/*.hhk 145 | DocProject/Help/*.hhp 146 | DocProject/Help/Html2 147 | DocProject/Help/html 148 | 149 | # Click-Once directory 150 | publish/ 151 | 152 | # Publish Web Output 153 | *.[Pp]ublish.xml 154 | *.azurePubxml 155 | # TODO: Uncomment the next line to ignore your web deploy settings. 156 | # By default, sensitive information, such as encrypted password 157 | # should be stored in the .pubxml.user file. 158 | #*.pubxml 159 | *.pubxml.user 160 | *.publishproj 161 | 162 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 163 | # checkin your Azure Web App publish settings, but sensitive information contained 164 | # in these scripts will be unencrypted 165 | PublishScripts/ 166 | 167 | # NuGet Packages 168 | *.nupkg 169 | # The packages folder can be ignored because of Package Restore 170 | **/packages/* 171 | # except build/, which is used as an MSBuild target. 172 | !**/packages/build/ 173 | # Uncomment if necessary however generally it will be regenerated when needed 174 | #!**/packages/repositories.config 175 | # NuGet v3's project.json files produces more ignorable files 176 | *.nuget.props 177 | *.nuget.targets 178 | 179 | # Microsoft Azure Build Output 180 | csx/ 181 | *.build.csdef 182 | 183 | # Microsoft Azure Emulator 184 | ecf/ 185 | rcf/ 186 | 187 | # Windows Store app package directories and files 188 | AppPackages/ 189 | BundleArtifacts/ 190 | Package.StoreAssociation.xml 191 | _pkginfo.txt 192 | 193 | # Visual Studio cache files 194 | # files ending in .cache can be ignored 195 | *.[Cc]ache 196 | # but keep track of directories ending in .cache 197 | !*.[Cc]ache/ 198 | 199 | # Others 200 | ClientBin/ 201 | ~$* 202 | *~ 203 | *.dbmdl 204 | *.dbproj.schemaview 205 | *.jfm 206 | *.pfx 207 | *.publishsettings 208 | orleans.codegen.cs 209 | 210 | # Since there are multiple workflows, uncomment next line to ignore bower_components 211 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 212 | #bower_components/ 213 | 214 | # RIA/Silverlight projects 215 | Generated_Code/ 216 | 217 | # Backup & report files from converting an old project file 218 | # to a newer Visual Studio version. Backup files are not needed, 219 | # because we have git ;-) 220 | _UpgradeReport_Files/ 221 | Backup*/ 222 | UpgradeLog*.XML 223 | UpgradeLog*.htm 224 | 225 | # SQL Server files 226 | *.mdf 227 | *.ldf 228 | *.ndf 229 | 230 | # Business Intelligence projects 231 | *.rdl.data 232 | *.bim.layout 233 | *.bim_*.settings 234 | 235 | # Microsoft Fakes 236 | FakesAssemblies/ 237 | 238 | # GhostDoc plugin setting file 239 | *.GhostDoc.xml 240 | 241 | # Node.js Tools for Visual Studio 242 | .ntvs_analysis.dat 243 | node_modules/ 244 | 245 | # Typescript v1 declaration files 246 | typings/ 247 | 248 | # Visual Studio 6 build log 249 | *.plg 250 | 251 | # Visual Studio 6 workspace options file 252 | *.opt 253 | 254 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 255 | *.vbw 256 | 257 | # Visual Studio LightSwitch build output 258 | **/*.HTMLClient/GeneratedArtifacts 259 | **/*.DesktopClient/GeneratedArtifacts 260 | **/*.DesktopClient/ModelManifest.xml 261 | **/*.Server/GeneratedArtifacts 262 | **/*.Server/ModelManifest.xml 263 | _Pvt_Extensions 264 | 265 | # Paket dependency manager 266 | .paket/paket.exe 267 | paket-files/ 268 | 269 | # FAKE - F# Make 270 | .fake/ 271 | 272 | # JetBrains Rider 273 | .idea/ 274 | *.sln.iml 275 | 276 | # CodeRush 277 | .cr/ 278 | 279 | # Python Tools for Visual Studio (PTVS) 280 | __pycache__/ 281 | *.pyc 282 | 283 | # Cake - Uncomment if you are using it 284 | # tools/** 285 | # !tools/packages.config 286 | 287 | # Telerik's JustMock configuration file 288 | *.jmconfig 289 | 290 | # BizTalk build output 291 | *.btp.cs 292 | *.btm.cs 293 | *.odx.cs 294 | *.xsd.cs 295 | 296 | ### VisualStudioCode ### 297 | .vscode/* 298 | !.vscode/settings.json 299 | !.vscode/tasks.json 300 | !.vscode/launch.json 301 | !.vscode/extensions.json 302 | .history 303 | 304 | ### VisualStudio ### 305 | ## Ignore Visual Studio temporary files, build results, and 306 | ## files generated by popular Visual Studio add-ons. 307 | ## 308 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 309 | 310 | # User-specific files 311 | 312 | # User-specific files (MonoDevelop/Xamarin Studio) 313 | 314 | # Build results 315 | 316 | # Visual Studio 2015 cache/options directory 317 | # Uncomment if you have tasks that create the project's static files in wwwroot 318 | #wwwroot/ 319 | 320 | # MSTest test Results 321 | 322 | # NUNIT 323 | 324 | # Build Results of an ATL Project 325 | 326 | # .NET Core 327 | 328 | 329 | # Chutzpah Test files 330 | 331 | # Visual C++ cache files 332 | 333 | # Visual Studio profiler 334 | 335 | # TFS 2012 Local Workspace 336 | 337 | # Guidance Automation Toolkit 338 | 339 | # ReSharper is a .NET coding add-in 340 | 341 | # JustCode is a .NET coding add-in 342 | 343 | # TeamCity is a build add-in 344 | 345 | # DotCover is a Code Coverage Tool 346 | 347 | # Visual Studio code coverage results 348 | 349 | # NCrunch 350 | 351 | # MightyMoose 352 | 353 | # Web workbench (sass) 354 | 355 | # Installshield output folder 356 | 357 | # DocProject is a documentation generator add-in 358 | 359 | # Click-Once directory 360 | 361 | # Publish Web Output 362 | # TODO: Uncomment the next line to ignore your web deploy settings. 363 | # By default, sensitive information, such as encrypted password 364 | # should be stored in the .pubxml.user file. 365 | #*.pubxml 366 | 367 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 368 | # checkin your Azure Web App publish settings, but sensitive information contained 369 | # in these scripts will be unencrypted 370 | 371 | # NuGet Packages 372 | # The packages folder can be ignored because of Package Restore 373 | # except build/, which is used as an MSBuild target. 374 | # Uncomment if necessary however generally it will be regenerated when needed 375 | #!**/packages/repositories.config 376 | # NuGet v3's project.json files produces more ignorable files 377 | 378 | # Microsoft Azure Build Output 379 | 380 | # Microsoft Azure Emulator 381 | 382 | # Windows Store app package directories and files 383 | 384 | # Visual Studio cache files 385 | # files ending in .cache can be ignored 386 | # but keep track of directories ending in .cache 387 | 388 | # Others 389 | 390 | # Since there are multiple workflows, uncomment next line to ignore bower_components 391 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 392 | #bower_components/ 393 | 394 | # RIA/Silverlight projects 395 | 396 | # Backup & report files from converting an old project file 397 | # to a newer Visual Studio version. Backup files are not needed, 398 | # because we have git ;-) 399 | 400 | # SQL Server files 401 | 402 | # Business Intelligence projects 403 | 404 | # Microsoft Fakes 405 | 406 | # GhostDoc plugin setting file 407 | 408 | # Node.js Tools for Visual Studio 409 | 410 | # Typescript v1 declaration files 411 | 412 | # Visual Studio 6 build log 413 | 414 | # Visual Studio 6 workspace options file 415 | 416 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 417 | 418 | # Visual Studio LightSwitch build output 419 | 420 | # Paket dependency manager 421 | 422 | # FAKE - F# Make 423 | 424 | # JetBrains Rider 425 | 426 | # CodeRush 427 | 428 | # Python Tools for Visual Studio (PTVS) 429 | 430 | # Cake - Uncomment if you are using it 431 | # tools/** 432 | # !tools/packages.config 433 | 434 | # Telerik's JustMock configuration file 435 | 436 | # BizTalk build output 437 | 438 | ### VisualStudio Patch ### 439 | # By default, sensitive information, such as encrypted password 440 | # should be stored in the .pubxml.user file. 441 | 442 | 443 | # End of https://www.gitignore.io/api/csharp,visualstudio,visualstudiocode -------------------------------------------------------------------------------- /ConsoleMenu.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31919.166 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A114A1AF-445A-4014-B664-52FA8DFC5B9D}" 7 | ProjectSection(SolutionItems) = preProject 8 | .editorconfig = .editorconfig 9 | .gitignore = .gitignore 10 | preview.gif = preview.gif 11 | README.md = README.md 12 | EndProjectSection 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleMenu", "ConsoleMenu\ConsoleMenu.csproj", "{A1653921-DC17-48FD-9C74-F62A0B23E742}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleMenuSampleApp", "ConsoleMenuSampleApp\ConsoleMenuSampleApp.csproj", "{6F5E57C6-45FE-4946-95FF-5B3259E08EB7}" 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleMenuTests", "ConsoleMenuTests\ConsoleMenuTests.csproj", "{E8D9B8AB-FA77-4456-B1ED-B036231DBA22}" 19 | EndProject 20 | Global 21 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 22 | Debug|Any CPU = Debug|Any CPU 23 | Release|Any CPU = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 26 | {A1653921-DC17-48FD-9C74-F62A0B23E742}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {A1653921-DC17-48FD-9C74-F62A0B23E742}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {A1653921-DC17-48FD-9C74-F62A0B23E742}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {A1653921-DC17-48FD-9C74-F62A0B23E742}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {6F5E57C6-45FE-4946-95FF-5B3259E08EB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {6F5E57C6-45FE-4946-95FF-5B3259E08EB7}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {6F5E57C6-45FE-4946-95FF-5B3259E08EB7}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {6F5E57C6-45FE-4946-95FF-5B3259E08EB7}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {E8D9B8AB-FA77-4456-B1ED-B036231DBA22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {E8D9B8AB-FA77-4456-B1ED-B036231DBA22}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {E8D9B8AB-FA77-4456-B1ED-B036231DBA22}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {E8D9B8AB-FA77-4456-B1ED-B036231DBA22}.Release|Any CPU.Build.0 = Release|Any CPU 38 | EndGlobalSection 39 | GlobalSection(SolutionProperties) = preSolution 40 | HideSolutionNode = FALSE 41 | EndGlobalSection 42 | GlobalSection(ExtensibilityGlobals) = postSolution 43 | SolutionGuid = {CA1BB767-870E-4B68-91ED-81C05D0D0F75} 44 | EndGlobalSection 45 | EndGlobal 46 | -------------------------------------------------------------------------------- /ConsoleMenu/CloseTrigger.cs: -------------------------------------------------------------------------------- 1 | namespace ConsoleTools; 2 | 3 | internal sealed class CloseTrigger 4 | { 5 | private bool close; 6 | 7 | public void SetOn() => this.close = true; 8 | 9 | public void SetOff() => this.close = false; 10 | 11 | public bool IsOn() => this.close; 12 | } 13 | -------------------------------------------------------------------------------- /ConsoleMenu/ConsoleMenu.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Runtime.CompilerServices; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | [assembly: InternalsVisibleTo("ConsoleMenuTests")] 9 | 10 | namespace ConsoleTools; 11 | 12 | /// 13 | /// A simple, highly customizable, DOS-like console menu. 14 | /// 15 | public class ConsoleMenu : IEnumerable 16 | { 17 | internal IConsole Console = new SystemConsole(); 18 | private readonly ItemsCollection menuItems; 19 | private readonly CloseTrigger closeTrigger; 20 | private MenuConfig config = new MenuConfig(); 21 | private ConsoleMenu? parent = null; 22 | private bool isShown = false; 23 | 24 | /// 25 | /// Initializes a new instance of the class. 26 | /// 27 | public ConsoleMenu() 28 | { 29 | this.menuItems = new ItemsCollection(); 30 | this.closeTrigger = new CloseTrigger(); 31 | } 32 | 33 | /// 34 | /// Initializes a new instance of the class 35 | /// with possibility to pre-select items via console parameter. 36 | /// 37 | /// args collection from Main. 38 | /// Level of whole menu. 39 | public ConsoleMenu(string[] args, int level) 40 | { 41 | if (args == null) 42 | { 43 | throw new ArgumentNullException(nameof(args)); 44 | } 45 | 46 | if (level < 0) 47 | { 48 | throw new ArgumentException("Cannot be below 0", nameof(level)); 49 | } 50 | 51 | this.menuItems = new ItemsCollection(args, level); 52 | this.closeTrigger = new CloseTrigger(); 53 | } 54 | 55 | /// 56 | /// Gets menu items that can be modified. 57 | /// 58 | public IReadOnlyList Items => this.menuItems.Items; 59 | 60 | /// 61 | /// Gets or sets selected menu item that can be modified. 62 | /// 63 | public MenuItem CurrentItem 64 | { 65 | get => this.menuItems.CurrentItem; 66 | set => this.menuItems.CurrentItem = value; 67 | } 68 | 69 | private IReadOnlyList Titles 70 | { 71 | get 72 | { 73 | ConsoleMenu? current = this; 74 | List titles = new(); 75 | while (current != null) 76 | { 77 | titles.Add(current.config.Title ?? ""); 78 | current = current.parent; 79 | } 80 | 81 | titles.Reverse(); 82 | return titles; 83 | } 84 | } 85 | 86 | /// 87 | /// Don't run this method directly. Just pass a reference to this method. 88 | /// 89 | /// Cancellation token. 90 | /// A representing the asynchronous operation. 91 | /// Thrown if this method is called directly. 92 | public static Task Close(CancellationToken cancellationToken) => throw new InvalidOperationException("Don't run this method directly. Just pass a reference to this method."); 93 | 94 | /// 95 | /// Close the menu before or after a menu action was triggered. 96 | /// 97 | public void CloseMenu() 98 | { 99 | this.closeTrigger.SetOn(); 100 | } 101 | 102 | /// 103 | /// Adds a menu action into this instance. 104 | /// 105 | /// Name of menu item. 106 | /// Action to call when menu item is chosen. 107 | /// This instance with added menu item. 108 | public ConsoleMenu Add(string name, Action action) 109 | { 110 | if (name == null) 111 | { 112 | throw new ArgumentNullException(nameof(name)); 113 | } 114 | 115 | if (action == null) 116 | { 117 | throw new ArgumentNullException(nameof(action)); 118 | } 119 | 120 | if (action.Target is ConsoleMenu child && action == child.Show) 121 | { 122 | child.parent = this; 123 | } 124 | 125 | this.menuItems.Add(name, (_) => 126 | { 127 | action(); 128 | return Task.CompletedTask; 129 | }); 130 | return this; 131 | } 132 | 133 | /// 134 | /// Adds an asynchronous menu action into this instance. 135 | /// 136 | /// Name of menu item. 137 | /// Action to call when menu item is chosen. 138 | /// This instance with added menu item. 139 | public ConsoleMenu Add(string name, Func action) 140 | { 141 | if (name == null) 142 | { 143 | throw new ArgumentNullException(nameof(name)); 144 | } 145 | 146 | if (action == null) 147 | { 148 | throw new ArgumentNullException(nameof(action)); 149 | } 150 | 151 | if (action.Target is ConsoleMenu child && action == child.ShowAsync) 152 | { 153 | child.parent = this; 154 | } 155 | 156 | this.menuItems.Add(name, action); 157 | return this; 158 | } 159 | 160 | /// 161 | /// Adds a menu action into this instance. 162 | /// 163 | /// Name of menu item. 164 | /// Action to call when menu item is chosen. 165 | /// This instance with added menu item. 166 | public ConsoleMenu Add(string name, Action action) 167 | { 168 | if (name == null) 169 | { 170 | throw new ArgumentNullException(nameof(name)); 171 | } 172 | 173 | if (action is null) 174 | { 175 | throw new ArgumentNullException(nameof(action)); 176 | } 177 | 178 | this.menuItems.Add(name, (_) => 179 | { 180 | action(this); 181 | return Task.CompletedTask; 182 | }); 183 | return this; 184 | } 185 | 186 | /// 187 | /// Adds an asynchronous menu action into this instance. 188 | /// 189 | /// Name of menu item. 190 | /// Action to call when menu item is chosen. 191 | /// This instance with added menu item. 192 | public ConsoleMenu Add(string name, Func action) 193 | { 194 | if (name == null) 195 | { 196 | throw new ArgumentNullException(nameof(name)); 197 | } 198 | 199 | if (action is null) 200 | { 201 | throw new ArgumentNullException(nameof(action)); 202 | } 203 | 204 | this.menuItems.Add(name, (cancellationToken) => action(this, cancellationToken)); 205 | return this; 206 | } 207 | 208 | /// 209 | /// Adds range of menu actions into this instance. 210 | /// 211 | /// Menu items to add. 212 | /// This instance with added menu items. 213 | public ConsoleMenu AddRange(IEnumerable> menuItems) 214 | { 215 | if (menuItems is null) 216 | { 217 | throw new ArgumentNullException(nameof(menuItems)); 218 | } 219 | 220 | foreach (var item in menuItems) 221 | { 222 | this.Add(item.Item1, item.Item2); 223 | } 224 | 225 | return this; 226 | } 227 | 228 | /// 229 | /// Adds range of asynchronous menu actions into this instance. 230 | /// 231 | /// Menu items to add. 232 | /// This instance with added menu items. 233 | public ConsoleMenu AddRange(IEnumerable>> menuItems) 234 | { 235 | if (menuItems is null) 236 | { 237 | throw new ArgumentNullException(nameof(menuItems)); 238 | } 239 | 240 | foreach (var item in menuItems) 241 | { 242 | this.Add(item.Item1, item.Item2); 243 | } 244 | 245 | return this; 246 | } 247 | 248 | /// 249 | /// Applies an configuration action on this instance. 250 | /// 251 | /// Configuration action. 252 | /// An configured instance. 253 | /// is null. 254 | public ConsoleMenu Configure(Action configure) 255 | { 256 | if (configure is null) 257 | { 258 | throw new ArgumentNullException(nameof(configure)); 259 | } 260 | 261 | configure.Invoke(this.config); 262 | return this; 263 | } 264 | 265 | /// 266 | /// Applies an configuration action on this instance. 267 | /// 268 | /// Configuration to apply. 269 | /// An configured instance. 270 | /// is null. 271 | public ConsoleMenu Configure(MenuConfig config) 272 | { 273 | if (config is null) 274 | { 275 | throw new ArgumentNullException(nameof(config)); 276 | } 277 | 278 | this.config = new MenuConfig(config); 279 | this.menuItems.EnableAlphabet = this.config.EnableAlphabet; 280 | return this; 281 | } 282 | 283 | /// 284 | /// Displays the menu in console. 285 | /// 286 | public void Show() 287 | { 288 | ShowAsync(CancellationToken.None).GetAwaiter().GetResult(); 289 | } 290 | 291 | /// 292 | /// Displays the menu in console. 293 | /// 294 | public async Task ShowAsync(CancellationToken cancellationToken = default) 295 | { 296 | if (isShown) 297 | { 298 | this.menuItems.UnsetSelectedIndex(); 299 | } 300 | 301 | isShown = true; 302 | 303 | await new ConsoleMenuDisplay( 304 | this.menuItems, 305 | this.Console, 306 | new List(this.Titles), 307 | this.config, 308 | this.closeTrigger).ShowAsync(cancellationToken); 309 | } 310 | 311 | /// 312 | /// Returns an enumeration of the current menu items. 313 | /// See . 314 | /// 315 | /// An enumeration of the current menu items. 316 | public IEnumerator GetEnumerator() => this.Items.GetEnumerator(); 317 | } 318 | -------------------------------------------------------------------------------- /ConsoleMenu/ConsoleMenu.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard1.3;net6.0 5 | ConsoleMenu-simple 6 | lechu445 7 | ConsoleMenu-simple 8 | A simple, highly customizable, DOS-like console menu 9 | - Added support for up to 36 menu items (#21) 10 | - Added support to change the encoding (#23) 11 | https://github.com/lechu445/ConsoleMenu 12 | true 13 | 2.7.0 14 | https://github.com/lechu445/ConsoleMenu 15 | console, menu, simple 16 | LICENSE.txt 17 | README.md 18 | true 19 | snupkg 20 | latest 21 | enable 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | True 36 | 37 | 38 | 39 | 40 | 41 | 42 | all 43 | runtime; build; native; contentfiles; analyzers; buildtransitive 44 | 45 | 46 | all 47 | runtime; build; native; contentfiles; analyzers; buildtransitive 48 | 49 | 50 | all 51 | runtime; build; native; contentfiles; analyzers; buildtransitive 52 | 53 | 54 | -------------------------------------------------------------------------------- /ConsoleMenu/ConsoleMenuDisplay.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace ConsoleTools; 8 | 9 | internal sealed class ConsoleMenuDisplay 10 | { 11 | private readonly IConsole console; 12 | private readonly ItemsCollection menuItems; 13 | private readonly List titles; 14 | private readonly MenuConfig config; 15 | private readonly VisibilityManager visibility; 16 | private readonly CloseTrigger closeTrigger; 17 | private readonly string noSelectorLine; 18 | 19 | public ConsoleMenuDisplay( 20 | ItemsCollection menuItems, 21 | IConsole console, 22 | List titles, 23 | MenuConfig config, 24 | CloseTrigger closeTrigger) 25 | { 26 | this.menuItems = menuItems; 27 | this.console = console; 28 | this.titles = titles; 29 | this.config = config; 30 | this.visibility = new VisibilityManager(menuItems.Items.Count); 31 | this.closeTrigger = closeTrigger; 32 | this.noSelectorLine = new string(' ', this.config.Selector.Length); 33 | } 34 | 35 | public async Task ShowAsync(CancellationToken token) 36 | { 37 | var selectedItem = this.menuItems.GetSelectedItem(); 38 | if (selectedItem != null) 39 | { 40 | await selectedItem.AsyncAction.Invoke(token); 41 | return; 42 | } 43 | 44 | ConsoleKeyInfo key; 45 | this.menuItems.ResetCurrentIndex(); 46 | var currentForegroundColor = this.console.ForegroundColor; 47 | var currentBackgroundColor = this.console.BackgroundColor; 48 | bool breakIteration = false; 49 | var filter = new StringBuilder(); 50 | 51 | while (true) 52 | { 53 | token.ThrowIfCancellationRequested(); 54 | do 55 | { 56 | if (this.config.ClearConsole) 57 | { 58 | this.console.Clear(); 59 | } 60 | 61 | if (this.config.EnableBreadcrumb) 62 | { 63 | this.config.WriteBreadcrumbAction(this.titles); 64 | } 65 | 66 | if (this.config.EnableWriteTitle) 67 | { 68 | this.config.WriteTitleAction(this.config.Title); 69 | } 70 | 71 | this.config.WriteHeaderAction(); 72 | 73 | foreach (var menuItem in this.menuItems.Items) 74 | { 75 | if (this.config.EnableFilter && !this.visibility.IsVisibleAt(menuItem.Index)) 76 | { 77 | this.menuItems.SelectClosestVisibleItem(this.visibility); 78 | } 79 | else 80 | { 81 | this.WriteLineWithItem(menuItem); 82 | } 83 | } 84 | 85 | if (breakIteration) 86 | { 87 | breakIteration = false; 88 | break; 89 | } 90 | 91 | if (this.config.EnableFilter) 92 | { 93 | this.console.Write(this.config.FilterPrompt + filter); 94 | } 95 | 96 | readKey: 97 | key = this.console.ReadKey(true); 98 | 99 | if (key.Key == ConsoleKey.DownArrow) 100 | { 101 | this.menuItems.SelectNextVisibleItem(this.visibility); 102 | } 103 | else if (key.Key == ConsoleKey.UpArrow) 104 | { 105 | this.menuItems.SelectPreviousVisibleItem(this.visibility); 106 | } 107 | else if (!this.config.DisableKeyboardNavigation && this.menuItems.CanSelect(key.KeyChar)) 108 | { 109 | this.menuItems.Select(key.KeyChar); 110 | breakIteration = true; 111 | } 112 | else if (key.Key != ConsoleKey.Enter) 113 | { 114 | if (this.config.EnableFilter) 115 | { 116 | if (key.Key == ConsoleKey.Backspace) 117 | { 118 | if (filter.Length > 0) 119 | { 120 | filter.Length--; 121 | } 122 | } 123 | else if (!char.IsControl(key.KeyChar)) 124 | { 125 | filter.Append(key.KeyChar); 126 | } 127 | 128 | var filterString = filter.ToString(); 129 | 130 | this.visibility.SetVisibleWithPredicate(this.menuItems.Items, (item) => item.Name.Contains(filterString, StringComparison.OrdinalIgnoreCase)); 131 | } 132 | else 133 | { 134 | goto readKey; 135 | } 136 | } 137 | } 138 | while (key.Key != ConsoleKey.Enter); 139 | 140 | this.console.WriteLine(); 141 | this.console.ForegroundColor = currentForegroundColor; 142 | this.console.BackgroundColor = currentBackgroundColor; 143 | var action = this.menuItems.CurrentItem.AsyncAction; 144 | if (action == ConsoleMenu.Close) 145 | { 146 | this.menuItems.UnsetSelectedIndex(); 147 | return; 148 | } 149 | else 150 | { 151 | await action(token).ConfigureAwait(false); 152 | if (this.closeTrigger.IsOn()) 153 | { 154 | this.menuItems.UnsetSelectedIndex(); 155 | this.closeTrigger.SetOff(); 156 | return; 157 | } 158 | } 159 | } 160 | } 161 | 162 | private void WriteLineWithItem(MenuItem menuItem) 163 | { 164 | if (this.menuItems.IsSelected(menuItem)) 165 | { 166 | this.console.BackgroundColor = this.config.SelectedItemBackgroundColor; 167 | this.console.ForegroundColor = this.config.SelectedItemForegroundColor; 168 | this.console.Write(this.config.Selector); 169 | this.config.WriteItemAction(menuItem); 170 | this.console.WriteLine(); 171 | this.console.BackgroundColor = this.config.ItemBackgroundColor; 172 | this.console.ForegroundColor = this.config.ItemForegroundColor; 173 | } 174 | else 175 | { 176 | this.console.BackgroundColor = this.config.ItemBackgroundColor; 177 | this.console.ForegroundColor = this.config.ItemForegroundColor; 178 | this.console.Write(this.noSelectorLine); 179 | this.config.WriteItemAction(menuItem); 180 | this.console.WriteLine(); 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /ConsoleMenu/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace ConsoleTools; 5 | 6 | internal static class Extensions 7 | { 8 | public static List SplitItems(this string input, char separator, char itemQuote) 9 | { 10 | var result = new List(); 11 | var isInQuote = false; 12 | var start = 0; 13 | for (int i = 0; i < input.Length; i++) 14 | { 15 | var ch = input[i]; 16 | if (ch == itemQuote) 17 | { 18 | isInQuote = !isInQuote; 19 | } 20 | 21 | if (!isInQuote && ch == separator) 22 | { 23 | result.Add(input.Substring(start, i - start)); 24 | start = i + 1; 25 | } 26 | } 27 | 28 | if (start < input.Length) 29 | { 30 | result.Add(input.Substring(start, input.Length - start)); 31 | } 32 | 33 | return result; 34 | } 35 | 36 | public static bool Contains(this string source, string toCheck, StringComparison comp) 37 | { 38 | return source?.IndexOf(toCheck, comp) >= 0; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ConsoleMenu/IConsole.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | 5 | namespace ConsoleTools; 6 | 7 | internal interface IConsole 8 | { 9 | event ConsoleCancelEventHandler? CancelKeyPress; 10 | 11 | bool IsOutputRedirected { get; } 12 | 13 | int BufferHeight { get; set; } 14 | 15 | int BufferWidth { get; set; } 16 | 17 | bool CapsLock { get; } 18 | 19 | int CursorLeft { get; set; } 20 | 21 | int CursorSize { get; set; } 22 | 23 | int CursorTop { get; set; } 24 | 25 | bool CursorVisible { get; set; } 26 | 27 | TextWriter Error { get; } 28 | 29 | ConsoleColor ForegroundColor { get; set; } 30 | 31 | Encoding InputEncoding { get; set; } 32 | 33 | bool IsErrorRedirected { get; } 34 | 35 | bool IsInputRedirected { get; } 36 | 37 | int WindowTop { get; set; } 38 | 39 | TextReader In { get; } 40 | 41 | bool KeyAvailable { get; } 42 | 43 | int LargestWindowWidth { get; } 44 | 45 | int LargestWindowHeight { get; } 46 | 47 | bool NumberLock { get; } 48 | 49 | TextWriter Out { get; } 50 | 51 | Encoding OutputEncoding { get; set; } 52 | 53 | string Title { get; set; } 54 | 55 | bool TreatControlCAsInput { get; set; } 56 | 57 | int WindowHeight { get; set; } 58 | 59 | int WindowWidth { get; set; } 60 | 61 | int WindowLeft { get; set; } 62 | 63 | ConsoleColor BackgroundColor { get; set; } 64 | 65 | void Beep(); 66 | 67 | void Beep(int frequency, int duration); 68 | 69 | void Clear(); 70 | 71 | void MoveBufferArea(int sourceLeft, int sourceTop, int sourceWidth, int sourceHeight, int targetLeft, int targetTop); 72 | 73 | void MoveBufferArea(int sourceLeft, int sourceTop, int sourceWidth, int sourceHeight, int targetLeft, int targetTop, char sourceChar, ConsoleColor sourceForeColor, ConsoleColor sourceBackColor); 74 | 75 | Stream OpenStandardError(); 76 | 77 | Stream OpenStandardInput(); 78 | 79 | Stream OpenStandardOutput(); 80 | 81 | int Read(); 82 | 83 | ConsoleKeyInfo ReadKey(bool intercept); 84 | 85 | ConsoleKeyInfo ReadKey(); 86 | 87 | string? ReadLine(); 88 | 89 | void ResetColor(); 90 | 91 | void SetBufferSize(int width, int height); 92 | 93 | void SetCursorPosition(int left, int top); 94 | 95 | void SetError(TextWriter newError); 96 | 97 | void SetIn(TextReader newIn); 98 | 99 | void SetOut(TextWriter newOut); 100 | 101 | void SetWindowPosition(int left, int top); 102 | 103 | void SetWindowSize(int width, int height); 104 | 105 | void Write(char[] buffer, int index, int count); 106 | 107 | void Write(char[] buffer); 108 | 109 | void Write(float value); 110 | 111 | void Write(bool value); 112 | 113 | void Write(decimal value); 114 | 115 | void Write(char value); 116 | 117 | void Write(double value); 118 | 119 | void Write(int value); 120 | 121 | void Write(long value); 122 | 123 | void Write(string value); 124 | 125 | void Write(string format, object arg0); 126 | 127 | void Write(string format, object arg0, object arg1); 128 | 129 | void Write(string format, object arg0, object arg1, object arg2); 130 | 131 | void Write(string format, params object[] arg); 132 | 133 | void Write(uint value); 134 | 135 | void Write(ulong value); 136 | 137 | void Write(object value); 138 | 139 | void WriteLine(); 140 | 141 | void WriteLine(bool value); 142 | 143 | void WriteLine(char value); 144 | 145 | void WriteLine(char[] buffer); 146 | 147 | void WriteLine(ulong value); 148 | 149 | void WriteLine(double value); 150 | 151 | void WriteLine(int value); 152 | 153 | void WriteLine(long value); 154 | 155 | void WriteLine(object value); 156 | 157 | void WriteLine(float value); 158 | 159 | void WriteLine(string value); 160 | 161 | void WriteLine(string format, object arg0); 162 | 163 | void WriteLine(string format, object arg0, object arg1); 164 | 165 | void WriteLine(string format, object arg0, object arg1, object arg2); 166 | 167 | void WriteLine(string format, params object[] arg); 168 | 169 | void WriteLine(uint value); 170 | 171 | void WriteLine(decimal value); 172 | 173 | void WriteLine(char[] buffer, int index, int count); 174 | } 175 | -------------------------------------------------------------------------------- /ConsoleMenu/ItemsCollection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace ConsoleTools; 7 | 8 | internal sealed class ItemsCollection 9 | { 10 | private readonly List menuItems = new List(); 11 | private readonly MenuConfig config = new MenuConfig(); 12 | private int? selectedIndex; 13 | private string? selectedName; 14 | private int currentItemIndex; 15 | 16 | public ItemsCollection() 17 | { 18 | } 19 | 20 | public ItemsCollection(string[] args, int level) 21 | { 22 | this.SetSelectedItems(args, level); 23 | } 24 | 25 | public List Items => this.menuItems; 26 | 27 | public MenuItem CurrentItem 28 | { 29 | get => this.menuItems[this.currentItemIndex]; 30 | set => this.menuItems[this.currentItemIndex] = value; 31 | } 32 | 33 | public bool EnableAlphabet { get; set; } = false; 34 | 35 | public void Add(string name, Func action) 36 | { 37 | this.menuItems.Add(new MenuItem(name, action, this.menuItems.Count)); 38 | } 39 | 40 | public void ResetCurrentIndex() 41 | { 42 | this.selectedIndex = 0; 43 | } 44 | 45 | public void SetSelectedItems(string[] args, int level) 46 | { 47 | var arg = Array.Find(args, a => a.StartsWith(this.config.ArgsPreselectedItemsKey)); 48 | this.SetSelectedItems(level, this.config.ArgsPreselectedItemsKey, ref arg); 49 | } 50 | 51 | public MenuItem? GetSelectedItem() 52 | { 53 | if (this.selectedIndex < this.menuItems.Count) 54 | { 55 | return this.menuItems[this.selectedIndex.Value]; 56 | } 57 | 58 | if (this.selectedName != null) 59 | { 60 | return this.menuItems.Find(item => item.Name == this.selectedName); 61 | } 62 | 63 | return null; 64 | } 65 | 66 | public void SelectClosestVisibleItem(VisibilityManager visibility) 67 | { 68 | this.currentItemIndex = visibility.IndexOfClosestVisibleItem(this.currentItemIndex); 69 | } 70 | 71 | public void SelectNextVisibleItem(VisibilityManager visibility) 72 | { 73 | this.currentItemIndex = visibility.IndexOfNextVisibleItem(this.currentItemIndex); 74 | } 75 | 76 | public void SelectPreviousVisibleItem(VisibilityManager visibility) 77 | { 78 | this.currentItemIndex = visibility.IndexOfPreviousVisibleItem(this.currentItemIndex); 79 | } 80 | 81 | public bool CanSelect(char ch) 82 | { 83 | if (this.EnableAlphabet) 84 | { 85 | return ch >= '0' && GetSelectedIndex(ch) < this.menuItems.Count; 86 | } 87 | 88 | return ch >= '0' && (ch - '0') < this.menuItems.Count; 89 | } 90 | 91 | public void Select(char ch) 92 | { 93 | if (this.EnableAlphabet) 94 | { 95 | this.currentItemIndex = GetSelectedIndex(ch); 96 | } 97 | else 98 | { 99 | this.currentItemIndex = ch - '0'; 100 | } 101 | } 102 | 103 | public void UnsetSelectedIndex() 104 | { 105 | this.selectedIndex = null; 106 | this.selectedName = null; 107 | } 108 | 109 | internal bool IsSelected(MenuItem menuItem) 110 | { 111 | return this.currentItemIndex == menuItem.Index; 112 | } 113 | 114 | private static int GetSelectedIndex(char ch) 115 | { 116 | int index = ch switch 117 | { 118 | >= 'a' and <= 'z' => ch - 'a' + 10, 119 | >= 'A' and <= 'Z' => ch - 'A' + 10, 120 | _ => ch - '0', 121 | }; 122 | 123 | return index; 124 | } 125 | 126 | private void SetSelectedItems(int level, string paramKey, ref string? arg) 127 | { 128 | if (arg == null) 129 | { 130 | return; 131 | } 132 | 133 | arg = arg.Replace(paramKey, string.Empty).Trim(); 134 | var items = arg.SplitItems(this.config.ArgsPreselectedItemsValueSeparator, '\''); 135 | if (level < items.Count) 136 | { 137 | var item = items[level].Trim('\''); 138 | if (int.TryParse(item, out var selectedIndex)) 139 | { 140 | this.selectedIndex = selectedIndex; 141 | return; 142 | } 143 | 144 | this.selectedName = item; 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /ConsoleMenu/MenuConfig.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable SA1401 // Fields should be private 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace ConsoleTools; 7 | 8 | /// 9 | /// Menu configuration. 10 | /// 11 | public class MenuConfig 12 | { 13 | /// 14 | /// Initializes a new instance of the class. 15 | /// 16 | public MenuConfig() 17 | { 18 | this.WriteItemAction = item => 19 | { 20 | if (this.EnableAlphabet) 21 | { 22 | AlphabetItemAction(item); 23 | } 24 | else 25 | { 26 | DefaultItemAction(item); 27 | } 28 | }; 29 | } 30 | 31 | internal MenuConfig(MenuConfig config) 32 | : this() 33 | { 34 | this.ArgsPreselectedItemsKey = config.ArgsPreselectedItemsKey; 35 | this.ArgsPreselectedItemsValueSeparator = config.ArgsPreselectedItemsValueSeparator; 36 | this.ClearConsole = config.ClearConsole; 37 | this.EnableBreadcrumb = config.EnableBreadcrumb; 38 | this.EnableFilter = config.EnableFilter; 39 | this.EnableWriteTitle = config.EnableWriteTitle; 40 | this.FilterPrompt = config.FilterPrompt; 41 | this.InputEncoding = config.InputEncoding; 42 | this.ItemBackgroundColor = config.ItemBackgroundColor; 43 | this.ItemForegroundColor = config.ItemForegroundColor; 44 | this.OutputEncoding = config.OutputEncoding; 45 | this.SelectedItemBackgroundColor = config.SelectedItemBackgroundColor; 46 | this.SelectedItemForegroundColor = config.SelectedItemForegroundColor; 47 | this.Selector = config.Selector; 48 | this.Title = config.Title; 49 | this.WriteBreadcrumbAction = config.WriteBreadcrumbAction; 50 | this.WriteHeaderAction = config.WriteHeaderAction; 51 | this.WriteItemAction = config.WriteItemAction; 52 | this.WriteTitleAction = config.WriteTitleAction; 53 | this.EnableAlphabet = config.EnableAlphabet; 54 | this.DisableKeyboardNavigation = config.DisableKeyboardNavigation; 55 | } 56 | 57 | /// default: Console.ForegroundColor 58 | public ConsoleColor SelectedItemBackgroundColor = Console.ForegroundColor; 59 | 60 | /// default: Console.BackgroundColor 61 | public ConsoleColor SelectedItemForegroundColor = Console.BackgroundColor; 62 | 63 | /// default: Console.BackgroundColor 64 | public ConsoleColor ItemBackgroundColor = Console.BackgroundColor; 65 | 66 | /// default: Console.ForegroundColor 67 | public ConsoleColor ItemForegroundColor = Console.ForegroundColor; 68 | 69 | /// default: Console.OutputEncoding 70 | public Encoding InputEncoding = Console.InputEncoding; 71 | 72 | /// default: Console.OutputEncoding 73 | public Encoding OutputEncoding = Console.OutputEncoding; 74 | 75 | /// default: () => Console.WriteLine("Pick an option:") 76 | public Action WriteHeaderAction = () => Console.WriteLine("Pick an option:"); 77 | 78 | /// default: (item) => Console.Write("[{0}] {1}", item.Index, item.Name) 79 | public Action WriteItemAction; 80 | 81 | /// default: ">> " 82 | public string Selector = ">> "; 83 | 84 | /// default: "Filter: " 85 | public string FilterPrompt = "Filter: "; 86 | 87 | /// default: true 88 | public bool ClearConsole = true; 89 | 90 | /// default: true 91 | public bool EnableFilter = false; 92 | 93 | /// Console parameter that runs menu with pre-selection. default: "--menu-select=" 94 | public string ArgsPreselectedItemsKey = "--menu-select="; 95 | 96 | /// default: '.' 97 | public char ArgsPreselectedItemsValueSeparator = '.'; 98 | 99 | /// default: false 100 | public bool EnableWriteTitle = false; 101 | 102 | /// Menu title to write at top of the menu. default: "My menu" 103 | public string Title = "My menu"; 104 | 105 | /// default: title => Console.WriteLine(title) 106 | public Action WriteTitleAction = title => Console.WriteLine(title); 107 | 108 | /// default: false 109 | public bool EnableBreadcrumb = false; 110 | 111 | /// default: titles => Console.WriteLine(string.Join(" > ", titles)) 112 | public Action> WriteBreadcrumbAction = titles => Console.WriteLine(string.Join(" > ", titles)); 113 | 114 | /// 115 | /// Uses A..Z for the menu index to enable keyboard navigation for up to 36 menu items. 116 | /// If you have more than 36 menu items, it will switch back to using numbers for item 37 forward. 117 | /// default: false 118 | /// 119 | /// 120 | /// Note that enabling this feature effectively prevents filtering, as the first letter of the 121 | /// filtered value is treated as a menu item key. 122 | /// 123 | public bool EnableAlphabet = false; 124 | 125 | /// Disables keyboard navigation, forcing the use of the up and down arrows. default: false 126 | public bool DisableKeyboardNavigation = false; 127 | 128 | private static void DefaultItemAction(MenuItem item) 129 | { 130 | Console.Write("[{0}] {1}", item.Index, item.Name); 131 | } 132 | 133 | private static void AlphabetItemAction(MenuItem item) 134 | { 135 | char index; 136 | 137 | switch (item.Index) 138 | { 139 | case >= 36: 140 | // use item.Index (i.e. 37, 38, 39) 141 | Console.Write("[{0}] {1}", item.Index, item.Name); 142 | break; 143 | 144 | case >= 10: 145 | // use A..Z 146 | index = (char)(item.Index + 'A' - 10); 147 | Console.Write("[{0}] {1}", index, item.Name); 148 | break; 149 | 150 | default: 151 | // use 0..9 152 | index = (char)(item.Index + '0'); 153 | Console.Write("[{0}] {1}", index, item.Name); 154 | break; 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /ConsoleMenu/MenuItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace ConsoleTools; 7 | 8 | /// 9 | /// Menu item. 10 | /// 11 | public sealed class MenuItem 12 | { 13 | internal MenuItem(string name, Func action, int index) 14 | { 15 | Debug.Assert(index >= 0); 16 | 17 | this.Name = name ?? throw new ArgumentNullException(nameof(name)); 18 | this.AsyncAction = action ?? throw new ArgumentNullException(nameof(action)); 19 | this.Index = index; 20 | } 21 | 22 | /// 23 | /// Gets or sets name of the menu item that will be displayed. 24 | /// 25 | public string Name { get; set; } 26 | 27 | /// 28 | /// Gets or sets an action of the menu item that will be called when the item is called. 29 | /// If you get asynchronous action, it will be converted to synchronous, so better use getter. 30 | /// 31 | public Action Action 32 | { 33 | get => () => this.AsyncAction(CancellationToken.None).GetAwaiter().GetResult(); 34 | set => this.AsyncAction = (_) => 35 | { 36 | value(); 37 | return Task.CompletedTask; 38 | }; 39 | } 40 | 41 | /// 42 | /// Gets or sets an action of the menu item that will be called when the item is called. 43 | /// 44 | public Func AsyncAction { get; set; } 45 | 46 | /// 47 | /// Gets an index of the menu item. 48 | /// 49 | public int Index { get; } 50 | } 51 | -------------------------------------------------------------------------------- /ConsoleMenu/Properties/PublishProfiles/FolderProfile.pubxml: -------------------------------------------------------------------------------- 1 |  2 | 5 | 6 | 7 | Release 8 | Any CPU 9 | bin\Release\netstandard1.3\publish\ 10 | FileSystem 11 | 12 | -------------------------------------------------------------------------------- /ConsoleMenu/PublicAPI.Shipped.txt: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | -------------------------------------------------------------------------------- /ConsoleMenu/PublicAPI.Unshipped.txt: -------------------------------------------------------------------------------- 1 | ConsoleTools.ConsoleMenu 2 | ConsoleTools.ConsoleMenu.Add(string! name, System.Action! action) -> ConsoleTools.ConsoleMenu! 3 | ConsoleTools.ConsoleMenu.Add(string! name, System.Action! action) -> ConsoleTools.ConsoleMenu! 4 | ConsoleTools.ConsoleMenu.Add(string! name, System.Func! action) -> ConsoleTools.ConsoleMenu! 5 | ConsoleTools.ConsoleMenu.Add(string! name, System.Func! action) -> ConsoleTools.ConsoleMenu! 6 | ConsoleTools.ConsoleMenu.AddRange(System.Collections.Generic.IEnumerable!>! menuItems) -> ConsoleTools.ConsoleMenu! 7 | ConsoleTools.ConsoleMenu.AddRange(System.Collections.Generic.IEnumerable!>!>! menuItems) -> ConsoleTools.ConsoleMenu! 8 | ConsoleTools.ConsoleMenu.CloseMenu() -> void 9 | ConsoleTools.ConsoleMenu.Configure(ConsoleTools.MenuConfig! config) -> ConsoleTools.ConsoleMenu! 10 | ConsoleTools.ConsoleMenu.Configure(System.Action! configure) -> ConsoleTools.ConsoleMenu! 11 | ConsoleTools.ConsoleMenu.ConsoleMenu() -> void 12 | ConsoleTools.ConsoleMenu.ConsoleMenu(string![]! args, int level) -> void 13 | ConsoleTools.ConsoleMenu.CurrentItem.get -> ConsoleTools.MenuItem! 14 | ConsoleTools.ConsoleMenu.CurrentItem.set -> void 15 | ConsoleTools.ConsoleMenu.GetEnumerator() -> System.Collections.IEnumerator! 16 | ConsoleTools.ConsoleMenu.Items.get -> System.Collections.Generic.IReadOnlyList! 17 | ConsoleTools.ConsoleMenu.Show() -> void 18 | ConsoleTools.MenuConfig 19 | ConsoleTools.MenuConfig.ArgsPreselectedItemsKey -> string! 20 | ConsoleTools.MenuConfig.ArgsPreselectedItemsValueSeparator -> char 21 | ConsoleTools.MenuConfig.ClearConsole -> bool 22 | ConsoleTools.MenuConfig.DisableKeyboardNavigation -> bool 23 | ConsoleTools.MenuConfig.EnableAlphabet -> bool 24 | ConsoleTools.MenuConfig.EnableBreadcrumb -> bool 25 | ConsoleTools.MenuConfig.EnableFilter -> bool 26 | ConsoleTools.MenuConfig.EnableWriteTitle -> bool 27 | ConsoleTools.MenuConfig.FilterPrompt -> string! 28 | ConsoleTools.MenuConfig.InputEncoding -> System.Text.Encoding! 29 | ConsoleTools.MenuConfig.ItemBackgroundColor -> System.ConsoleColor 30 | ConsoleTools.MenuConfig.ItemForegroundColor -> System.ConsoleColor 31 | ConsoleTools.MenuConfig.MenuConfig() -> void 32 | ConsoleTools.MenuConfig.OutputEncoding -> System.Text.Encoding! 33 | ConsoleTools.MenuConfig.SelectedItemBackgroundColor -> System.ConsoleColor 34 | ConsoleTools.MenuConfig.SelectedItemForegroundColor -> System.ConsoleColor 35 | ConsoleTools.MenuConfig.Selector -> string! 36 | ConsoleTools.MenuConfig.Title -> string! 37 | ConsoleTools.MenuConfig.WriteBreadcrumbAction -> System.Action!>! 38 | ConsoleTools.MenuConfig.WriteHeaderAction -> System.Action! 39 | ConsoleTools.MenuConfig.WriteItemAction -> System.Action! 40 | ConsoleTools.MenuConfig.WriteTitleAction -> System.Action! 41 | ConsoleTools.MenuItem 42 | ConsoleTools.MenuItem.Action.get -> System.Action! 43 | ConsoleTools.MenuItem.Action.set -> void 44 | ConsoleTools.MenuItem.AsyncAction.get -> System.Func! 45 | ConsoleTools.MenuItem.AsyncAction.set -> void 46 | ConsoleTools.MenuItem.Index.get -> int 47 | ConsoleTools.MenuItem.Name.get -> string! 48 | ConsoleTools.MenuItem.Name.set -> void 49 | static ConsoleTools.ConsoleMenu.Close(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -------------------------------------------------------------------------------- /ConsoleMenu/SystemConsole.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | 5 | namespace ConsoleTools; 6 | 7 | internal sealed class SystemConsole : IConsole 8 | { 9 | public event ConsoleCancelEventHandler? CancelKeyPress; 10 | 11 | public bool IsOutputRedirected => Console.IsOutputRedirected; 12 | 13 | public int BufferHeight { get => Console.BufferHeight; set => Console.BufferHeight = value; } 14 | 15 | public int BufferWidth { get => Console.BufferWidth; set => Console.BufferWidth = value; } 16 | 17 | public bool CapsLock => Console.CapsLock; 18 | 19 | public int CursorLeft { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } 20 | 21 | public int CursorSize { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } 22 | 23 | public int CursorTop { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } 24 | 25 | public bool CursorVisible { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } 26 | 27 | public TextWriter Error => throw new NotImplementedException(); 28 | 29 | public ConsoleColor ForegroundColor { get => Console.ForegroundColor; set => Console.ForegroundColor = value; } 30 | 31 | public Encoding InputEncoding { get => Console.InputEncoding; set => Console.InputEncoding = value; } 32 | 33 | public bool IsErrorRedirected => throw new NotImplementedException(); 34 | 35 | public bool IsInputRedirected => throw new NotImplementedException(); 36 | 37 | public int WindowTop { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } 38 | 39 | public TextReader In => throw new NotImplementedException(); 40 | 41 | public bool KeyAvailable => throw new NotImplementedException(); 42 | 43 | public int LargestWindowWidth => throw new NotImplementedException(); 44 | 45 | public int LargestWindowHeight => throw new NotImplementedException(); 46 | 47 | public bool NumberLock => throw new NotImplementedException(); 48 | 49 | public TextWriter Out => throw new NotImplementedException(); 50 | 51 | public Encoding OutputEncoding { get => Console.OutputEncoding; set => Console.OutputEncoding = value; } 52 | 53 | public string Title { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } 54 | 55 | public bool TreatControlCAsInput { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } 56 | 57 | public int WindowHeight { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } 58 | 59 | public int WindowWidth { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } 60 | 61 | public int WindowLeft { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } 62 | 63 | public ConsoleColor BackgroundColor { get => Console.BackgroundColor; set => Console.BackgroundColor = value; } 64 | 65 | public void Beep() 66 | { 67 | throw new NotImplementedException(); 68 | } 69 | 70 | public void Beep(int frequency, int duration) 71 | { 72 | throw new NotImplementedException(); 73 | } 74 | 75 | public void Clear() => Console.Clear(); 76 | 77 | public void MoveBufferArea(int sourceLeft, int sourceTop, int sourceWidth, int sourceHeight, int targetLeft, int targetTop) 78 | { 79 | throw new NotImplementedException(); 80 | } 81 | 82 | public void MoveBufferArea(int sourceLeft, int sourceTop, int sourceWidth, int sourceHeight, int targetLeft, int targetTop, char sourceChar, ConsoleColor sourceForeColor, ConsoleColor sourceBackColor) 83 | { 84 | throw new NotImplementedException(); 85 | } 86 | 87 | public Stream OpenStandardError() 88 | { 89 | throw new NotImplementedException(); 90 | } 91 | 92 | public Stream OpenStandardInput() 93 | { 94 | throw new NotImplementedException(); 95 | } 96 | 97 | public Stream OpenStandardOutput() 98 | { 99 | throw new NotImplementedException(); 100 | } 101 | 102 | public int Read() 103 | => Console.Read(); 104 | 105 | public ConsoleKeyInfo ReadKey(bool intercept) 106 | => Console.ReadKey(intercept); 107 | 108 | public ConsoleKeyInfo ReadKey() 109 | => Console.ReadKey(); 110 | 111 | public string? ReadLine() 112 | => Console.ReadLine(); 113 | 114 | public void ResetColor() 115 | => Console.ResetColor(); 116 | 117 | public void SetBufferSize(int width, int height) 118 | { 119 | throw new NotImplementedException(); 120 | } 121 | 122 | public void SetCursorPosition(int left, int top) 123 | { 124 | throw new NotImplementedException(); 125 | } 126 | 127 | public void SetError(TextWriter newError) 128 | { 129 | throw new NotImplementedException(); 130 | } 131 | 132 | public void SetIn(TextReader newIn) 133 | { 134 | throw new NotImplementedException(); 135 | } 136 | 137 | public void SetOut(TextWriter newOut) 138 | { 139 | throw new NotImplementedException(); 140 | } 141 | 142 | public void SetWindowPosition(int left, int top) 143 | { 144 | throw new NotImplementedException(); 145 | } 146 | 147 | public void SetWindowSize(int width, int height) 148 | { 149 | throw new NotImplementedException(); 150 | } 151 | 152 | public void Write(char[] buffer, int index, int count) 153 | { 154 | throw new NotImplementedException(); 155 | } 156 | 157 | public void Write(char[] buffer) 158 | => Console.Write(buffer); 159 | 160 | public void Write(float value) 161 | => Console.Write(value); 162 | 163 | public void Write(bool value) 164 | => Console.Write(value); 165 | 166 | public void Write(decimal value) 167 | => Console.Write(value); 168 | 169 | public void Write(char value) 170 | => Console.Write(value); 171 | 172 | public void Write(double value) 173 | => Console.Write(value); 174 | 175 | public void Write(int value) 176 | => Console.Write(value); 177 | 178 | public void Write(long value) 179 | => Console.Write(value); 180 | 181 | public void Write(string value) 182 | => Console.Write(value); 183 | 184 | public void Write(string format, object arg0) 185 | { 186 | throw new NotImplementedException(); 187 | } 188 | 189 | public void Write(string format, object arg0, object arg1) 190 | { 191 | throw new NotImplementedException(); 192 | } 193 | 194 | public void Write(string format, object arg0, object arg1, object arg2) 195 | { 196 | throw new NotImplementedException(); 197 | } 198 | 199 | public void Write(string format, params object[] arg) 200 | { 201 | throw new NotImplementedException(); 202 | } 203 | 204 | public void Write(uint value) 205 | => Console.Write(value); 206 | 207 | public void Write(ulong value) 208 | => Console.Write(value); 209 | 210 | public void Write(object value) 211 | => Console.Write(value); 212 | 213 | public void WriteLine() 214 | => Console.WriteLine(); 215 | 216 | public void WriteLine(bool value) 217 | => Console.WriteLine(value); 218 | 219 | public void WriteLine(char value) 220 | => Console.WriteLine(value); 221 | 222 | public void WriteLine(char[] buffer) 223 | => Console.WriteLine(buffer); 224 | 225 | public void WriteLine(ulong value) 226 | => Console.WriteLine(value); 227 | 228 | public void WriteLine(double value) 229 | => Console.WriteLine(value); 230 | 231 | public void WriteLine(int value) 232 | => Console.WriteLine(value); 233 | 234 | public void WriteLine(long value) 235 | => Console.WriteLine(value); 236 | 237 | public void WriteLine(object value) 238 | => Console.WriteLine(value); 239 | 240 | public void WriteLine(float value) 241 | => Console.WriteLine(value); 242 | 243 | public void WriteLine(string value) 244 | => Console.WriteLine(value); 245 | 246 | public void WriteLine(string format, object arg0) 247 | { 248 | throw new NotImplementedException(); 249 | } 250 | 251 | public void WriteLine(string format, object arg0, object arg1) 252 | { 253 | throw new NotImplementedException(); 254 | } 255 | 256 | public void WriteLine(string format, object arg0, object arg1, object arg2) 257 | { 258 | throw new NotImplementedException(); 259 | } 260 | 261 | public void WriteLine(string format, params object[] arg) 262 | { 263 | throw new NotImplementedException(); 264 | } 265 | 266 | public void WriteLine(uint value) 267 | => Console.WriteLine(value); 268 | 269 | public void WriteLine(decimal value) 270 | => Console.WriteLine(value); 271 | 272 | public void WriteLine(char[] buffer, int index, int count) 273 | { 274 | throw new NotImplementedException(); 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /ConsoleMenu/VisibilityManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace ConsoleTools; 5 | 6 | internal sealed class VisibilityManager 7 | { 8 | private readonly bool[] visibility; 9 | 10 | public VisibilityManager(int size) 11 | { 12 | bool[] visibility = new bool[size]; 13 | for (int i = 0; i < visibility.Length; i++) 14 | { 15 | visibility[i] = true; // true means visible 16 | } 17 | 18 | this.visibility = visibility; 19 | } 20 | 21 | public bool IsVisibleAt(int index) 22 | { 23 | return this.visibility[index]; 24 | } 25 | 26 | public int IndexOfPreviousVisibleItem(int startIndex) 27 | { 28 | int idx = -1; 29 | if (startIndex - 1 >= 0) 30 | { 31 | idx = Array.LastIndexOf(this.visibility, true, startIndex - 1); 32 | } 33 | 34 | if (idx == -1) 35 | { 36 | idx = Array.LastIndexOf(this.visibility, true, this.visibility.Length - 1); 37 | } 38 | 39 | if (idx == -1) 40 | { 41 | idx = startIndex; 42 | } 43 | 44 | return idx; 45 | } 46 | 47 | public int IndexOfNextVisibleItem(int startIndex) 48 | { 49 | int idx = -1; 50 | if (startIndex + 1 < this.visibility.Length) 51 | { 52 | idx = Array.IndexOf(this.visibility, value: true, startIndex + 1); 53 | } 54 | 55 | if (idx == -1) 56 | { 57 | idx = Array.IndexOf(this.visibility, value: true, 0); 58 | } 59 | 60 | if (idx == -1) 61 | { 62 | idx = startIndex; 63 | } 64 | 65 | return idx; 66 | } 67 | 68 | public int IndexOfClosestVisibleItem(int startIndex) 69 | { 70 | // find closest next visible item 71 | var idx = Array.IndexOf(this.visibility, true, startIndex); 72 | if (idx == -1) 73 | { 74 | // find closest previous visible item 75 | idx = Array.LastIndexOf(this.visibility, true, startIndex); 76 | } 77 | 78 | if (idx == -1) 79 | { 80 | idx = 0; 81 | } 82 | 83 | return idx; 84 | } 85 | 86 | public void SetVisibleWithPredicate(List items, Predicate isVisible) 87 | { 88 | for (int i = 0; i < this.visibility.Length; i++) 89 | { 90 | this.visibility[i] = isVisible(items[i]); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /ConsoleMenu/stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | // ACTION REQUIRED: This file was automatically added to your project, but it 3 | // will not take effect until additional steps are taken to enable it. See the 4 | // following page for additional information: 5 | // 6 | // https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/EnableConfiguration.md 7 | 8 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 9 | "settings": { 10 | "documentationRules": { 11 | "documentInternalElements": false, 12 | "documentInterfaces": false, 13 | "excludeFromPunctuationCheck": [ "summary" ] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ConsoleMenuSampleApp/ConsoleMenuSampleApp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net6.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /ConsoleMenuSampleApp/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using ConsoleTools; 6 | 7 | namespace ConsoleMenuSampleApp 8 | { 9 | public class Program 10 | { 11 | private static async Task Main(string[] args) 12 | { 13 | var commonConfig = new MenuConfig 14 | { 15 | Selector = "--> ", 16 | EnableFilter = true, 17 | EnableBreadcrumb = true, 18 | WriteBreadcrumbAction = titles => Console.WriteLine(string.Join(" / ", titles)), 19 | EnableAlphabet = true, 20 | DisableKeyboardNavigation = false, 21 | OutputEncoding = Encoding.Unicode, 22 | }; 23 | 24 | var subMenu1 = new ConsoleMenu(args, level: 2) 25 | .Add("One", () => SomeAction("One1")) 26 | .Add("Sub_Close", ConsoleMenu.Close) 27 | .Add("Sub_Exit", () => Environment.Exit(0)) 28 | .Configure(commonConfig) 29 | .Configure(config => 30 | { 31 | config.Title = "Submenu1"; 32 | }); 33 | 34 | var subMenu = new ConsoleMenu(args, level: 1) 35 | .Add("Sub_One", () => SomeAction("Sub_One")) 36 | .Add("Sub_Two", () => SomeAction("Sub_Two")) 37 | .Add("Sub_Three", () => SomeAction("Sub_Three")) 38 | .Add("Sub_Four", () => SomeAction("Sub_Four")) 39 | .Add("Sub_Five", async (cancellationToken) => await SomeAction2(cancellationToken)) 40 | .Add("Sub_Close", ConsoleMenu.Close) 41 | .Add("Sub_Action then Close", (thisMenu) => { SomeAction("Closing action..."); thisMenu.CloseMenu(); }) 42 | .Add("Sub_Exit", () => Environment.Exit(0)) 43 | .Configure(commonConfig) 44 | .Configure(config => 45 | { 46 | config.Title = "Submenu"; 47 | }); 48 | 49 | var menu = new ConsoleMenu(args, level: 0) 50 | .Add("One", () => SomeAction("One")) 51 | .Add("Two", () => SomeAction("Two")) 52 | .Add("Three", () => SomeAction("Three")) 53 | .Add("Sub \u00BB", subMenu.Show) 54 | .Add("Change me!", (thisMenu) => thisMenu.CurrentItem.Name = "I am changed!") 55 | .Add("Close", ConsoleMenu.Close) 56 | .Add("Action then Close", (thisMenu) => { SomeAction("Closing action..."); thisMenu.CloseMenu(); }) 57 | .Add("Eight", () => SomeAction("Eight")) 58 | .Add("Nine", () => SomeAction("Nine")) 59 | .Add("Ten", () => SomeAction("Ten")) 60 | .Configure(commonConfig) 61 | .Configure(config => 62 | { 63 | config.Title = "Main menu"; 64 | config.EnableWriteTitle = true; 65 | config.EnableBreadcrumb = true; 66 | }); 67 | 68 | for (int i = 0; i < 30; i++) 69 | { 70 | string word = NumberToWords(i + 11); 71 | menu.Add(word, () => SomeAction(word)); 72 | } 73 | 74 | menu.Add("Exit", () => Environment.Exit(0)); 75 | 76 | var token = new CancellationTokenSource(7000).Token; 77 | await menu.ShowAsync(); 78 | } 79 | 80 | private static void SomeAction(string text) 81 | { 82 | Console.WriteLine(text); 83 | Console.WriteLine("Press any key to continue..."); 84 | Console.ReadKey(true); 85 | } 86 | 87 | private static async Task SomeAction2(CancellationToken token) 88 | { 89 | Console.WriteLine("start delay..."); 90 | await Task.Delay(2000, token); 91 | Console.WriteLine("end delay"); 92 | Console.WriteLine("Press any key to continue..."); 93 | Console.ReadKey(true); 94 | } 95 | 96 | private static string NumberToWords(int number) 97 | { 98 | if (number == 0) 99 | return "zero"; 100 | 101 | if (number < 0) 102 | return "minus " + NumberToWords(Math.Abs(number)); 103 | 104 | string words = ""; 105 | 106 | if ((number / 1000000) > 0) 107 | { 108 | words += NumberToWords(number / 1000000) + " million "; 109 | number %= 1000000; 110 | } 111 | 112 | if ((number / 1000) > 0) 113 | { 114 | words += NumberToWords(number / 1000) + " thousand "; 115 | number %= 1000; 116 | } 117 | 118 | if ((number / 100) > 0) 119 | { 120 | words += NumberToWords(number / 100) + " hundred "; 121 | number %= 100; 122 | } 123 | 124 | if (number > 0) 125 | { 126 | if (words != "") 127 | words += "and "; 128 | 129 | var unitsMap = new[] 130 | { 131 | "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve", 132 | "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen" 133 | }; 134 | var tensMap = new[] 135 | { 136 | "zero", "ten", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety" 137 | }; 138 | 139 | if (number < 20) 140 | words += unitsMap[number]; 141 | else 142 | { 143 | words += tensMap[number / 10]; 144 | if ((number % 10) > 0) 145 | words += "-" + unitsMap[number % 10]; 146 | } 147 | } 148 | 149 | return words; 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /ConsoleMenuTests/BreadcrumbsScenarioTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ConsoleMenuTests.TestHelpers; 3 | using ConsoleTools; 4 | using Xunit; 5 | 6 | namespace ConsoleMenuTests 7 | { 8 | public class BreadcrumbsScenarioTest 9 | { 10 | [Fact] 11 | public void Breadcrumbs() 12 | { 13 | var console = new TestConsole(); 14 | console.AddUserInputWithActionBefore("1", () => AssertHelper.Equal(@"First menu 15 | Pick an option: 16 | >> [0] One 17 | [1] Two 18 | [2] Close 19 | [3] Exit 20 | ", console.ToString())); 21 | 22 | console.AddUserInputWithActionBefore("0", () => AssertHelper.Equal(@"First menu > Second menu 23 | Pick an option: 24 | >> [0] Close 25 | ", console.ToString())); 26 | 27 | console.AddUserInputWithActionBefore("2", () => AssertHelper.Equal(@"First menu 28 | Pick an option: 29 | [0] One 30 | >> [1] Two 31 | [2] Close 32 | [3] Exit 33 | ", console.ToString())); 34 | 35 | var submenu = new ConsoleMenu { Console = console } 36 | .Add("Close", ConsoleMenu.Close) 37 | .Configure(m => 38 | { 39 | ConfigHelper.BaseTestConfiguration(m, console); 40 | m.EnableBreadcrumb = true; 41 | m.WriteBreadcrumbAction = titles => console.WriteLine(string.Join(" > ", titles)); 42 | m.Title = "Second menu"; 43 | }); 44 | 45 | var menu = new ConsoleMenu() { Console = console } 46 | .Add("One", () => { }) 47 | .Add("Two", submenu.Show) 48 | .Add("Close", ConsoleMenu.Close) 49 | .Add("Exit", () => Environment.Exit(0)) 50 | .Configure(m => 51 | { 52 | ConfigHelper.BaseTestConfiguration(m, console); 53 | m.EnableBreadcrumb = true; 54 | m.WriteBreadcrumbAction = titles => console.WriteLine(string.Join(" > ", titles)); 55 | m.Title = "First menu"; 56 | }); 57 | menu.Show(); 58 | 59 | AssertHelper.Equal(@"First menu 60 | Pick an option: 61 | [0] One 62 | [1] Two 63 | >> [2] Close 64 | [3] Exit 65 | 66 | ", console.ToString()); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /ConsoleMenuTests/ConsoleMenuTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | 6 | false 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | all 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /ConsoleMenuTests/ExtensionsTest.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using ConsoleTools; 3 | using Xunit; 4 | 5 | namespace ConsoleMenuTests 6 | { 7 | public class ExtensionsTest 8 | { 9 | [Theory] 10 | [InlineData("")] 11 | [InlineData(".", "")] 12 | [InlineData("''", "''")] 13 | [InlineData("1.2.3", "1", "2", "3")] 14 | [InlineData("1.'somet.hing'.3", "1", "'somet.hing'", "3")] 15 | [InlineData("1.''.3", "1", "''", "3")] 16 | public void SplitItems_Test(string input, params string[] expected) 17 | { 18 | List actual = Extensions.SplitItems(input, '.', '\''); 19 | Assert.Equal(expected, actual); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ConsoleMenuTests/FilteringScenarioTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ConsoleMenuTests.TestHelpers; 3 | using ConsoleTools; 4 | using Xunit; 5 | 6 | namespace ConsoleMenuTests 7 | { 8 | public class FilteringScenarioTest 9 | { 10 | [Fact] 11 | public void Filtering_Navigation() 12 | { 13 | var console = new TestConsole(); 14 | console.AddUserInputWithActionBefore("e", () => AssertHelper.Equal(@"Pick an option: 15 | >> [0] One 16 | [1] Two 17 | [2] Close 18 | [3] Exit 19 | Filter: ", console.ToString())); 20 | 21 | console.AddUserInputWithActionBefore(ConsoleKey.DownArrow, () => AssertHelper.Equal(@"Pick an option: 22 | >> [0] One 23 | [2] Close 24 | [3] Exit 25 | Filter: e", console.ToString())); 26 | 27 | console.AddUserInputWithActionBefore(ConsoleKey.UpArrow, () => AssertHelper.Equal(@"Pick an option: 28 | [0] One 29 | >> [2] Close 30 | [3] Exit 31 | Filter: e", console.ToString())); 32 | 33 | console.AddUserInputWithActionBefore(ConsoleKey.UpArrow, () => AssertHelper.Equal(@"Pick an option: 34 | >> [0] One 35 | [2] Close 36 | [3] Exit 37 | Filter: e", console.ToString())); 38 | 39 | console.AddUserInputWithActionBefore(ConsoleKey.UpArrow, () => AssertHelper.Equal(@"Pick an option: 40 | [0] One 41 | [2] Close 42 | >> [3] Exit 43 | Filter: e", console.ToString())); 44 | 45 | console.AddUserInputWithActionBefore(ConsoleKey.Enter, () => AssertHelper.Equal(@"Pick an option: 46 | [0] One 47 | >> [2] Close 48 | [3] Exit 49 | Filter: e", console.ToString())); 50 | 51 | var submenu = new ConsoleMenu { Console = console } 52 | .Add("Close", ConsoleMenu.Close) 53 | .Configure(m => 54 | { 55 | ConfigHelper.BaseTestConfiguration(m, console); 56 | m.EnableFilter = true; 57 | }); 58 | 59 | var menu = new ConsoleMenu() { Console = console } 60 | .Add("One", () => { }) 61 | .Add("Two", submenu.Show) 62 | .Add("Close", ConsoleMenu.Close) 63 | .Add("Exit", () => Environment.Exit(0)) 64 | .Configure(m => 65 | { 66 | ConfigHelper.BaseTestConfiguration(m, console); 67 | m.EnableFilter = true; 68 | }); 69 | menu.Show(); 70 | 71 | AssertHelper.Equal(@"Pick an option: 72 | [0] One 73 | >> [2] Close 74 | [3] Exit 75 | Filter: e 76 | ", console.ToString()); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /ConsoleMenuTests/PreSelectionScenarioTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ConsoleMenuTests.TestHelpers; 3 | using ConsoleTools; 4 | using Xunit; 5 | 6 | namespace ConsoleMenuTests 7 | { 8 | public class PreSelectionScenarioTest 9 | { 10 | [Fact] 11 | public void PreSelection_Simple() 12 | { 13 | var console = new TestConsole(); 14 | 15 | var menu = new ConsoleMenu(args: new[] { "--menu-select=0", }, level: 0) { Console = console } 16 | .Add("One", () => console.Write("Expected action")) 17 | .Add("Close", ConsoleMenu.Close) 18 | .Configure(m => 19 | { 20 | ConfigHelper.BaseTestConfiguration(m, console); 21 | m.ArgsPreselectedItemsKey = "--menu-select="; 22 | }); 23 | menu.Show(); 24 | 25 | Assert.Equal("Expected action", console.ToString(), ignoreLineEndingDifferences: true); 26 | } 27 | 28 | [Fact] 29 | public void PreSelected_SubmenuItem() 30 | { 31 | var console = new TestConsole(); 32 | 33 | var submenu = new ConsoleMenu(args: new[] { "--menu-select=0.1" }, level: 1) { Console = console } 34 | .Add("One", () => { }) 35 | .Add("Two", () => console.Write("Expected action")) 36 | .Add("Close", ConsoleMenu.Close) 37 | .Configure(m => 38 | { 39 | ConfigHelper.BaseTestConfiguration(m, console); 40 | }); 41 | 42 | var menu = new ConsoleMenu(args: new[] { "--menu-select=0.1" }, level: 0) { Console = console } 43 | .Add("One", submenu.Show) 44 | .Add("Close", ConsoleMenu.Close) 45 | .Configure(m => 46 | { 47 | ConfigHelper.BaseTestConfiguration(m, console); 48 | }); 49 | menu.Show(); 50 | 51 | Assert.Equal("Expected action", console.ToString(), ignoreLineEndingDifferences: true); 52 | } 53 | 54 | [Fact] 55 | public void PreSelected_OpenSubmenu() 56 | { 57 | var console = new TestConsole(); 58 | 59 | var submenu = new ConsoleMenu(args: new[] { "--menu-select=0" }, level: 1) { Console = console } 60 | .Add("One1", () => console.Write("Should not be chosen")) 61 | .Add("Close1", ConsoleMenu.Close) 62 | .Configure(m => 63 | { 64 | ConfigHelper.BaseTestConfiguration(m, console); 65 | }); 66 | 67 | var menu = new ConsoleMenu(args: new[] { "--menu-select=0" }, level: 0) { Console = console } 68 | .Add("One0", submenu.Show) 69 | .Add("Close0", ConsoleMenu.Close) 70 | .Configure(m => 71 | { 72 | ConfigHelper.BaseTestConfiguration(m, console); 73 | }); 74 | 75 | console.AddUserInputWithActionBefore("2", () => 76 | { 77 | Assert.Equal(@"Pick an option: 78 | >> [0] One1 79 | [1] Close1 80 | ", console.ToString(), ignoreLineEndingDifferences: true); 81 | }); 82 | 83 | console.AddUserInputWithActionBefore(ConsoleKey.Enter, () => 84 | { 85 | submenu.CloseMenu(); 86 | menu.CloseMenu(); 87 | }); 88 | 89 | menu.Show(); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /ConsoleMenuTests/ReentrySubmenuScenarioTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ConsoleMenuTests.TestHelpers; 3 | using ConsoleTools; 4 | using Xunit; 5 | 6 | namespace ConsoleMenuTests 7 | { 8 | public class ReentrySubmenuScenarioTest 9 | { 10 | [Fact] 11 | public void Reentry_Submenu() 12 | { 13 | var console = new TestConsole(); 14 | console.AddUserInput(ConsoleKey.D1); 15 | console.AddUserInputWithActionBefore(ConsoleKey.D1, () => 16 | { 17 | AssertHelper.Equal(@"Pick an option: 18 | [0] One 19 | >> [1] Close 20 | ", console.ToString()); 21 | }); 22 | 23 | // open submenu once again 24 | console.AddUserInputWithActionBefore(ConsoleKey.D1, () => 25 | { 26 | AssertHelper.Equal(@"Pick an option: 27 | [0] Sub_One 28 | >> [1] Sub_Close 29 | ", console.ToString()); 30 | }); 31 | console.AddUserInputWithActionBefore(ConsoleKey.D1, () => 32 | { 33 | AssertHelper.Equal(@"Pick an option: 34 | [0] One 35 | >> [1] Close 36 | ", console.ToString()); 37 | }); 38 | var submenu = new ConsoleMenu() { Console = console } 39 | .Add("Sub_One", () => { }) 40 | .Add("Sub_Close", ConsoleMenu.Close) 41 | .Configure(m => 42 | { 43 | ConfigHelper.BaseTestConfiguration(m, console); 44 | }); 45 | 46 | var menu = new ConsoleMenu() { Console = console } 47 | .Add("One", submenu.Show) 48 | .Add("Close", ConsoleMenu.Close) 49 | .Configure(m => 50 | { 51 | ConfigHelper.BaseTestConfiguration(m, console); 52 | }); 53 | menu.Show(); 54 | 55 | AssertHelper.Equal(@"Pick an option: 56 | [0] One 57 | >> [1] Close 58 | 59 | ", console.ToString()); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /ConsoleMenuTests/SimpleScenarioTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ConsoleMenuTests.TestHelpers; 3 | using ConsoleTools; 4 | using Xunit; 5 | 6 | namespace ConsoleMenuTests 7 | { 8 | public class SimpleScenarioTest 9 | { 10 | [Fact] 11 | public void SimpleScenario() 12 | { 13 | var console = new TestConsole(); 14 | console.AddUserInput("2"); 15 | 16 | var menu = new ConsoleMenu() { Console = console } 17 | .Add("One", () => { }) 18 | .Add("Two", () => { }) 19 | .Add("Close", ConsoleMenu.Close) 20 | .Add("Exit", () => Environment.Exit(0)) 21 | .Configure(m => 22 | { 23 | ConfigHelper.BaseTestConfiguration(m, console); 24 | }); 25 | menu.Show(); 26 | 27 | Assert.Equal(@"Pick an option: 28 | [0] One 29 | [1] Two 30 | >> [2] Close 31 | [3] Exit 32 | 33 | ", console.ToString(), ignoreLineEndingDifferences: true); 34 | } 35 | 36 | [Fact] 37 | public void SimpleScenario_Colors() 38 | { 39 | var console = new TestConsole { Details = true }; 40 | console.AddUserInput("2"); 41 | 42 | var menu = new ConsoleMenu() { Console = console } 43 | .Add("One", () => { }) 44 | .Add("Two", () => { }) 45 | .Add("Close", ConsoleMenu.Close) 46 | .Add("Exit", () => Environment.Exit(0)) 47 | .Configure(m => 48 | { 49 | ConfigHelper.BaseTestConfiguration(m, console); 50 | }); 51 | menu.Show(); 52 | 53 | Assert.Equal(@"Pick an option: 54 | [0] One 55 | [1] Two 56 | >> [2] Close 57 | [3] Exit 58 | 59 | ", console.ToString(), ignoreLineEndingDifferences: true); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /ConsoleMenuTests/TestConsole.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text; 5 | using ConsoleTools; 6 | 7 | namespace ConsoleMenuTests 8 | { 9 | internal sealed class TestConsole : IConsole 10 | { 11 | private MemoryStream output; 12 | private StreamWriter outputWriter; 13 | private readonly Queue> GetUserInputs = new(); 14 | public bool Details; 15 | 16 | public override string ToString() 17 | { 18 | var pos = this.output.Position; 19 | this.output.Position = 0; 20 | var reader = new StreamReader(this.output); 21 | string text = reader.ReadToEnd(); 22 | this.output.Position = pos; 23 | return text; 24 | } 25 | 26 | public TestConsole() 27 | { 28 | this.output = new MemoryStream(); 29 | this.outputWriter = new StreamWriter(output) { AutoFlush = true }; 30 | this.Title = ""; 31 | } 32 | 33 | public event ConsoleCancelEventHandler? CancelKeyPress; 34 | 35 | public bool IsOutputRedirected => throw new NotImplementedException(); 36 | 37 | public int BufferHeight { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } 38 | public int BufferWidth { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } 39 | 40 | public bool CapsLock => throw new NotImplementedException(); 41 | 42 | public int CursorLeft { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } 43 | public int CursorSize { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } 44 | public int CursorTop { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } 45 | public bool CursorVisible { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } 46 | 47 | public TextWriter Error => throw new NotImplementedException(); 48 | 49 | public ConsoleColor ForegroundColor { get; set; } = ConsoleColor.White; 50 | 51 | public Encoding InputEncoding { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } 52 | 53 | public bool IsErrorRedirected => throw new NotImplementedException(); 54 | 55 | public bool IsInputRedirected => throw new NotImplementedException(); 56 | 57 | public int WindowTop { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } 58 | 59 | public TextReader In => throw new NotImplementedException(); 60 | 61 | public bool KeyAvailable => throw new NotImplementedException(); 62 | 63 | public int LargestWindowWidth => throw new NotImplementedException(); 64 | 65 | public int LargestWindowHeight => throw new NotImplementedException(); 66 | 67 | public bool NumberLock => throw new NotImplementedException(); 68 | 69 | public TextWriter Out => throw new NotImplementedException(); 70 | 71 | public Encoding OutputEncoding { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } 72 | public string Title { get; set; } 73 | public bool TreatControlCAsInput { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } 74 | public int WindowHeight { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } 75 | public int WindowWidth { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } 76 | public int WindowLeft { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } 77 | 78 | public ConsoleColor BackgroundColor { get; set; } 79 | 80 | public void Beep() 81 | { 82 | throw new NotSupportedException("Test Console cannot do this operation."); 83 | } 84 | 85 | public void Beep(int frequency, int duration) 86 | { 87 | throw new NotSupportedException("Test Console cannot do this operation."); 88 | } 89 | 90 | public void Clear() 91 | { 92 | this.output.SetLength(0); 93 | } 94 | 95 | public void MoveBufferArea(int sourceLeft, int sourceTop, int sourceWidth, int sourceHeight, int targetLeft, int targetTop) 96 | { 97 | throw new NotImplementedException(); 98 | } 99 | 100 | public void MoveBufferArea(int sourceLeft, int sourceTop, int sourceWidth, int sourceHeight, int targetLeft, int targetTop, char sourceChar, ConsoleColor sourceForeColor, ConsoleColor sourceBackColor) 101 | { 102 | throw new NotImplementedException(); 103 | } 104 | 105 | public Stream OpenStandardError() 106 | { 107 | throw new NotImplementedException(); 108 | } 109 | 110 | public Stream OpenStandardInput() 111 | { 112 | throw new NotImplementedException(); 113 | } 114 | 115 | public Stream OpenStandardOutput() 116 | { 117 | throw new NotImplementedException(); 118 | } 119 | 120 | public int Read() 121 | { 122 | throw new NotImplementedException(); 123 | } 124 | 125 | public void AddUserInput(ConsoleKey key) 126 | { 127 | string input = ((char)key).ToString(); 128 | AddUserInput(input); 129 | } 130 | 131 | public void AddUserInput(string text) 132 | { 133 | GetUserInputs.Enqueue(() => text); 134 | } 135 | 136 | public void AddUserInputWithActionBefore(ConsoleKey key, Action action) 137 | { 138 | string input = ((char)key).ToString(); 139 | AddUserInputWithActionBefore(input, action); 140 | } 141 | 142 | public void AddUserInputWithActionBefore(string text, Action action) 143 | { 144 | GetUserInputs.Enqueue(() => 145 | { 146 | action(); 147 | return text; 148 | }); 149 | } 150 | 151 | public ConsoleKeyInfo ReadKey(bool intercept) 152 | { 153 | var line = this.GetUserInputs.Dequeue().Invoke(); 154 | if (line.Length > 1) 155 | { 156 | throw new ArgumentException($"input should be single character but was `{line}`"); 157 | } 158 | if (!intercept) 159 | { 160 | this.outputWriter.Write(line); 161 | this.outputWriter.WriteLine("\t//typed from keyboard"); 162 | } 163 | return new ConsoleKeyInfo(line[0], (ConsoleKey)line[0], false, false, false); 164 | } 165 | 166 | public ConsoleKeyInfo ReadKey() 167 | => ReadKey(intercept: false); 168 | 169 | public string ReadLine() 170 | { 171 | var line = this.GetUserInputs.Dequeue().Invoke(); 172 | this.outputWriter.Write(line); 173 | this.outputWriter.WriteLine("\t//typed from keyboard"); 174 | return line; 175 | } 176 | 177 | public void ResetColor() 178 | { 179 | throw new NotImplementedException(); 180 | } 181 | 182 | public void SetBufferSize(int width, int height) 183 | { 184 | throw new NotSupportedException("Test Console cannot do this operation."); 185 | } 186 | 187 | public void SetCursorPosition(int left, int top) 188 | { 189 | throw new NotSupportedException("Test Console cannot do this operation."); 190 | } 191 | 192 | public void SetError(TextWriter newError) 193 | { 194 | throw new NotSupportedException("Test Console cannot do this operation."); 195 | } 196 | 197 | public void SetIn(TextReader newIn) 198 | { 199 | this.output = new MemoryStream(); 200 | this.outputWriter = new StreamWriter(this.output); 201 | } 202 | 203 | public void SetOut(TextWriter newOut) 204 | { 205 | if (!(newOut is StreamWriter streamWriter)) 206 | { 207 | throw new NotSupportedException(newOut.ToString()); 208 | } 209 | this.output = (MemoryStream)streamWriter.BaseStream; 210 | this.outputWriter = streamWriter; 211 | } 212 | 213 | public void SetWindowPosition(int left, int top) 214 | { 215 | throw new NotSupportedException("Test Console cannot do this operation."); 216 | } 217 | 218 | public void SetWindowSize(int width, int height) 219 | { 220 | throw new NotSupportedException("Test Console cannot do this operation."); 221 | } 222 | 223 | public void Write(char[] buffer, int index, int count) 224 | => this.Write(new string(buffer, index, count)); 225 | 226 | public void Write(char[] buffer) 227 | => this.Write(new string(buffer)); 228 | 229 | public void Write(float value) 230 | => this.Write(value.ToString()); 231 | 232 | public void Write(bool value) 233 | => this.Write(value.ToString()); 234 | 235 | public void Write(decimal value) 236 | => this.Write(value.ToString()); 237 | 238 | public void Write(char value) 239 | => this.Write(value.ToString()); 240 | 241 | public void Write(double value) 242 | => this.Write(value.ToString()); 243 | 244 | public void Write(int value) 245 | => this.Write(value.ToString()); 246 | 247 | public void Write(long value) 248 | => this.Write(value.ToString()); 249 | 250 | public void Write(string value) 251 | { 252 | this.outputWriter.Write(value); 253 | 254 | if (this.Details) 255 | { 256 | this.outputWriter.Write($""); 257 | } 258 | } 259 | 260 | public void Write(string format, object arg0) 261 | => this.Write(string.Format(format, arg0)); 262 | 263 | public void Write(string format, object arg0, object arg1) 264 | => this.Write(string.Format(format, arg0, arg1)); 265 | 266 | public void Write(string format, object arg0, object arg1, object arg2) 267 | => this.Write(string.Format(format, arg0, arg1, arg2)); 268 | 269 | public void Write(string format, params object[] arg) 270 | => this.Write(string.Format(format, arg)); 271 | 272 | public void Write(uint value) 273 | => this.Write(value.ToString()); 274 | 275 | public void Write(ulong value) 276 | => this.Write(value.ToString()); 277 | 278 | public void Write(object value) 279 | => this.Write(value.ToString()!); 280 | 281 | public void WriteLine() 282 | => this.outputWriter.WriteLine(); 283 | 284 | public void WriteLine(bool value) 285 | => this.WriteLine(value.ToString()); 286 | 287 | public void WriteLine(char value) 288 | => this.WriteLine(value.ToString()); 289 | 290 | public void WriteLine(char[] buffer) 291 | => this.WriteLine(new string(buffer)); 292 | 293 | public void WriteLine(ulong value) 294 | => this.WriteLine(value.ToString()); 295 | 296 | public void WriteLine(double value) 297 | => this.WriteLine(value.ToString()); 298 | 299 | public void WriteLine(int value) 300 | => this.WriteLine(value.ToString()); 301 | 302 | public void WriteLine(long value) 303 | => this.WriteLine(value.ToString()); 304 | 305 | public void WriteLine(object value) 306 | => this.WriteLine(value.ToString()!); 307 | 308 | public void WriteLine(float value) 309 | => this.WriteLine(value.ToString()); 310 | 311 | public void WriteLine(string value) 312 | { 313 | if (this.Details) 314 | { 315 | this.outputWriter.Write(value); 316 | this.outputWriter.Write($""); 317 | this.outputWriter.WriteLine(); 318 | } 319 | else 320 | { 321 | this.outputWriter.WriteLine(value); 322 | } 323 | } 324 | 325 | public void WriteLine(string format, object arg0) 326 | => this.WriteLine(string.Format(format, arg0)); 327 | 328 | public void WriteLine(string format, object arg0, object arg1) 329 | => this.WriteLine(string.Format(format, arg0, arg1)); 330 | 331 | public void WriteLine(string format, object arg0, object arg1, object arg2) 332 | => this.WriteLine(string.Format(format, arg0, arg1, arg2)); 333 | 334 | public void WriteLine(string format, params object[] arg) 335 | => this.WriteLine(string.Format(format, arg)); 336 | 337 | public void WriteLine(uint value) 338 | => this.WriteLine(value.ToString()); 339 | 340 | public void WriteLine(decimal value) 341 | => this.WriteLine(value.ToString()); 342 | 343 | public void WriteLine(char[] buffer, int index, int count) 344 | => this.WriteLine(new string(buffer, index, count)); 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /ConsoleMenuTests/TestHelpers/AssertHelper.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace ConsoleMenuTests.TestHelpers 4 | { 5 | public static class AssertHelper 6 | { 7 | public static void Equal(string expected, string actual) 8 | { 9 | try 10 | { 11 | Assert.Equal(expected, actual, ignoreLineEndingDifferences: true); 12 | } 13 | catch (Xunit.Sdk.EqualException ex) 14 | { 15 | throw new Xunit.Sdk.XunitException("Expected was not equal to actual", ex); 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ConsoleMenuTests/TestHelpers/ConfigHelper.cs: -------------------------------------------------------------------------------- 1 | using ConsoleTools; 2 | 3 | namespace ConsoleMenuTests.TestHelpers 4 | { 5 | internal static class ConfigHelper 6 | { 7 | public static void BaseTestConfiguration(MenuConfig m, TestConsole console) 8 | { 9 | m.SelectedItemBackgroundColor = console.ForegroundColor; 10 | m.SelectedItemForegroundColor = console.BackgroundColor; 11 | m.ItemBackgroundColor = console.BackgroundColor; 12 | m.ItemForegroundColor = console.ForegroundColor; 13 | m.WriteHeaderAction = () => console.WriteLine("Pick an option:"); 14 | m.WriteItemAction = item => console.Write("[{0}] {1}", item.Index, item.Name); 15 | m.WriteTitleAction = title => console.WriteLine(title); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Leszek Bochenek 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ConsoleMenu 2 | A simple, highly customizable, DOS-like console menu 3 | 4 | ![img](https://raw.githubusercontent.com/lechu445/ConsoleMenu/master/preview.gif) 5 | 6 | Nuget package: https://www.nuget.org/packages/ConsoleMenu-simple 7 | 8 | ## Usage 9 | ```csharp 10 | var subMenu = new ConsoleMenu(args, level: 1) 11 | .Add("Sub_One", () => SomeAction("Sub_One")) 12 | .Add("Sub_Two", () => SomeAction("Sub_Two")) 13 | .Add("Sub_Three", () => SomeAction("Sub_Three")) 14 | .Add("Sub_Four", () => SomeAction("Sub_Four")) 15 | .Add("Sub_Close", ConsoleMenu.Close) 16 | .Configure(config => 17 | { 18 | config.Selector = "--> "; 19 | config.EnableFilter = true; 20 | config.Title = "Submenu"; 21 | config.EnableBreadcrumb = true; 22 | config.WriteBreadcrumbAction = titles => Console.WriteLine(string.Join(" / ", titles)); 23 | }); 24 | 25 | var menu = new ConsoleMenu(args, level: 0) 26 | .Add("One", () => SomeAction("One")) 27 | .Add("Two", () => SomeAction("Two")) 28 | .Add("Three", () => SomeAction("Three")) 29 | .Add("Sub", subMenu.Show) 30 | .Add("Change me", (thisMenu) => thisMenu.CurrentItem.Name = "I am changed!") 31 | .Add("Close", ConsoleMenu.Close) 32 | .Add("Action then Close", (thisMenu) => { SomeAction("Close"); thisMenu.CloseMenu(); }) 33 | .Add("Exit", () => Environment.Exit(0)) 34 | .Configure(config => 35 | { 36 | config.Selector = "--> "; 37 | config.EnableFilter = true; 38 | config.Title = "Main menu"; 39 | config.EnableWriteTitle = true; 40 | config.EnableBreadcrumb = true; 41 | }); 42 | 43 | menu.Show(); 44 | ``` 45 | 46 | ### Running app from console with pre-selected menu items 47 | To do this, use `public ConsoleMenu(string[] args, int level)` constructor during initialization. 48 | Use double quotes for item names and digits for item numbers. Here are some examples: 49 | ```csharp 50 | --menu-select=0.1 //run first at level 0 and second at level 1 51 | --menu-select="Sub.Sub_One.'Close...'" //run "Sub" at level 0 and "Sub_One" at level 1, and "Close..." at level 2 52 | --menu-select="Sub.2" //run item "Sub" at level 0, and then run third item at level 1 53 | ``` 54 | 55 | ### Configuration 56 | You can also define configuration via .Configure() method. The default config looks like: 57 | ```csharp 58 | public class MenuConfig 59 | { 60 | public ConsoleColor SelectedItemBackgroundColor = Console.ForegroundColor; 61 | public ConsoleColor SelectedItemForegroundColor = Console.BackgroundColor; 62 | public ConsoleColor ItemBackgroundColor = Console.BackgroundColor; 63 | public ConsoleColor ItemForegroundColor = Console.ForegroundColor; 64 | public Encoding InputEncoding = Console.InputEncoding; 65 | public Encoding OutputEncoding = Console.OutputEncoding; 66 | public Action WriteHeaderAction = () => Console.WriteLine("Pick an option:"); 67 | public Action WriteItemAction = item => Console.Write("[{0}] {1}", item.Index, item.Name); 68 | public string Selector = ">> "; 69 | public string FilterPrompt = "Filter: "; 70 | public bool ClearConsole = true; 71 | public bool EnableFilter = false; 72 | public string ArgsPreselectedItemsKey = "--menu-select="; 73 | public char ArgsPreselectedItemsValueSeparator = '.'; 74 | public bool EnableWriteTitle = false; 75 | public string Title = "My menu"; 76 | public Action WriteTitleAction = title => Console.WriteLine(title); 77 | public bool EnableBreadcrumb = false; 78 | public Action> WriteBreadcrumbAction = titles => Console.WriteLine(string.Join(" > ", titles)); 79 | public bool EnableAlphabet = false; 80 | } 81 | ``` 82 | Example: 83 | ```csharp 84 | new ConsoleMenu() 85 | .Add("One", () => SomeAction("One")) 86 | .Add("Two", () => SomeAction("Two")) 87 | .Add("Close", ConsoleMenu.Close) 88 | .Configure(config => { config.Selector = "--> "; }) 89 | .Show(); 90 | ``` 91 | ## Requirements 92 | Framework compatible with .NET Standard 1.3 (.NET Core 1.0, .NET Framework 4.6, Mono 4.6) or higher. 93 | -------------------------------------------------------------------------------- /preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lechu445/ConsoleMenu/4183e70974a2c22fb4196abe5b0adbf0deb67ae4/preview.gif --------------------------------------------------------------------------------