├── .editorconfig ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── UPGRADING.md ├── docs └── README.md ├── mkdocs.yml ├── shard.yml ├── spec ├── application_spec.cr ├── application_tester_spec.cr ├── command_spec.cr ├── command_tester_spec.cr ├── commands │ ├── complete_spec.cr │ ├── dump_completion_spec.cr │ ├── help_spec.cr │ ├── lazy_spec.cr │ └── list_spec.cr ├── compiler_spec.cr ├── completion │ ├── input_spec.cr │ └── output │ │ ├── bash_spec.cr │ │ ├── completion_output_test_case.cr │ │ ├── fish_spec.cr │ │ └── zsh_spec.cr ├── cursor_spec.cr ├── descriptor │ ├── abstract_descriptor_test_case.cr │ ├── application_spec.cr │ ├── object_provider.cr │ └── text_spec.cr ├── fixtures │ ├── applications │ │ ├── descriptor1.cr │ │ └── descriptor2.cr │ ├── commands │ │ ├── annotation_configured.cr │ │ ├── annotation_configured_aliases.cr │ │ ├── annotation_configured_hidden.cr │ │ ├── annotation_configured_hidden_field.cr │ │ ├── bar_buc.cr │ │ ├── descriptor1.cr │ │ ├── descriptor2.cr │ │ ├── descriptor3.cr │ │ ├── descriptor4.cr │ │ ├── foo.cr │ │ ├── foo1.cr │ │ ├── foo2.cr │ │ ├── foo3.cr │ │ ├── foo4.cr │ │ ├── foo6.cr │ │ ├── foo_bar.cr │ │ ├── foo_hidden.cr │ │ ├── foo_opt.cr │ │ ├── foo_same_case_lowercase.cr │ │ ├── foo_same_case_uppercase.cr │ │ ├── foo_subnamespaced1.cr │ │ ├── foo_subnamespaced2.cr │ │ ├── foo_without_alias.cr │ │ ├── io.cr │ │ ├── test.cr │ │ ├── test_ambiguous_command_registrering1.cr │ │ └── test_ambiguous_command_registrering2.cr │ ├── helper │ │ └── table │ │ │ ├── borderless.txt │ │ │ ├── borderless_vertical.txt │ │ │ ├── box.txt │ │ │ ├── compact.txt │ │ │ ├── compact_vertical.txt │ │ │ ├── default.txt │ │ │ ├── default_cells_with_colspan.txt │ │ │ ├── default_cells_with_formatting_tags.txt │ │ │ ├── default_cells_with_non_formatting_tags.txt │ │ │ ├── default_cells_with_rowspan.txt │ │ │ ├── default_cells_with_rowspan_and_colspan.txt │ │ │ ├── default_cells_with_rowspan_and_colspan_and_alignment.txt │ │ │ ├── default_cells_with_rowspan_and_colspan_and_custom_format.txt │ │ │ ├── default_cells_with_rowspan_and_colspan_and_fgbg.txt │ │ │ ├── default_cells_with_rowspan_and_colspan_and_line_breaks.txt │ │ │ ├── default_cells_with_rowspan_and_colspan_no_separators.txt │ │ │ ├── default_cells_with_rowspan_and_colspan_separator_in_rowspan.txt │ │ │ ├── default_colspan_and_table_cell_with_comment_style.txt │ │ │ ├── default_formatted_row_with_line_breaks.txt │ │ │ ├── default_headerless.txt │ │ │ ├── default_line_break_after_colspan_cell.txt │ │ │ ├── default_line_breaks_after_colspan_cell.txt │ │ │ ├── default_missing_cell_values.txt │ │ │ ├── default_multiline_cells.txt │ │ │ ├── default_multiple_header_lines.txt │ │ │ ├── default_no_rows.txt │ │ │ ├── default_row_with_multiple_cells.txt │ │ │ ├── double_box_separator.txt │ │ │ ├── markdown.txt │ │ │ └── suggested_vertical.txt │ ├── style │ │ ├── backslashes.txt │ │ ├── block.txt │ │ ├── block_line_endings.txt │ │ ├── block_no_prefix_type.txt │ │ ├── block_padding.txt │ │ ├── block_prefix_no_type.txt │ │ ├── blocks.txt │ │ ├── closing_tag.txt │ │ ├── definition_list.txt │ │ ├── emojis.txt │ │ ├── empty_buffer.txt │ │ ├── horizontal_table.txt │ │ ├── long_line_block.txt │ │ ├── long_line_block_wrapping.txt │ │ ├── long_line_comment.txt │ │ ├── long_line_comment_decorated.txt │ │ ├── multi_line_block.txt │ │ ├── nested_tag_prefix.txt │ │ ├── non_interactive_question.txt │ │ ├── table.txt │ │ ├── table_horizontal.txt │ │ ├── table_vertical.txt │ │ ├── text_block_blank_line.txt │ │ ├── title_block.txt │ │ ├── titles.txt │ │ └── titles_text.txt │ └── text │ │ ├── application_1.txt │ │ ├── application_2.txt │ │ ├── application_alternative_namespace.txt │ │ ├── application_filtered_namespace.txt │ │ ├── application_renderexception1.txt │ │ ├── application_renderexception2.txt │ │ ├── application_renderexception3.txt │ │ ├── application_renderexception3_decorated.txt │ │ ├── application_renderexception4.txt │ │ ├── application_renderexception_doublewidth1.txt │ │ ├── application_renderexception_escapeslines.txt │ │ ├── application_renderexception_linebreaks.txt │ │ ├── application_renderexception_synopsis_escapeslines.txt │ │ ├── application_run1.txt │ │ ├── application_run2.txt │ │ ├── application_run3.txt │ │ ├── application_run4.txt │ │ ├── application_run5.txt │ │ ├── command_1.txt │ │ ├── command_2.txt │ │ ├── input_argument_1.txt │ │ ├── input_argument_2.txt │ │ ├── input_argument_3.txt │ │ ├── input_argument_4.txt │ │ ├── input_argument_with_style.txt │ │ ├── input_definition_1.txt │ │ ├── input_definition_2.txt │ │ ├── input_definition_3.txt │ │ ├── input_definition_4.txt │ │ ├── input_option_1.txt │ │ ├── input_option_2.txt │ │ ├── input_option_3.txt │ │ ├── input_option_4.txt │ │ ├── input_option_5.txt │ │ ├── input_option_6.txt │ │ ├── input_option_with_style.txt │ │ └── input_option_with_style_array.txt ├── formatter │ ├── null_spec.cr │ ├── null_style_spec.cr │ ├── output_formatter_spec.cr │ ├── output_formatter_style_spec.cr │ └── output_formatter_style_stack_spec.cr ├── helper │ ├── abstract_question_helper_test_case.cr │ ├── athena_question_spec.cr │ ├── formatter_spec.cr │ ├── helper_spec.cr │ ├── output_wrapper_spec.cr │ ├── progress_bar_spec.cr │ ├── progress_indicator_spec.cr │ ├── question_spec.cr │ ├── table_spec.cr │ └── table_style_spec.cr ├── input │ ├── argument_spec.cr │ ├── argv_spec.cr │ ├── definition_spec.cr │ ├── hash_spec.cr │ ├── input_spec.cr │ ├── option_spec.cr │ ├── string_line_spec.cr │ └── value │ │ ├── array_spec.cr │ │ ├── bool_spec.cr │ │ ├── nil_spec.cr │ │ ├── number_spec.cr │ │ └── string_spec.cr ├── output │ ├── console_section_output_spec.cr │ ├── io_spec.cr │ ├── null_spec.cr │ └── output_spec.cr ├── question │ ├── choice_spec.cr │ ├── confirmation_spec.cr │ ├── multiple_choice_spec.cr │ └── question_spec.cr ├── spec_helper.cr ├── style │ └── athena_style_spec.cr └── terminal_spec.cr └── src ├── annotations.cr ├── application.cr ├── athena-console.cr ├── command.cr ├── commands ├── complete.cr ├── dump_completion.cr ├── generic.cr ├── help.cr ├── lazy.cr └── list.cr ├── completion ├── input.cr ├── output │ ├── bash.cr │ ├── completion.bash │ ├── completion.fish │ ├── completion.zsh │ ├── fish.cr │ ├── interface.cr │ └── zsh.cr └── suggestions.cr ├── cursor.cr ├── descriptor ├── application.cr ├── context.cr ├── descriptor.cr ├── interface.cr └── text.cr ├── exception ├── command_not_found.cr ├── invalid_argument.cr ├── invalid_option.cr ├── logic.cr ├── missing_input.cr ├── namespace_not_found.cr └── runtime.cr ├── ext └── terminal.cr ├── formatter ├── interface.cr ├── null.cr ├── null_style.cr ├── output.cr ├── output_formatter_style_stack.cr ├── output_style.cr ├── output_style_interface.cr └── wrappable_interface.cr ├── helper ├── athena_question.cr ├── descriptor_helper.cr ├── formatter.cr ├── helper.cr ├── helper_set.cr ├── interface.cr ├── output_wrapper.cr ├── progress_bar.cr ├── progress_indicator.cr ├── question.cr ├── table.cr ├── table_cell_style.cr └── table_style.cr ├── input ├── argument.cr ├── argv.cr ├── definition.cr ├── hash.cr ├── input.cr ├── interface.cr ├── option.cr ├── streamable.cr ├── string_line.cr └── value │ ├── array.cr │ ├── bool.cr │ ├── nil.cr │ ├── number.cr │ ├── string.cr │ └── value.cr ├── loader ├── factory.cr └── interface.cr ├── output ├── console_output.cr ├── console_output_interface.cr ├── interface.cr ├── io.cr ├── null.cr ├── output.cr ├── section.cr ├── sized_buffer.cr ├── type.cr └── verbosity.cr ├── question ├── abstract_choice.cr ├── base.cr ├── choice.cr ├── confirmation.cr ├── multiple_choice.cr └── question.cr ├── spec.cr ├── spec └── expectations │ └── command_is_successful.cr ├── style ├── athena.cr ├── interface.cr └── output.cr └── terminal.cr /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.cr] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /lib/ 2 | /bin/ 3 | /.shards/ 4 | *.dwarf 5 | 6 | # Libraries don't need dependency lock 7 | # Dependencies will be locked in applications that use them 8 | /shard.lock 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This repository is a read-only mirror. Please refer the [main Athena repository](https://github.com/athena-framework/athena/blob/master/CONTRIBUTING.md) on how to start contributing. 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 George Dietrich 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Console 2 | 3 | [![Common Changelog](https://common-changelog.org/badge.svg)](https://common-changelog.org) 4 | [![CI](https://github.com/athena-framework/athena/workflows/CI/badge.svg)](https://github.com/athena-framework/athena/actions/workflows/ci.yml) 5 | [![Latest release](https://img.shields.io/github/release/athena-framework/console.svg)](https://github.com/athena-framework/console/releases) 6 | 7 | Allows for the creation of CLI based commands. 8 | 9 | ## Getting Started 10 | 11 | Checkout the [Documentation](https://athenaframework.org/Console). 12 | 13 | ## Contributing 14 | 15 | Read the general [Contributing Guide](./CONTRIBUTING.md) for information on how to get started. 16 | -------------------------------------------------------------------------------- /UPGRADING.md: -------------------------------------------------------------------------------- 1 | # Upgrading 2 | 3 | Documents the changes that may be required when upgrading to a newer component version. 4 | 5 | ## Upgrade to 0.4.0 6 | 7 | ### New `ACON::Output::Verbosity::SILENT` verbosity level 8 | 9 | Existing commands that define a `--silent` option will have to be renamed. 10 | 11 | ### Normalization of Exception types 12 | 13 | The namespace exception types live in has changed from `ACON::Exceptions` to `ACON::Exception`. 14 | Any usages of `console` exception types will need to be updated. 15 | 16 | Some additional types have also been removed/renamed: 17 | 18 | * `ACON::Exceptions::ConsoleException` has been removed in favor of using `ACON::Exception` directly 19 | * `ACON::Exceptions::RuntimeError` has been renamed `ACON::Exception::Runtime` 20 | * `ACON::Exceptions::ValidationError` has been removed with past usages now raising an `ACON::Exception::Runtime` error 21 | 22 | If using a `rescue` statement with a parent exception type, either from the `console` component or Crystal stdlib, double check it to ensure it'll still rescue what you are expecting it will. 23 | 24 | ## Upgrade to 0.3.6 25 | 26 | ### `ACON::Application` version is now represented as a `String` 27 | 28 | If passing a [SemanticVersion](https://crystal-lang.org/api/SemanticVersion.html) as the *version* of an `ACON::Application`, call `#to_s` on it or ideally pass a semver `String` directly. 29 | If using the `#version` getter off the `ACON::Application`, your code will need to adapt to it now being a `String`. 30 | Either by manually constructing a `SemanticVersion` or ideally just supporting the returned `String`. 31 | 32 | ## Upgrade to 0.3.3 33 | 34 | ### New `ACON::Style::Interface` methods 35 | 36 | If implementing a custom style, you will now need to implement the following methods: 37 | 38 | - `abstract def progress_start(max : Int32? = nil) : Nil` 39 | - `abstract def progress_advance(by step : Int32 = 1) : Nil` 40 | - `abstract def progress_finish : Nil` 41 | 42 | These should use an internal `ACON::Helper::ProgressBar` customized to fit your style that delegates to the related methods. 43 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | INHERIT: ../../../mkdocs-common.yml 2 | 3 | site_name: Console 4 | site_url: https://athenaframework.org/Console/ 5 | repo_url: https://github.com/athena-framework/console 6 | 7 | nav: 8 | - Introduction: README.md 9 | - Back to Manual: project://. 10 | - API: 11 | - Aliases: aliases.md 12 | - Top Level: top_level.md 13 | - '*' 14 | 15 | plugins: 16 | - search 17 | - section-index 18 | - literate-nav 19 | - gen-files: 20 | scripts: 21 | - ../../../gen_doc_stubs.py 22 | - mkdocstrings: 23 | default_handler: crystal 24 | custom_templates: ../../../docs/templates 25 | handlers: 26 | crystal: 27 | crystal_docs_flags: 28 | - ../../../docs/index.cr 29 | - ../../../lib/athena-console/src/athena-console.cr 30 | - ../../../lib/athena-console/src/spec.cr 31 | source_locations: 32 | lib/athena-console: https://github.com/athena-framework/console/blob/v{shard_version}/{file}#L{line} 33 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: athena-console 2 | 3 | version: 0.4.1 4 | 5 | crystal: ~> 1.13 6 | 7 | license: MIT 8 | 9 | repository: https://github.com/athena-framework/console 10 | 11 | documentation: https://athenaframework.org/Console 12 | 13 | description: | 14 | Allows the creation of CLI based commands. 15 | 16 | authors: 17 | - George Dietrich 18 | 19 | dependencies: 20 | athena-clock: 21 | github: athena-framework/clock 22 | version: ~> 0.2.0 23 | -------------------------------------------------------------------------------- /spec/application_tester_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | struct ApplicationTesterTest < ASPEC::TestCase 4 | @app : ACON::Application 5 | @tester : ACON::Spec::ApplicationTester 6 | 7 | def initialize 8 | @app = ACON::Application.new "foo" 9 | @app.auto_exit = false 10 | 11 | @app.register "foo" do |_, output| 12 | output.puts "foo" 13 | 14 | ACON::Command::Status::SUCCESS 15 | end.argument "foo" 16 | 17 | @tester = ACON::Spec::ApplicationTester.new @app 18 | @tester.run command: "foo", foo: "bar", interactive: false, decorated: false, verbosity: :verbose 19 | end 20 | 21 | def test_run : Nil 22 | @tester.input.interactive?.should be_false 23 | @tester.output.decorated?.should be_false 24 | @tester.output.verbosity.verbose?.should be_true 25 | end 26 | 27 | def test_input : Nil 28 | @tester.input.argument("foo").should eq "bar" 29 | end 30 | 31 | def test_output : Nil 32 | @tester.output.to_s.should eq "foo#{EOL}" 33 | end 34 | 35 | def test_display : Nil 36 | @tester.display.to_s.should eq "foo#{EOL}" 37 | end 38 | 39 | def test_status : Nil 40 | @tester.status.should eq ACON::Command::Status::SUCCESS 41 | end 42 | 43 | def test_inputs : Nil 44 | app = ACON::Application.new "foo" 45 | app.auto_exit = false 46 | app.register "foo" do |input, output| 47 | helper = ACON::Helper::Question.new 48 | 49 | helper.ask input, output, ACON::Question(String?).new "Q1", nil 50 | helper.ask input, output, ACON::Question(String?).new "Q2", nil 51 | helper.ask input, output, ACON::Question(String?).new "Q3", nil 52 | 53 | ACON::Command::Status::SUCCESS 54 | end 55 | 56 | tester = ACON::Spec::ApplicationTester.new app 57 | tester.inputs = ["A1", "A2", "A3"] 58 | tester.run command: "foo" 59 | 60 | tester.status.should eq ACON::Command::Status::SUCCESS 61 | tester.display.should eq "Q1Q2Q3" 62 | end 63 | 64 | def test_error_output : Nil 65 | app = ACON::Application.new "foo" 66 | app.auto_exit = false 67 | app.register "foo" do |_, output| 68 | output.as(ACON::Output::ConsoleOutput).error_output.print "foo" 69 | 70 | ACON::Command::Status::SUCCESS 71 | end.argument "foo" 72 | 73 | tester = ACON::Spec::ApplicationTester.new app 74 | tester.run command: "foo", foo: "bar", capture_stderr_separately: true 75 | 76 | tester.error_output.should eq "foo" 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /spec/commands/dump_completion_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | struct DumpCompletionCommandTest < ASPEC::TestCase 4 | @[DataProvider("complete_provider")] 5 | def test_complete(input : Array(String), expected_suggestions : Array(String)) : Nil 6 | tester = ACON::Spec::CommandCompletionTester.new ACON::Commands::DumpCompletion.new 7 | suggestions = tester.complete input 8 | 9 | suggestions.should eq expected_suggestions 10 | end 11 | 12 | def complete_provider : Hash 13 | { 14 | "shell" => {[] of String, ["bash", "fish", "zsh"]}, 15 | } 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/commands/help_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | struct HelpCommandTest < ASPEC::TestCase 4 | def test_execute_alias : Nil 5 | command = ACON::Commands::Help.new 6 | command.application = ACON::Application.new "foo" 7 | 8 | tester = ACON::Spec::CommandTester.new command 9 | tester.execute command_name: "li", decorated: false 10 | 11 | tester.display.should contain "list [options] [--] []" 12 | tester.display.should contain "format=FORMAT" 13 | tester.display.should contain "raw" 14 | end 15 | 16 | def test_execute : Nil 17 | command = ACON::Commands::Help.new 18 | command.application = ACON::Application.new "foo" 19 | 20 | tester = ACON::Spec::CommandTester.new command 21 | tester.execute command_name: "li", decorated: false 22 | 23 | tester.display.should contain "list [options] [--] []" 24 | tester.display.should contain "format=FORMAT" 25 | tester.display.should contain "raw" 26 | end 27 | 28 | def test_execute_application_command : Nil 29 | app = ACON::Application.new "foo" 30 | tester = ACON::Spec::CommandTester.new app.get "help" 31 | tester.execute command_name: "list" 32 | 33 | tester.display.should contain "list [options] [--] []" 34 | tester.display.should contain "format=FORMAT" 35 | tester.display.should contain "raw" 36 | end 37 | 38 | @[DataProvider("complete_provider")] 39 | def test_complete(input : Array(String), expected_suggestions : Array(String)) : Nil 40 | app = ACON::Application.new "foo" 41 | app.add FooCommand.new 42 | 43 | tester = ACON::Spec::CommandCompletionTester.new app.get "help" 44 | suggestions = tester.complete input 45 | 46 | suggestions.should eq expected_suggestions 47 | end 48 | 49 | def complete_provider : Hash 50 | { 51 | "long option" => {["--format"], ["txt"]}, 52 | "nothing" => {[] of String, ["completion", "help", "list", "foo:bar"]}, 53 | "command name" => {["f"], ["completion", "help", "list", "foo:bar"]}, 54 | } 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/commands/lazy_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | @[ACONA::AsCommand("blahhhh")] 4 | private class MockCommand < ACON::Command 5 | protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status 6 | ACON::Command::Status::SUCCESS 7 | end 8 | end 9 | 10 | describe ACON::Commands::Lazy do 11 | it "applies metadata to the instantiated command" do 12 | lazy_command = ACON::Commands::Lazy.new "cmd_name", ["foo", "bar"], "description", true, -> { MockCommand.new.as ACON::Command } 13 | command = lazy_command.command 14 | 15 | command.should be_a MockCommand 16 | command.name.should eq "cmd_name" 17 | command.aliases.should eq ["foo", "bar"] 18 | command.description.should eq "description" 19 | command.hidden?.should be_true 20 | end 21 | 22 | it "forwards methods to the wrapped command instance" do 23 | mock_command = MockCommand.new 24 | 25 | lazy_command = ACON::Commands::Lazy.new "cmd_name", ["foo", "bar"], "description", true, -> { mock_command.as ACON::Command } 26 | command = lazy_command.command 27 | 28 | command.helper_set = ACON::Helper::HelperSet.new ACON::Helper::Question.new 29 | command.process_title "title" 30 | command.usage "usages" 31 | command.argument "name" 32 | command.option "active" 33 | 34 | command.definition.should eq mock_command.definition 35 | command.help.should eq mock_command.help 36 | command.processed_help.should eq mock_command.processed_help 37 | command.synopsis.should eq mock_command.synopsis 38 | command.usages.should eq mock_command.usages 39 | command.helper(ACON::Helper::Question).should eq mock_command.helper(ACON::Helper::Question) 40 | end 41 | 42 | it "is runnable" do 43 | command = MockCommand.new 44 | command.application = ACON::Application.new "foo" 45 | 46 | tester = ACON::Spec::CommandTester.new command 47 | tester.execute.should eq ACON::Command::Status::SUCCESS 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/compiler_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe Athena::Console do 4 | describe "compiler errors", tags: "compiled" do 5 | describe "when a command configured via annotation doesn't have a name" do 6 | it "non hidden no aliases" do 7 | ASPEC::Methods.assert_error "Console command 'NoNameCommand' has an 'ACONA::AsCommand' annotation but is missing the commands's name. It was not provided as the first positional argument nor via the 'name' field.", <<-CR 8 | require "./spec_helper.cr" 9 | 10 | @[ACONA::AsCommand] 11 | class NoNameCommand < ACON::Command 12 | protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status 13 | ACON::Command::Status::SUCCESS 14 | end 15 | end 16 | 17 | NoNameCommand.default_name 18 | CR 19 | end 20 | 21 | it "hidden" do 22 | ASPEC::Methods.assert_error "Console command 'NoNameCommand' has an 'ACONA::AsCommand' annotation but is missing the commands's name. It was not provided as the first positional argument nor via the 'name' field.", <<-CR 23 | require "./spec_helper.cr" 24 | 25 | @[ACONA::AsCommand(hidden: true)] 26 | class NoNameCommand < ACON::Command 27 | protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status 28 | ACON::Command::Status::SUCCESS 29 | end 30 | end 31 | 32 | NoNameCommand.default_name 33 | CR 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/completion/output/bash_spec.cr: -------------------------------------------------------------------------------- 1 | require "./completion_output_test_case" 2 | 3 | struct BashTest < CompletionOutputTestCase 4 | def completion_output : ACON::Completion::Output::Interface 5 | ACON::Completion::Output::Bash.new 6 | end 7 | 8 | def expected_options_output : String 9 | "--option1\n--negatable\n--no-negatable#{EOL}" 10 | end 11 | 12 | def expected_values_output : String 13 | "Green\nRed\nYellow#{EOL}" 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/completion/output/completion_output_test_case.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | 3 | abstract struct CompletionOutputTestCase < ASPEC::TestCase 4 | abstract def completion_output : ACON::Completion::Output::Interface 5 | abstract def expected_options_output : String 6 | abstract def expected_values_output : String 7 | 8 | def test_options : Nil 9 | options = [ 10 | ACON::Input::Option.new("option1", "o", :none, "First Option"), 11 | ACON::Input::Option.new("negatable", nil, :negatable, "Can be negative"), 12 | ] 13 | 14 | suggestions = ACON::Completion::Suggestions.new 15 | suggestions.suggest_options options 16 | 17 | buffer = IO::Memory.new 18 | 19 | self.completion_output.write suggestions, ACON::Output::IO.new buffer 20 | 21 | buffer.to_s.should eq self.expected_options_output 22 | end 23 | 24 | def test_values : Nil 25 | suggestions = ACON::Completion::Suggestions.new 26 | suggestions.suggest_value "Green", "Beans are green" 27 | suggestions.suggest_value "Red", "Roses are red" 28 | suggestions.suggest_value "Yellow", "Canaries are yellow" 29 | 30 | buffer = IO::Memory.new 31 | 32 | self.completion_output.write suggestions, ACON::Output::IO.new buffer 33 | 34 | buffer.to_s.should eq self.expected_values_output 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/completion/output/fish_spec.cr: -------------------------------------------------------------------------------- 1 | require "./completion_output_test_case" 2 | 3 | struct FishTest < CompletionOutputTestCase 4 | def completion_output : ACON::Completion::Output::Interface 5 | ACON::Completion::Output::Fish.new 6 | end 7 | 8 | def expected_options_output : String 9 | "--option1\n--negatable\n--no-negatable" 10 | end 11 | 12 | def expected_values_output : String 13 | "Green\nRed\nYellow" 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/completion/output/zsh_spec.cr: -------------------------------------------------------------------------------- 1 | require "./completion_output_test_case" 2 | 3 | struct ZshTest < CompletionOutputTestCase 4 | def completion_output : ACON::Completion::Output::Interface 5 | ACON::Completion::Output::Zsh.new 6 | end 7 | 8 | def expected_options_output : String 9 | "--option1\tFirst Option\n--negatable\tCan be negative\n--no-negatable\tCan be negative\n" 10 | end 11 | 12 | def expected_values_output : String 13 | "Green\tBeans are green\nRed\tRoses are red\nYellow\tCanaries are yellow\n" 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/cursor_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | struct CursorTest < ASPEC::TestCase 4 | @cursor : ACON::Cursor 5 | @output : ACON::Output::IO 6 | 7 | def initialize 8 | @output = ACON::Output::IO.new IO::Memory.new 9 | @cursor = ACON::Cursor.new @output 10 | end 11 | 12 | def test_move_up_one_line : Nil 13 | @cursor.move_up 14 | @output.to_s.should eq "\x1b[1A" 15 | end 16 | 17 | def test_move_up_multiple_lines : Nil 18 | @cursor.move_up 12 19 | @output.to_s.should eq "\x1b[12A" 20 | end 21 | 22 | def test_move_down_one_line : Nil 23 | @cursor.move_down 24 | @output.to_s.should eq "\x1b[1B" 25 | end 26 | 27 | def test_move_down_multiple_lines : Nil 28 | @cursor.move_down 12 29 | @output.to_s.should eq "\x1b[12B" 30 | end 31 | 32 | def test_move_right_one_line : Nil 33 | @cursor.move_right 34 | @output.to_s.should eq "\x1b[1C" 35 | end 36 | 37 | def test_move_right_multiple_lines : Nil 38 | @cursor.move_right 12 39 | @output.to_s.should eq "\x1b[12C" 40 | end 41 | 42 | def test_move_left_one_line : Nil 43 | @cursor.move_left 44 | @output.to_s.should eq "\x1b[1D" 45 | end 46 | 47 | def test_move_left_multiple_lines : Nil 48 | @cursor.move_left 12 49 | @output.to_s.should eq "\x1b[12D" 50 | end 51 | 52 | def test_move_to_column : Nil 53 | @cursor.move_to_column 5 54 | @output.to_s.should eq "\x1b[5G" 55 | end 56 | 57 | def test_move_to_position : Nil 58 | @cursor.move_to_position 18, 16 59 | @output.to_s.should eq "\x1b[17;18H" 60 | end 61 | 62 | def test_clear_line : Nil 63 | @cursor.clear_line 64 | @output.to_s.should eq "\x1b[2K" 65 | end 66 | 67 | def test_clear_line_after : Nil 68 | @cursor.clear_line_after 69 | @output.to_s.should eq "\x1b[K" 70 | end 71 | 72 | def test_clear_screen : Nil 73 | @cursor.clear_screen 74 | @output.to_s.should eq "\x1b[2J" 75 | end 76 | 77 | def test_save_position : Nil 78 | @cursor.save_position 79 | @output.to_s.should eq "\x1b7" 80 | end 81 | 82 | def test_restore_position : Nil 83 | @cursor.restore_position 84 | @output.to_s.should eq "\x1b8" 85 | end 86 | 87 | def test_hide : Nil 88 | @cursor.hide 89 | @output.to_s.should eq "\x1b[?25l" 90 | end 91 | 92 | def test_show : Nil 93 | @cursor.show 94 | @output.to_s.should eq "\x1b[?25h\x1b[?0c" 95 | end 96 | 97 | def test_clear_output : Nil 98 | @cursor.clear_output 99 | @output.to_s.should eq "\x1b[0J" 100 | end 101 | 102 | def test_current_position : Nil 103 | @cursor = ACON::Cursor.new @output, IO::Memory.new 104 | 105 | @cursor.move_to_position 10, 10 106 | position = @cursor.current_position 107 | 108 | @output.to_s.should eq "\x1b[11;10H" 109 | 110 | position.should eq({1, 1}) 111 | end 112 | 113 | def test_current_position_tty : Nil 114 | pending! "Cursor input must be a TTY" unless STDIN.tty? 115 | 116 | @cursor = ACON::Cursor.new @output 117 | 118 | @cursor.move_to_position 10, 10 119 | position = @cursor.current_position 120 | 121 | @output.to_s.should eq "\x1b[11;10H" 122 | 123 | position.should_not eq({1, 1}) 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /spec/descriptor/abstract_descriptor_test_case.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | require "./object_provider" 3 | 4 | abstract struct AbstractDescriptorTestCase < ASPEC::TestCase 5 | @[DataProvider("input_argument_test_data")] 6 | def test_describe_input_argument(object : ACON::Input::Argument, expected : String) : Nil 7 | self.assert_description expected, object 8 | end 9 | 10 | @[DataProvider("input_option_test_data")] 11 | def test_describe_input_option(object : ACON::Input::Option, expected : String) : Nil 12 | self.assert_description expected, object 13 | end 14 | 15 | @[DataProvider("input_definition_test_data")] 16 | def test_describe_input_definition(object : ACON::Input::Definition, expected : String) : Nil 17 | self.assert_description expected, object 18 | end 19 | 20 | @[DataProvider("command_test_data")] 21 | def test_describe_command(object : ACON::Command, expected : String) : Nil 22 | self.assert_description expected, object 23 | end 24 | 25 | @[DataProvider("application_test_data")] 26 | def test_describe_application(object : ACON::Application, expected : String) : Nil 27 | self.assert_description expected, object 28 | end 29 | 30 | def input_argument_test_data : Array 31 | self.description_test_data ObjectProvider.input_arguments 32 | end 33 | 34 | def input_option_test_data : Array 35 | self.description_test_data ObjectProvider.input_options 36 | end 37 | 38 | def input_definition_test_data : Array 39 | self.description_test_data ObjectProvider.input_definitions 40 | end 41 | 42 | def command_test_data : Array 43 | self.description_test_data ObjectProvider.commands 44 | end 45 | 46 | def application_test_data : Array 47 | self.description_test_data ObjectProvider.applications 48 | end 49 | 50 | protected abstract def descriptor : ACON::Descriptor::Interface 51 | protected abstract def format : String 52 | 53 | protected def description_test_data(data : Hash(String, _)) : Array 54 | data.map do |k, v| 55 | normalized_path = File.join __DIR__, "..", "fixtures", "text" 56 | {v, File.read "#{normalized_path}/#{k}.#{self.format}"} 57 | end 58 | end 59 | 60 | protected def assert_description(expected : String, object, context : ACON::Descriptor::Context = ACON::Descriptor::Context.new) : Nil 61 | output = ACON::Output::IO.new IO::Memory.new 62 | context = context.clone 63 | context.raw_output = true 64 | self.descriptor.describe output, object, context 65 | self.normalize_output(output.to_s).should eq self.normalize_output(expected) 66 | end 67 | 68 | private def normalize_output(output : String) : String 69 | output.gsub(EOL, "\n").strip 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /spec/descriptor/application_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | private class TestApplication < ACON::Application 4 | protected def default_commands : Array(ACON::Command) 5 | [] of ACON::Command 6 | end 7 | end 8 | 9 | struct ApplicationDescriptorTest < ASPEC::TestCase 10 | @[DataProvider("namespace_provider")] 11 | def test_namespaces(expected : Array(String), names : Array(String)) : Nil 12 | app = TestApplication.new "foo" 13 | 14 | names.each do |name| 15 | app.register name do 16 | ACON::Command::Status::SUCCESS 17 | end 18 | end 19 | 20 | ACON::Descriptor::Application.new(app).namespaces.keys.should eq expected 21 | end 22 | 23 | def namespace_provider : Tuple 24 | { 25 | {["_global"], ["foobar"]}, 26 | {["a", "b"], ["b:foo", "a:foo", "b:bar"]}, 27 | {["_global", "22", "33", "b", "z"], ["z:foo", "1", "33:foo", "b:foo", "22:foo:bar"]}, 28 | } 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/descriptor/object_provider.cr: -------------------------------------------------------------------------------- 1 | module ObjectProvider 2 | def self.input_arguments : Hash(String, ACON::Input::Argument) 3 | { 4 | "input_argument_1" => ACON::Input::Argument.new("argument_name", :required), 5 | "input_argument_2" => ACON::Input::Argument.new("argument_name", :is_array, "argument description"), 6 | "input_argument_3" => ACON::Input::Argument.new("argument_name", :optional, "argument description", "default_value"), 7 | "input_argument_4" => ACON::Input::Argument.new("argument_name", :required, "multiline\nargument description"), 8 | "input_argument_with_style" => ACON::Input::Argument.new("argument_name", :optional, "argument description", "style"), 9 | } 10 | end 11 | 12 | def self.input_options : Hash(String, ACON::Input::Option) 13 | { 14 | "input_option_1" => ACON::Input::Option.new("option_name", "o", :none), 15 | "input_option_2" => ACON::Input::Option.new("option_name", "o", :optional, "option description", "default_value"), 16 | "input_option_3" => ACON::Input::Option.new("option_name", "o", :required, "option description"), 17 | "input_option_4" => ACON::Input::Option.new("option_name", "o", ACON::Input::Option::Value[:optional, :is_array], "option description", Array(String).new), 18 | "input_option_5" => ACON::Input::Option.new("option_name", "o", :required, "multiline\noption description"), 19 | "input_option_6" => ACON::Input::Option.new("option_name", {"o", "O"}, :required, "option with multiple shortcuts"), 20 | "input_option_with_style" => ACON::Input::Option.new("option_name", "o", :required, "option description", "style"), 21 | "input_option_with_style_array" => ACON::Input::Option.new("option_name", "o", ACON::Input::Option::Value[:required, :is_array], "option description", ["Hello", "world"]), 22 | } 23 | end 24 | 25 | def self.input_definitions : Hash(String, ACON::Input::Definition) 26 | { 27 | "input_definition_1" => ACON::Input::Definition.new, 28 | "input_definition_2" => ACON::Input::Definition.new(ACON::Input::Argument.new("argument_name", :required)), 29 | "input_definition_3" => ACON::Input::Definition.new(ACON::Input::Option.new("option_name", "o", :none)), 30 | "input_definition_4" => ACON::Input::Definition.new( 31 | ACON::Input::Argument.new("argument_name", :required), 32 | ACON::Input::Option.new("option_name", "o", :none), 33 | ), 34 | } 35 | end 36 | 37 | def self.commands : Hash(String, ACON::Command) 38 | { 39 | "command_1" => DescriptorCommand1.new, 40 | "command_2" => DescriptorCommand2.new, 41 | } 42 | end 43 | 44 | def self.applications : Hash(String, ACON::Application) 45 | { 46 | "application_1" => DescriptorApplication1.new("foo"), 47 | "application_2" => DescriptorApplication2.new, 48 | } 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/descriptor/text_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | require "./abstract_descriptor_test_case" 3 | 4 | struct TextDescriptorTest < AbstractDescriptorTestCase 5 | # TODO: Include test data for double width chars 6 | # For both Application and Command contexts 7 | 8 | def test_describe_application_filtered_namespace : Nil 9 | self.assert_description( 10 | File.read("#{__DIR__}/../fixtures/text/application_filtered_namespace.txt"), 11 | DescriptorApplication2.new, 12 | ACON::Descriptor::Context.new(namespace: "command4"), 13 | ) 14 | end 15 | 16 | protected def descriptor : ACON::Descriptor::Interface 17 | ACON::Descriptor::Text.new 18 | end 19 | 20 | protected def format : String 21 | "txt" 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/fixtures/applications/descriptor1.cr: -------------------------------------------------------------------------------- 1 | class DescriptorApplication1 < ACON::Application 2 | end 3 | -------------------------------------------------------------------------------- /spec/fixtures/applications/descriptor2.cr: -------------------------------------------------------------------------------- 1 | class DescriptorApplication2 < ACON::Application 2 | def initialize 3 | super "My Athena application", "1.0.0" 4 | 5 | self.add DescriptorCommand1.new 6 | self.add DescriptorCommand2.new 7 | self.add DescriptorCommand3.new 8 | self.add DescriptorCommand4.new 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/fixtures/commands/annotation_configured.cr: -------------------------------------------------------------------------------- 1 | @[ACONA::AsCommand("annotation:configured", description: "Command configured via annotation", aliases: ["ac"])] 2 | class AnnotationConfiguredCommand < ACON::Command 3 | protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status 4 | ACON::Command::Status::SUCCESS 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/fixtures/commands/annotation_configured_aliases.cr: -------------------------------------------------------------------------------- 1 | @[ACONA::AsCommand("annotation:configured|ac")] 2 | class AnnotationConfiguredAliasesCommand < ACON::Command 3 | protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status 4 | ACON::Command::Status::SUCCESS 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/fixtures/commands/annotation_configured_hidden.cr: -------------------------------------------------------------------------------- 1 | @[ACONA::AsCommand("|annotation:configured")] 2 | class AnnotationConfiguredHiddenCommand < ACON::Command 3 | protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status 4 | ACON::Command::Status::SUCCESS 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/fixtures/commands/annotation_configured_hidden_field.cr: -------------------------------------------------------------------------------- 1 | @[ACONA::AsCommand("annotation:configured", hidden: true)] 2 | class AnnotationConfiguredHiddenFieldCommand < ACON::Command 3 | protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status 4 | ACON::Command::Status::SUCCESS 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/fixtures/commands/bar_buc.cr: -------------------------------------------------------------------------------- 1 | class BarBucCommand < ACON::Command 2 | protected def configure : Nil 3 | self 4 | .name("bar:buc") 5 | end 6 | 7 | protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status 8 | ACON::Command::Status::SUCCESS 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/fixtures/commands/descriptor1.cr: -------------------------------------------------------------------------------- 1 | class DescriptorCommand1 < ACON::Command 2 | protected def configure : Nil 3 | self 4 | .name("descriptor:command1") 5 | .aliases("alias1", "alias2") 6 | .description("command 1 description") 7 | .help("command 1 help") 8 | end 9 | 10 | protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status 11 | ACON::Command::Status::SUCCESS 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/fixtures/commands/descriptor2.cr: -------------------------------------------------------------------------------- 1 | class DescriptorCommand2 < ACON::Command 2 | protected def configure : Nil 3 | self 4 | .name("descriptor:command2") 5 | .description("command 2 description") 6 | .help("command 2 help") 7 | .usage("-o|--option_name ") 8 | .usage("") 9 | .argument("argument_name", :required) 10 | .option("option_name", "o", :none) 11 | end 12 | 13 | protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status 14 | ACON::Command::Status::SUCCESS 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/fixtures/commands/descriptor3.cr: -------------------------------------------------------------------------------- 1 | class DescriptorCommand3 < ACON::Command 2 | protected def configure : Nil 3 | self 4 | .name("descriptor:command3") 5 | .description("command 3 description") 6 | .help("command 3 help") 7 | .hidden 8 | end 9 | 10 | protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status 11 | ACON::Command::Status::SUCCESS 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/fixtures/commands/descriptor4.cr: -------------------------------------------------------------------------------- 1 | class DescriptorCommand4 < ACON::Command 2 | protected def configure : Nil 3 | self 4 | .name("descriptor:command4") 5 | .aliases("descriptor:alias_command4", "command4:descriptor") 6 | end 7 | 8 | protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status 9 | ACON::Command::Status::SUCCESS 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/fixtures/commands/foo.cr: -------------------------------------------------------------------------------- 1 | class FooCommand < IOCommand 2 | protected def configure : Nil 3 | self 4 | .name("foo:bar") 5 | .description("The foo:bar command") 6 | .aliases("afoobar") 7 | end 8 | 9 | protected def interact(input : ACON::Input::Interface, output : ACON::Output::Interface) : Nil 10 | output.puts "interact called" 11 | end 12 | 13 | protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status 14 | output.puts "execute called" 15 | 16 | super 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/fixtures/commands/foo1.cr: -------------------------------------------------------------------------------- 1 | class Foo1Command < IOCommand 2 | protected def configure : Nil 3 | self 4 | .name("foo:bar1") 5 | .description("The foo:bar1 command") 6 | .aliases("afoobar1") 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/fixtures/commands/foo2.cr: -------------------------------------------------------------------------------- 1 | class Foo2Command < IOCommand 2 | protected def configure : Nil 3 | self 4 | .name("foo1:bar") 5 | .description("The foo1:bar command") 6 | .aliases("afoobar2") 7 | end 8 | 9 | protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status 10 | ACON::Command::Status::SUCCESS 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/fixtures/commands/foo3.cr: -------------------------------------------------------------------------------- 1 | class Foo3Command < ACON::Command 2 | protected def configure : Nil 3 | self 4 | .name("foo3:bar") 5 | .description("The foo3:bar command") 6 | end 7 | 8 | protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status 9 | begin 10 | begin 11 | raise Exception.new "First exception

this is html

" 12 | rescue ex 13 | raise Exception.new "Second exception comment", ex 14 | end 15 | rescue ex 16 | raise Exception.new "Third exception comment", ex 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/fixtures/commands/foo4.cr: -------------------------------------------------------------------------------- 1 | class Foo4Command < ACON::Command 2 | protected def configure : Nil 3 | self 4 | .name("foo3:bar:toh") 5 | end 6 | 7 | protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status 8 | ACON::Command::Status::SUCCESS 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/fixtures/commands/foo6.cr: -------------------------------------------------------------------------------- 1 | class Foo6Command < ACON::Command 2 | protected def configure : Nil 3 | self 4 | .name("0foo:bar") 5 | .description("0foo:bar command") 6 | end 7 | 8 | protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status 9 | ACON::Command::Status::SUCCESS 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/fixtures/commands/foo_bar.cr: -------------------------------------------------------------------------------- 1 | class FooBarCommand < IOCommand 2 | protected def configure : Nil 3 | self 4 | .name("foobar:foo") 5 | .description("The foobar:foo command") 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/fixtures/commands/foo_hidden.cr: -------------------------------------------------------------------------------- 1 | class FooHiddenCommand < ACON::Command 2 | protected def configure : Nil 3 | self 4 | .name("foo:hidden") 5 | .aliases("afoohidden") 6 | .hidden(true) 7 | end 8 | 9 | protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status 10 | ACON::Command::Status::SUCCESS 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/fixtures/commands/foo_opt.cr: -------------------------------------------------------------------------------- 1 | class FooOptCommand < IOCommand 2 | protected def configure : Nil 3 | self 4 | .name("foo:bar") 5 | .description("The foo:bar command") 6 | .aliases("afoobar") 7 | .option("fooopt", "f", :optional, "fooopt description") 8 | end 9 | 10 | protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status 11 | super 12 | 13 | self.output.puts "execute called" 14 | self.output.puts input.option("fooopt") 15 | 16 | ACON::Command::Status::SUCCESS 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/fixtures/commands/foo_same_case_lowercase.cr: -------------------------------------------------------------------------------- 1 | class FooSameCaseLowercaseCommand < ACON::Command 2 | protected def configure : Nil 3 | self 4 | .name("foo:bar") 5 | .description("foo:bar command") 6 | end 7 | 8 | protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status 9 | ACON::Command::Status::SUCCESS 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/fixtures/commands/foo_same_case_uppercase.cr: -------------------------------------------------------------------------------- 1 | class FooSameCaseUppercaseCommand < ACON::Command 2 | protected def configure : Nil 3 | self 4 | .name("foo:BAR") 5 | .description("foo:BAR command") 6 | end 7 | 8 | protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status 9 | ACON::Command::Status::SUCCESS 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/fixtures/commands/foo_subnamespaced1.cr: -------------------------------------------------------------------------------- 1 | class FooSubnamespaced1Command < IOCommand 2 | protected def configure : Nil 3 | self 4 | .name("foo:bar:baz") 5 | .description("The foo:bar:baz command") 6 | .aliases("foobarbaz") 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/fixtures/commands/foo_subnamespaced2.cr: -------------------------------------------------------------------------------- 1 | class FooSubnamespaced2Command < IOCommand 2 | protected def configure : Nil 3 | self 4 | .name("foo:bar:go") 5 | .description("The foo:bar:go command") 6 | .aliases("foobargo") 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/fixtures/commands/foo_without_alias.cr: -------------------------------------------------------------------------------- 1 | class FooWithoutAliasCommand < IOCommand 2 | protected def configure : Nil 3 | self 4 | .name("foo") 5 | .description("The foo command") 6 | end 7 | 8 | protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status 9 | output.puts "execute called" 10 | 11 | ACON::Command::Status::SUCCESS 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/fixtures/commands/io.cr: -------------------------------------------------------------------------------- 1 | abstract class IOCommand < ACON::Command 2 | getter! input : ACON::Input::Interface 3 | getter! output : ACON::Output::Interface 4 | 5 | protected def execute(@input : ACON::Input::Interface, @output : ACON::Output::Interface) : ACON::Command::Status 6 | ACON::Command::Status::SUCCESS 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/fixtures/commands/test.cr: -------------------------------------------------------------------------------- 1 | class TestCommand < ACON::Command 2 | protected def configure : Nil 3 | self 4 | .name("namespace:name") 5 | .description("description") 6 | .aliases("name") 7 | .help("help") 8 | end 9 | 10 | protected def interact(input : ACON::Input::Interface, output : ACON::Output::Interface) : Nil 11 | output.puts "interact called" 12 | end 13 | 14 | protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status 15 | output.puts "execute called" 16 | 17 | ACON::Command::Status::SUCCESS 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/fixtures/commands/test_ambiguous_command_registrering1.cr: -------------------------------------------------------------------------------- 1 | class TestAmbiguousCommandRegistering < ACON::Command 2 | protected def configure : Nil 3 | self 4 | .name("test-ambiguous") 5 | .description("The test-ambiguous command") 6 | .aliases("test") 7 | end 8 | 9 | protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status 10 | output.puts "test-ambiguous" 11 | 12 | ACON::Command::Status::SUCCESS 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/fixtures/commands/test_ambiguous_command_registrering2.cr: -------------------------------------------------------------------------------- 1 | class TestAmbiguousCommandRegistering2 < ACON::Command 2 | protected def configure : Nil 3 | self 4 | .name("test-ambiguous2") 5 | .description("The test-ambiguous2 command") 6 | end 7 | 8 | protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status 9 | output.puts "test-ambiguous2" 10 | 11 | ACON::Command::Status::SUCCESS 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/fixtures/helper/table/borderless.txt: -------------------------------------------------------------------------------- 1 | =============== ========================== ================== 2 | ISBN Title Author 3 | =============== ========================== ================== 4 | 99921-58-10-7 Divine Comedy Dante Alighieri 5 | 9971-5-0210-0 A Tale of Two Cities Charles Dickens 6 | 960-425-059-0 The Lord of the Rings J. R. R. Tolkien 7 | 80-902734-1-6 And Then There Were None Agatha Christie 8 | =============== ========================== ================== 9 | -------------------------------------------------------------------------------- /spec/fixtures/helper/table/borderless_vertical.txt: -------------------------------------------------------------------------------- 1 | ============================== 2 | ISBN: 99921-58-10-7 3 | Title: Divine Comedy 4 | Author: Dante Alighieri 5 | Price: 9.95 6 | ============================== 7 | ISBN: 9971-5-0210-0 8 | Title: A Tale of Two Cities 9 | Author: Charles Dickens 10 | Price: 139.25 11 | ============================== 12 | -------------------------------------------------------------------------------- /spec/fixtures/helper/table/box.txt: -------------------------------------------------------------------------------- 1 | ┌───────────────┬──────────────────────────┬──────────────────┐ 2 | │ ISBN │ Title │ Author │ 3 | ├───────────────┼──────────────────────────┼──────────────────┤ 4 | │ 99921-58-10-7 │ Divine Comedy │ Dante Alighieri │ 5 | │ 9971-5-0210-0 │ A Tale of Two Cities │ Charles Dickens │ 6 | │ 960-425-059-0 │ The Lord of the Rings │ J. R. R. Tolkien │ 7 | │ 80-902734-1-6 │ And Then There Were None │ Agatha Christie │ 8 | └───────────────┴──────────────────────────┴──────────────────┘ 9 | -------------------------------------------------------------------------------- /spec/fixtures/helper/table/compact.txt: -------------------------------------------------------------------------------- 1 | ISBN Title Author 2 | 99921-58-10-7 Divine Comedy Dante Alighieri 3 | 9971-5-0210-0 A Tale of Two Cities Charles Dickens 4 | 960-425-059-0 The Lord of the Rings J. R. R. Tolkien 5 | 80-902734-1-6 And Then There Were None Agatha Christie 6 | -------------------------------------------------------------------------------- /spec/fixtures/helper/table/compact_vertical.txt: -------------------------------------------------------------------------------- 1 | ISBN: 99921-58-10-7 2 | Title: Divine Comedy 3 | Author: Dante Alighieri 4 | Price: 9.95 5 | 6 | ISBN: 9971-5-0210-0 7 | Title: A Tale of Two Cities 8 | Author: Charles Dickens 9 | Price: 139.25 10 | -------------------------------------------------------------------------------- /spec/fixtures/helper/table/default.txt: -------------------------------------------------------------------------------- 1 | +---------------+--------------------------+------------------+ 2 | | ISBN | Title | Author | 3 | +---------------+--------------------------+------------------+ 4 | | 99921-58-10-7 | Divine Comedy | Dante Alighieri | 5 | | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | 6 | | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | 7 | | 80-902734-1-6 | And Then There Were None | Agatha Christie | 8 | +---------------+--------------------------+------------------+ 9 | -------------------------------------------------------------------------------- /spec/fixtures/helper/table/default_cells_with_colspan.txt: -------------------------------------------------------------------------------- 1 | +-------------------------------+-------------------------------+-----------------------------+ 2 | | ISBN | Title | Author | 3 | +-------------------------------+-------------------------------+-----------------------------+ 4 | | 99921-58-10-7 | Divine Comedy | Dante Alighieri | 5 | +-------------------------------+-------------------------------+-----------------------------+ 6 | | Divine Comedy(Dante Alighieri) | 7 | +-------------------------------+-------------------------------+-----------------------------+ 8 | | Arduino: A Quick-Start Guide | Mark Schmidt | 9 | +-------------------------------+-------------------------------+-----------------------------+ 10 | | 9971-5-0210-0 | A Tale of | 11 | | | Two Cities | 12 | +-------------------------------+-------------------------------+-----------------------------+ 13 | | Cupiditate dicta atque porro, tempora exercitationem modi animi nulla nemo vel nihil! | 14 | +-------------------------------+-------------------------------+-----------------------------+ 15 | -------------------------------------------------------------------------------- /spec/fixtures/helper/table/default_cells_with_formatting_tags.txt: -------------------------------------------------------------------------------- 1 | +---------------+----------------------+-----------------+ 2 | | ISBN | Title | Author | 3 | +---------------+----------------------+-----------------+ 4 | | 99921-58-10-7 | Divine Comedy | Dante Alighieri | 5 | | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | 6 | +---------------+----------------------+-----------------+ 7 | -------------------------------------------------------------------------------- /spec/fixtures/helper/table/default_cells_with_non_formatting_tags.txt: -------------------------------------------------------------------------------- 1 | +----------------------------------+----------------------+-----------------+ 2 | | ISBN | Title | Author | 3 | +----------------------------------+----------------------+-----------------+ 4 | | 99921-58-10-700 | Divine Com | Dante Alighieri | 5 | | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | 6 | +----------------------------------+----------------------+-----------------+ 7 | -------------------------------------------------------------------------------- /spec/fixtures/helper/table/default_cells_with_rowspan.txt: -------------------------------------------------------------------------------- 1 | +---------------+---------------+-----------------+ 2 | | ISBN | Title | Author | 3 | +---------------+---------------+-----------------+ 4 | | 9971-5-0210-0 | Divine Comedy | Dante Alighieri | 5 | | | | | 6 | | | The Lord of | J. R. | 7 | | | the Rings | R. Tolkien | 8 | +---------------+---------------+-----------------+ 9 | | 80-902734-1-6 | And Then | Agatha Christie | 10 | | 80-902734-1-7 | There | Test | 11 | | | Were None | | 12 | +---------------+---------------+-----------------+ 13 | -------------------------------------------------------------------------------- /spec/fixtures/helper/table/default_cells_with_rowspan_and_colspan.txt: -------------------------------------------------------------------------------- 1 | +------------------+---------+-----------------+ 2 | | ISBN | Title | Author | 3 | +------------------+---------+-----------------+ 4 | | 9971-5-0210-0 | Dante Alighieri | 5 | | | Charles Dickens | 6 | +------------------+---------+-----------------+ 7 | | Dante Alighieri | 9971-5-0210-0 | 8 | | J. R. R. Tolkien | | 9 | | J. R. R | | 10 | +------------------+---------+-----------------+ 11 | -------------------------------------------------------------------------------- /spec/fixtures/helper/table/default_cells_with_rowspan_and_colspan_and_alignment.txt: -------------------------------------------------------------------------------- 1 | +---------------+---------------+-------------------------------------------+ 2 | | ISBN | Title | Author | 3 | +---------------+---------------+-------------------------------------------+ 4 | | 978 | De Monarchia | Dante Alighieri | 5 | | 99921-58-10-7 | Divine Comedy | spans multiple rows rows Dante Alighieri | 6 | | | | spans multiple rows rows | 7 | +---------------+---------------+-------------------------------------------+ 8 | | test | tttt | 9 | +---------------+---------------+-------------------------------------------+ 10 | -------------------------------------------------------------------------------- /spec/fixtures/helper/table/default_cells_with_rowspan_and_colspan_and_custom_format.txt: -------------------------------------------------------------------------------- 1 | +----------------+---------------+---------------------+ 2 | | ISBN | Title | Author | 3 | +----------------+---------------+---------------------+ 4 | | 978-0521567817 | De Monarchia | Dante Alighieri | 5 | | 978-0804169127 | Divine Comedy | spans multiple rows | 6 | | test | tttt | 7 | +----------------+---------------+---------------------+ 8 | -------------------------------------------------------------------------------- /spec/fixtures/helper/table/default_cells_with_rowspan_and_colspan_and_fgbg.txt: -------------------------------------------------------------------------------- 1 | +---------------+---------------+-------------------------------------------+ 2 | | 978 | De Monarchia | Dante Alighieri | 3 | | 99921-58-10-7 | Divine Comedy | spans multiple rows rows Dante Alighieri | 4 | | | | spans multiple rows rows | 5 | +---------------+---------------+-------------------------------------------+ 6 | | test | tttt | 7 | +---------------+---------------+-------------------------------------------+ 8 | -------------------------------------------------------------------------------- /spec/fixtures/helper/table/default_cells_with_rowspan_and_colspan_and_line_breaks.txt: -------------------------------------------------------------------------------- 1 | +-----------------+-------+-----------------+ 2 | | ISBN | Title | Author | 3 | +-----------------+-------+-----------------+ 4 | | 9971 | Dante Alighieri | 5 | | -5- | Charles Dickens | 6 | | 021 | | 7 | | 0-0 | | 8 | +-----------------+-------+-----------------+ 9 | | Dante Alighieri | 9971 | 10 | | Charles Dickens | -5- | 11 | | | 021 | 12 | | | 0-0 | 13 | +-----------------+-------+-----------------+ 14 | | 9971 | Dante | 15 | | -5- | Alighieri | 16 | | 021 | | 17 | | 0-0 | | 18 | +-----------------+-------+-----------------+ 19 | -------------------------------------------------------------------------------- /spec/fixtures/helper/table/default_cells_with_rowspan_and_colspan_no_separators.txt: -------------------------------------------------------------------------------- 1 | +-----------------+-------+-----------------+ 2 | | ISBN | Title | Author | 3 | +-----------------+-------+-----------------+ 4 | | 9971 | Dante Alighieri | 5 | | -5- | Charles Dickens | 6 | | 021 | | 7 | | 0-0 | | 8 | | Dante Alighieri | 9971 | 9 | | Charles Dickens | -5- | 10 | | | 021 | 11 | | | 0-0 | 12 | +-----------------+-------+-----------------+ 13 | -------------------------------------------------------------------------------- /spec/fixtures/helper/table/default_cells_with_rowspan_and_colspan_separator_in_rowspan.txt: -------------------------------------------------------------------------------- 1 | +---------------+-----------------+ 2 | | ISBN | Author | 3 | +---------------+-----------------+ 4 | | 9971-5-0210-0 | Dante Alighieri | 5 | | |-----------------| 6 | | | Charles Dickens | 7 | +---------------+-----------------+ 8 | -------------------------------------------------------------------------------- /spec/fixtures/helper/table/default_colspan_and_table_cell_with_comment_style.txt: -------------------------------------------------------------------------------- 1 | +-----------------+------------------+---------+ 2 | | Long Title | 3 | +-----------------+------------------+---------+ 4 | | 9971-5-0210-0 | 5 | +-----------------+------------------+---------+ 6 | | Dante Alighieri | J. R. R. Tolkien | J. R. R | 7 | +-----------------+------------------+---------+ 8 | -------------------------------------------------------------------------------- /spec/fixtures/helper/table/default_formatted_row_with_line_breaks.txt: -------------------------------------------------------------------------------- 1 | +-------+------------+ 2 | | Dont break | 3 | | here | 4 | +-------+------------+ 5 | | foo | Dont break | 6 | | bar | here | 7 | +-------+------------+ 8 | -------------------------------------------------------------------------------- /spec/fixtures/helper/table/default_headerless.txt: -------------------------------------------------------------------------------- 1 | +---------------+--------------------------+------------------+ 2 | | 99921-58-10-7 | Divine Comedy | Dante Alighieri | 3 | | 9971-5-0210-0 | | | 4 | | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | 5 | | 80-902734-1-6 | And Then There Were None | Agatha Christie | 6 | +---------------+--------------------------+------------------+ 7 | -------------------------------------------------------------------------------- /spec/fixtures/helper/table/default_line_break_after_colspan_cell.txt: -------------------------------------------------------------------------------- 1 | +-----+-----+-----+ 2 | | Foo | Bar | Baz | 3 | +-----+-----+-----+ 4 | | foo | baz | 5 | | bar | qux | 6 | +-----+-----+-----+ 7 | -------------------------------------------------------------------------------- /spec/fixtures/helper/table/default_line_breaks_after_colspan_cell.txt: -------------------------------------------------------------------------------- 1 | +-----+-----+------+ 2 | | Foo | Bar | Baz | 3 | +-----+-----+------+ 4 | | foo | baz | 5 | | bar | qux | 6 | | | quux | 7 | +-----+-----+------+ 8 | -------------------------------------------------------------------------------- /spec/fixtures/helper/table/default_missing_cell_values.txt: -------------------------------------------------------------------------------- 1 | +---------------+--------------------------+------------------+ 2 | | ISBN | Title | | 3 | +---------------+--------------------------+------------------+ 4 | | 99921-58-10-7 | Divine Comedy | Dante Alighieri | 5 | | 9971-5-0210-0 | | | 6 | | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | 7 | | 80-902734-1-6 | And Then There Were None | Agatha Christie | 8 | +---------------+--------------------------+------------------+ 9 | -------------------------------------------------------------------------------- /spec/fixtures/helper/table/default_multiline_cells.txt: -------------------------------------------------------------------------------- 1 | +---------------+----------------------------+-----------------+ 2 | | ISBN | Title | Author | 3 | +---------------+----------------------------+-----------------+ 4 | | 99921-58-10-7 | Divine | Dante Alighieri | 5 | | | Comedy | | 6 | | 9971-5-0210-2 | Harry Potter | Rowling | 7 | | | and the Chamber of Secrets | Joanne K. | 8 | | 9971-5-0210-2 | Harry Potter | Rowling | 9 | | | and the Chamber of Secrets | Joanne K. | 10 | | 960-425-059-0 | The Lord of the Rings | J. R. R. | 11 | | | | Tolkien | 12 | +---------------+----------------------------+-----------------+ 13 | -------------------------------------------------------------------------------- /spec/fixtures/helper/table/default_multiple_header_lines.txt: -------------------------------------------------------------------------------- 1 | +------+-------+--------+ 2 | | Main title | 3 | +------+-------+--------+ 4 | | ISBN | Title | Author | 5 | +------+-------+--------+ 6 | -------------------------------------------------------------------------------- /spec/fixtures/helper/table/default_no_rows.txt: -------------------------------------------------------------------------------- 1 | +------+-------+ 2 | | ISBN | Title | 3 | +------+-------+ 4 | -------------------------------------------------------------------------------- /spec/fixtures/helper/table/default_row_with_multiple_cells.txt: -------------------------------------------------------------------------------- 1 | +---+--+--+---+--+---+--+---+--+ 2 | | 1 | 2 | 3 | 4 | 3 | +---+--+--+---+--+---+--+---+--+ 4 | -------------------------------------------------------------------------------- /spec/fixtures/helper/table/double_box_separator.txt: -------------------------------------------------------------------------------- 1 | ╔═══════════════╤══════════════════════════╤══════════════════╗ 2 | ║ ISBN │ Title │ Author ║ 3 | ╠═══════════════╪══════════════════════════╪══════════════════╣ 4 | ║ 99921-58-10-7 │ Divine Comedy │ Dante Alighieri ║ 5 | ║ 9971-5-0210-0 │ A Tale of Two Cities │ Charles Dickens ║ 6 | ╟───────────────┼──────────────────────────┼──────────────────╢ 7 | ║ 960-425-059-0 │ The Lord of the Rings │ J. R. R. Tolkien ║ 8 | ║ 80-902734-1-6 │ And Then There Were None │ Agatha Christie ║ 9 | ╚═══════════════╧══════════════════════════╧══════════════════╝ 10 | -------------------------------------------------------------------------------- /spec/fixtures/helper/table/markdown.txt: -------------------------------------------------------------------------------- 1 | | ISBN | Title | Author | 2 | |---------------|--------------------------|------------------| 3 | | 99921-58-10-7 | Divine Comedy | Dante Alighieri | 4 | | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | 5 | | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | 6 | | 80-902734-1-6 | And Then There Were None | Agatha Christie | 7 | -------------------------------------------------------------------------------- /spec/fixtures/helper/table/suggested_vertical.txt: -------------------------------------------------------------------------------- 1 | ------------------------------ 2 | ISBN: 99921-58-10-7 3 | Title: Divine Comedy 4 | Author: Dante Alighieri 5 | Price: 9.95 6 | ------------------------------ 7 | ISBN: 9971-5-0210-0 8 | Title: A Tale of Two Cities 9 | Author: Charles Dickens 10 | Price: 139.25 11 | ------------------------------ 12 | -------------------------------------------------------------------------------- /spec/fixtures/style/backslashes.txt: -------------------------------------------------------------------------------- 1 | 2 | Title ending with \\ 3 | =================== 4 | 5 | Section ending with \\ 6 | --------------------- 7 | 8 | -------------------------------------------------------------------------------- /spec/fixtures/style/block.txt: -------------------------------------------------------------------------------- 1 | 2 | ! \[CAUTION\] Lorem ipsum dolor sit amet 3 | 4 | -------------------------------------------------------------------------------- /spec/fixtures/style/block_line_endings.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet 2 | \* Lorem ipsum dolor sit amet 3 | \* consectetur adipiscing elit 4 | 5 | Lorem ipsum dolor sit amet 6 | \* Lorem ipsum dolor sit amet 7 | \* consectetur adipiscing elit 8 | 9 | Lorem ipsum dolor sit amet 10 | Lorem ipsum dolor sit amet 11 | consectetur adipiscing elit 12 | 13 | Lorem ipsum dolor sit amet 14 | 15 | \/\/ Lorem ipsum dolor sit amet 16 | \/\/ 17 | \/\/ consectetur adipiscing elit 18 | 19 | -------------------------------------------------------------------------------- /spec/fixtures/style/block_no_prefix_type.txt: -------------------------------------------------------------------------------- 1 | 2 | \[TEST\] Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore 3 | magna aliqua\. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 4 | consequat\. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla 5 | pariatur\. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est 6 | laborum 7 | 8 | -------------------------------------------------------------------------------- /spec/fixtures/style/block_padding.txt: -------------------------------------------------------------------------------- 1 | 2 | \e\[30;42m \e\[0m 3 | \e\[30;42m \[OK\] Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore \e\[0m 4 | \e\[30;42m magna aliqua\. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo \e\[0m 5 | \e\[30;42m consequat\. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur\. \e\[0m 6 | \e\[30;42m Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum \e\[0m 7 | \e\[30;42m \e\[0m 8 | 9 | -------------------------------------------------------------------------------- /spec/fixtures/style/block_prefix_no_type.txt: -------------------------------------------------------------------------------- 1 | 2 | \$ Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna 3 | \$ aliqua\. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat\. 4 | \$ Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur\. Excepteur sint 5 | \$ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum 6 | 7 | -------------------------------------------------------------------------------- /spec/fixtures/style/blocks.txt: -------------------------------------------------------------------------------- 1 | 2 | \[WARNING\] Warning 3 | 4 | ! \[CAUTION\] Caution 5 | 6 | \[ERROR\] Error 7 | 8 | \[OK\] Success 9 | 10 | ! \[NOTE\] Note 11 | 12 | \[INFO\] Info 13 | 14 | X \[CUSTOM\] Custom block 15 | 16 | -------------------------------------------------------------------------------- /spec/fixtures/style/closing_tag.txt: -------------------------------------------------------------------------------- 1 | \e\[30;46mdo you want \e\[0m\e\[33msomething\e\[0m\e\[30;46m\?\e\[0m 2 | -------------------------------------------------------------------------------- /spec/fixtures/style/definition_list.txt: -------------------------------------------------------------------------------- 1 | ---------- --------- 2 | foo bar 3 | ---------- --------- 4 | this is a title 5 | ---------- --------- 6 | foo2 bar2 7 | ---------- --------- 8 | 9 | -------------------------------------------------------------------------------- /spec/fixtures/style/emojis.txt: -------------------------------------------------------------------------------- 1 | 2 | \[OK\] Lorem ipsum dolor sit amet 3 | 4 | \[OK\] Lorem ipsum dolor sit amet with one emoji 🎉 5 | 6 | \[OK\] Lorem ipsum dolor sit amet with so many of them 👩‍🌾👩‍🌾👩‍🌾👩‍🌾👩‍🌾 7 | 8 | -------------------------------------------------------------------------------- /spec/fixtures/style/empty_buffer.txt: -------------------------------------------------------------------------------- 1 | Hello 2 | -------------------------------------------------------------------------------- /spec/fixtures/style/horizontal_table.txt: -------------------------------------------------------------------------------- 1 | --- --- --- --- 2 | a 1 4 7 3 | b 2 5 8 4 | c 3 9 5 | d 6 | --- --- --- --- 7 | 8 | -------------------------------------------------------------------------------- /spec/fixtures/style/long_line_block.txt: -------------------------------------------------------------------------------- 1 | 2 | X \[CUSTOM\] Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et 3 | X dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea 4 | X commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat 5 | X nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit 6 | X anim id est laborum 7 | 8 | -------------------------------------------------------------------------------- /spec/fixtures/style/long_line_block_wrapping.txt: -------------------------------------------------------------------------------- 1 | 2 | § \[CUSTOM\] Lopadotemachoselachogaleokranioleipsanodrimhypotrimmatosilphioparaomelitokatakechymenokichlepikossyphophatto 3 | § peristeralektryonoptekephalliokigklopeleiolagoiosiraiobaphetraganopterygon 4 | 5 | -------------------------------------------------------------------------------- /spec/fixtures/style/long_line_comment.txt: -------------------------------------------------------------------------------- 1 | 2 | // Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna 3 | // aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 4 | // Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur 5 | // sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum 6 | 7 | -------------------------------------------------------------------------------- /spec/fixtures/style/long_line_comment_decorated.txt: -------------------------------------------------------------------------------- 1 | 2 | \/\/ Árvíztűrőtükörfúrógép 🎼 Lorem ipsum dolor sit \e\[33m💕 amet, consectetur adipisicing elit, sed do eiusmod tempor incididu \e\[0m 3 | \/\/ \e\[33mlabore et dolore magna aliqua\. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex\e\[0m 4 | \/\/ \e\[33mea commodo consequat\. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla \e\[0m 5 | \/\/ \e\[33mpariatur\.\e\[0m Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est 6 | \/\/ laborum 7 | 8 | -------------------------------------------------------------------------------- /spec/fixtures/style/multi_line_block.txt: -------------------------------------------------------------------------------- 1 | 2 | X \[CUSTOM\] Custom block 3 | X 4 | X Second custom block line 5 | 6 | -------------------------------------------------------------------------------- /spec/fixtures/style/nested_tag_prefix.txt: -------------------------------------------------------------------------------- 1 | 2 | ║ \[★\] Árvíztűrőtükörfúrógép Lorem ipsum dolor sit \e\[33mamet, consectetur adipisicing elit, sed do eiusmod tempor incididunt \e\[0m 3 | ║ \e\[33m ut labore et dolore magna aliqua\. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut \e\[0m 4 | ║ \e\[33m aliquip ex ea commodo consequat\. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu \e\[0m 5 | ║ \e\[33m fugiat nulla pariatur\.\e\[0m Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit 6 | ║ anim id est laborum 7 | 8 | -------------------------------------------------------------------------------- /spec/fixtures/style/non_interactive_question.txt: -------------------------------------------------------------------------------- 1 | 2 | Title 3 | ===== 4 | 5 | Duis aute irure dolor in reprehenderit in voluptate velit esse 6 | -------------------------------------------------------------------------------- /spec/fixtures/style/table.txt: -------------------------------------------------------------------------------- 1 | ----- ------- 2 | \e\[32m Foo \e\[0m \e\[32m Bar \e\[0m 3 | ----- ------- 4 | Biz Baz 5 | 12 false 6 | ----- ------- 7 | 8 | -------------------------------------------------------------------------------- /spec/fixtures/style/table_horizontal.txt: -------------------------------------------------------------------------------- 1 | ----- ----- ------- 2 | \e\[32m Foo \e\[0m Biz 12 3 | \e\[32m Bar \e\[0m Baz false 4 | ----- ----- ------- 5 | 6 | -------------------------------------------------------------------------------- /spec/fixtures/style/table_vertical.txt: -------------------------------------------------------------------------------- 1 | ------------ 2 | \[33mFoo\[0m: Biz 3 | \[33mBar\[0m: Baz 4 | ------------ 5 | \[33mFoo\[0m: 12 6 | \[33mBar\[0m: false 7 | ------------ 8 | 9 | -------------------------------------------------------------------------------- /spec/fixtures/style/text_block_blank_line.txt: -------------------------------------------------------------------------------- 1 | \* Lorem ipsum dolor sit amet 2 | \* consectetur adipiscing elit 3 | 4 | \[OK\] Lorem ipsum dolor sit amet 5 | 6 | -------------------------------------------------------------------------------- /spec/fixtures/style/title_block.txt: -------------------------------------------------------------------------------- 1 | 2 | Title 3 | ===== 4 | 5 | \[WARNING\] Lorem ipsum dolor sit amet 6 | 7 | Title 8 | ===== 9 | 10 | -------------------------------------------------------------------------------- /spec/fixtures/style/titles.txt: -------------------------------------------------------------------------------- 1 | 2 | First title 3 | =========== 4 | 5 | Second title 6 | ============ 7 | 8 | -------------------------------------------------------------------------------- /spec/fixtures/style/titles_text.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet 2 | 3 | First title 4 | =========== 5 | 6 | Lorem ipsum dolor sit amet 7 | 8 | Second title 9 | ============ 10 | 11 | Lorem ipsum dolor sit amet 12 | 13 | Third title 14 | =========== 15 | 16 | Lorem ipsum dolor sit amet 17 | 18 | Fourth title 19 | ============ 20 | 21 | Lorem ipsum dolor sit amet 22 | 23 | 24 | Fifth title 25 | =========== 26 | 27 | Lorem ipsum dolor sit amet 28 | 29 | 30 | Sixth title 31 | =========== 32 | 33 | -------------------------------------------------------------------------------- /spec/fixtures/text/application_1.txt: -------------------------------------------------------------------------------- 1 | foo UNKNOWN 2 | 3 | Usage: 4 | command [options] [arguments] 5 | 6 | Options: 7 | -h, --help Display help for the given command. When no command is given display help for the list command 8 | --silent Do not output any message 9 | -q, --quiet Only errors are displayed. All other output is suppressed 10 | -V, --version Display this application version 11 | --ansi|--no-ansi Force (or disable --no-ansi) ANSI output 12 | -n, --no-interaction Do not ask any interactive question 13 | -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug 14 | 15 | Available commands: 16 | completion Dump the shell completion script 17 | help Display help for a command 18 | list List available commands 19 | -------------------------------------------------------------------------------- /spec/fixtures/text/application_2.txt: -------------------------------------------------------------------------------- 1 | My Athena application 1.0.0 2 | 3 | Usage: 4 | command [options] [arguments] 5 | 6 | Options: 7 | -h, --help Display help for the given command. When no command is given display help for the list command 8 | --silent Do not output any message 9 | -q, --quiet Only errors are displayed. All other output is suppressed 10 | -V, --version Display this application version 11 | --ansi|--no-ansi Force (or disable --no-ansi) ANSI output 12 | -n, --no-interaction Do not ask any interactive question 13 | -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug 14 | 15 | Available commands: 16 | completion Dump the shell completion script 17 | help Display help for a command 18 | list List available commands 19 | descriptor 20 | descriptor:command1 [alias1|alias2] command 1 description 21 | descriptor:command2 command 2 description 22 | descriptor:command4 [descriptor:alias_command4|command4:descriptor] 23 | -------------------------------------------------------------------------------- /spec/fixtures/text/application_alternative_namespace.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | There are no commands defined in the 'foos' namespace\. 4 | 5 | Did you mean this\? 6 | foo 7 | 8 | -------------------------------------------------------------------------------- /spec/fixtures/text/application_filtered_namespace.txt: -------------------------------------------------------------------------------- 1 | My Athena application 1.0.0 2 | 3 | Usage: 4 | command [options] [arguments] 5 | 6 | Options: 7 | -h, --help Display help for the given command. When no command is given display help for the list command 8 | --silent Do not output any message 9 | -q, --quiet Only errors are displayed. All other output is suppressed 10 | -V, --version Display this application version 11 | --ansi|--no-ansi Force (or disable --no-ansi) ANSI output 12 | -n, --no-interaction Do not ask any interactive question 13 | -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug 14 | 15 | Available commands for the 'command4' namespace: 16 | command4:descriptor 17 | -------------------------------------------------------------------------------- /spec/fixtures/text/application_renderexception1.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | Command 'foo' is not defined. 4 | 5 | 6 | -------------------------------------------------------------------------------- /spec/fixtures/text/application_renderexception2.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | The '--foo' option does not exist\. 4 | 5 | 6 | list \[--raw\] \[--format FORMAT\] \[--short\] \[--\] \[\] 7 | 8 | -------------------------------------------------------------------------------- /spec/fixtures/text/application_renderexception3.txt: -------------------------------------------------------------------------------- 1 | 2 | In foo3.cr line \d+: 3 | 4 | Third exception comment 5 | 6 | 7 | In foo3.cr line \d+: 8 | 9 | Second exception comment 10 | 11 | 12 | In foo3.cr line \d+: 13 | 14 | First exception

this is html

15 | 16 | 17 | foo3:bar 18 | 19 | -------------------------------------------------------------------------------- /spec/fixtures/text/application_renderexception3_decorated.txt: -------------------------------------------------------------------------------- 1 | 2 | \e\[33mIn foo3\.cr line \d+:\e\[0m 3 | \e\[97;41m \e\[0m 4 | \e\[97;41m Third exception comment \e\[0m 5 | \e\[97;41m \e\[0m 6 | 7 | \e\[33mIn foo3\.cr line \d+:\e\[0m 8 | \e\[97;41m \e\[0m 9 | \e\[97;41m Second exception comment \e\[0m 10 | \e\[97;41m \e\[0m 11 | 12 | \e\[33mIn foo3\.cr line \d+:\e\[0m 13 | \e\[97;41m \e\[0m 14 | \e\[97;41m First exception

this is html

\e\[0m 15 | \e\[97;41m \e\[0m 16 | 17 | \e\[32mfoo3:bar\e\[0m 18 | 19 | -------------------------------------------------------------------------------- /spec/fixtures/text/application_renderexception4.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | Command 'foo' is not define 4 | d. 5 | 6 | 7 | -------------------------------------------------------------------------------- /spec/fixtures/text/application_renderexception_doublewidth1.txt: -------------------------------------------------------------------------------- 1 | 2 | At spec/application_spec.cr:\d+:\d+ in '->' 3 | 4 | エラーメッセージ 5 | 6 | 7 | foo 8 | 9 | -------------------------------------------------------------------------------- /spec/fixtures/text/application_renderexception_escapeslines.txt: -------------------------------------------------------------------------------- 1 | 2 | In \w+\.cr line \d+: 3 | 4 | dont break here < 5 | info>!<\/info> 6 | 7 | 8 | foo 9 | 10 | -------------------------------------------------------------------------------- /spec/fixtures/text/application_renderexception_linebreaks.txt: -------------------------------------------------------------------------------- 1 | 2 | In \w+\.cr line \d+: 3 | 4 | line 1 with extra spaces 5 | line 2 6 | 7 | line 4 8 | 9 | 10 | foo 11 | 12 | -------------------------------------------------------------------------------- /spec/fixtures/text/application_renderexception_synopsis_escapeslines.txt: -------------------------------------------------------------------------------- 1 | 2 | In \w+\.cr line \d+: 3 | 4 | some exception 5 | 6 | 7 | foo \[\] 8 | 9 | -------------------------------------------------------------------------------- /spec/fixtures/text/application_run1.txt: -------------------------------------------------------------------------------- 1 | foo UNKNOWN 2 | 3 | Usage: 4 | command \[options\] \[arguments\] 5 | 6 | Options: 7 | -h, --help Display help for the given command\. When no command is given display help for the list command 8 | --silent Do not output any message 9 | -q, --quiet Only errors are displayed. All other output is suppressed 10 | -V, --version Display this application version 11 | --ansi\|--no-ansi Force \(or disable --no-ansi\) ANSI output 12 | -n, --no-interaction Do not ask any interactive question 13 | -v\|vv\|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug 14 | 15 | Available commands: 16 | completion Dump the shell completion script 17 | help Display help for a command 18 | list List available commands 19 | -------------------------------------------------------------------------------- /spec/fixtures/text/application_run2.txt: -------------------------------------------------------------------------------- 1 | Description: 2 | List available commands 3 | 4 | Usage: 5 | list \[options\] \[--\] \[\] 6 | 7 | Arguments: 8 | namespace Only list commands in this namespace 9 | 10 | Options: 11 | --raw To output raw command list 12 | --format=FORMAT The output format \(txt\) \[default: "txt"\] 13 | --short To skip describing command's arguments 14 | -h, --help Display help for the given command. When no command is given display help for the list command 15 | --silent Do not output any message 16 | -q, --quiet Only errors are displayed. All other output is suppressed 17 | -V, --version Display this application version 18 | --ansi\|--no-ansi Force \(or disable --no-ansi\) ANSI output 19 | -n, --no-interaction Do not ask any interactive question 20 | -v\|vv\|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug 21 | 22 | Help: 23 | The list command lists all commands: 24 | 25 | console list 26 | 27 | You can also display the commands for a specific namespace: 28 | 29 | console list test 30 | 31 | It's also possible to get raw list of commands \(useful for embedding command runner\): 32 | 33 | console list --raw 34 | -------------------------------------------------------------------------------- /spec/fixtures/text/application_run3.txt: -------------------------------------------------------------------------------- 1 | Description: 2 | List available commands 3 | 4 | Usage: 5 | list \[options\] \[--\] \[\] 6 | 7 | Arguments: 8 | namespace Only list commands in this namespace 9 | 10 | Options: 11 | --raw To output raw command list 12 | --format=FORMAT The output format \(txt\) \[default: "txt"\] 13 | --short To skip describing command's arguments 14 | -h, --help Display help for the given command\. When no command is given display help for the list command 15 | --silent Do not output any message 16 | -q, --quiet Only errors are displayed. All other output is suppressed 17 | -V, --version Display this application version 18 | --ansi\|--no-ansi Force \(or disable --no-ansi\) ANSI output 19 | -n, --no-interaction Do not ask any interactive question 20 | -v\|vv\|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug 21 | 22 | Help: 23 | The list command lists all commands: 24 | 25 | console list 26 | 27 | You can also display the commands for a specific namespace: 28 | 29 | console list test 30 | 31 | It's also possible to get raw list of commands \(useful for embedding command runner\): 32 | 33 | console list --raw 34 | -------------------------------------------------------------------------------- /spec/fixtures/text/application_run4.txt: -------------------------------------------------------------------------------- 1 | foo UNKNOWN 2 | -------------------------------------------------------------------------------- /spec/fixtures/text/application_run5.txt: -------------------------------------------------------------------------------- 1 | Description: 2 | Display help for a command 3 | 4 | Usage: 5 | help \[options\] \[--\] \[\] 6 | 7 | Arguments: 8 | command_name The command name \[default: "help"\] 9 | 10 | Options: 11 | --format=FORMAT The output format \(txt\) \[default: "txt"\] 12 | --raw To output raw command help 13 | -h, --help Display help for the given command\. When no command is given display help for the list command 14 | --silent Do not output any message 15 | -q, --quiet Only errors are displayed. All other output is suppressed 16 | -V, --version Display this application version 17 | --ansi\|--no-ansi Force \(or disable --no-ansi\) ANSI output 18 | -n, --no-interaction Do not ask any interactive question 19 | -v\|vv\|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug 20 | 21 | Help: 22 | The help command displays help for a given command: 23 | 24 | console help list 25 | 26 | To display the list of available commands, please use the list command\. 27 | -------------------------------------------------------------------------------- /spec/fixtures/text/command_1.txt: -------------------------------------------------------------------------------- 1 | Description: 2 | command 1 description 3 | 4 | Usage: 5 | descriptor:command1 6 | alias1 7 | alias2 8 | 9 | Help: 10 | command 1 help 11 | -------------------------------------------------------------------------------- /spec/fixtures/text/command_2.txt: -------------------------------------------------------------------------------- 1 | Description: 2 | command 2 description 3 | 4 | Usage: 5 | descriptor:command2 [options] [--] \ 6 | descriptor:command2 -o|--option_name \ 7 | descriptor:command2 \ 8 | 9 | Arguments: 10 | argument_name 11 | 12 | Options: 13 | -o, --option_name 14 | 15 | Help: 16 | command 2 help 17 | -------------------------------------------------------------------------------- /spec/fixtures/text/input_argument_1.txt: -------------------------------------------------------------------------------- 1 | argument_name 2 | -------------------------------------------------------------------------------- /spec/fixtures/text/input_argument_2.txt: -------------------------------------------------------------------------------- 1 | argument_name argument description 2 | -------------------------------------------------------------------------------- /spec/fixtures/text/input_argument_3.txt: -------------------------------------------------------------------------------- 1 | argument_name argument description [default: "default_value"] 2 | -------------------------------------------------------------------------------- /spec/fixtures/text/input_argument_4.txt: -------------------------------------------------------------------------------- 1 | argument_name multiline 2 | argument description 3 | -------------------------------------------------------------------------------- /spec/fixtures/text/input_argument_with_style.txt: -------------------------------------------------------------------------------- 1 | argument_name argument description [default: "\style\"] 2 | -------------------------------------------------------------------------------- /spec/fixtures/text/input_definition_1.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/athena-framework/console/75f479a9be979df23f5174cd172e17a636308131/spec/fixtures/text/input_definition_1.txt -------------------------------------------------------------------------------- /spec/fixtures/text/input_definition_2.txt: -------------------------------------------------------------------------------- 1 | Arguments: 2 | argument_name 3 | -------------------------------------------------------------------------------- /spec/fixtures/text/input_definition_3.txt: -------------------------------------------------------------------------------- 1 | Options: 2 | -o, --option_name 3 | -------------------------------------------------------------------------------- /spec/fixtures/text/input_definition_4.txt: -------------------------------------------------------------------------------- 1 | Arguments: 2 | argument_name 3 | 4 | Options: 5 | -o, --option_name 6 | -------------------------------------------------------------------------------- /spec/fixtures/text/input_option_1.txt: -------------------------------------------------------------------------------- 1 | -o, --option_name 2 | -------------------------------------------------------------------------------- /spec/fixtures/text/input_option_2.txt: -------------------------------------------------------------------------------- 1 | -o, --option_name[=OPTION_NAME] option description [default: "default_value"] 2 | -------------------------------------------------------------------------------- /spec/fixtures/text/input_option_3.txt: -------------------------------------------------------------------------------- 1 | -o, --option_name=OPTION_NAME option description 2 | -------------------------------------------------------------------------------- /spec/fixtures/text/input_option_4.txt: -------------------------------------------------------------------------------- 1 | -o, --option_name[=OPTION_NAME] option description (multiple values allowed) 2 | -------------------------------------------------------------------------------- /spec/fixtures/text/input_option_5.txt: -------------------------------------------------------------------------------- 1 | -o, --option_name=OPTION_NAME multiline 2 | option description 3 | -------------------------------------------------------------------------------- /spec/fixtures/text/input_option_6.txt: -------------------------------------------------------------------------------- 1 | -o|O, --option_name=OPTION_NAME option with multiple shortcuts 2 | -------------------------------------------------------------------------------- /spec/fixtures/text/input_option_with_style.txt: -------------------------------------------------------------------------------- 1 | -o, --option_name=OPTION_NAME option description [default: "\style\"] 2 | -------------------------------------------------------------------------------- /spec/fixtures/text/input_option_with_style_array.txt: -------------------------------------------------------------------------------- 1 | -o, --option_name=OPTION_NAME option description [default: ["\Hello\","\world\"]] (multiple values allowed) 2 | -------------------------------------------------------------------------------- /spec/formatter/null_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | struct NullFormatterTest < ASPEC::TestCase 4 | def test_has_style : Nil 5 | ACON::Formatter::Null.new.has_style?("error").should be_false 6 | end 7 | 8 | def test_style : Nil 9 | ACON::Formatter::Null.new.style("error").should be_a ACON::Formatter::NullStyle 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/formatter/null_style_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | struct NullStyleTest < ASPEC::TestCase 4 | def test_apply : Nil 5 | ACON::Formatter::NullStyle.new.apply("foo").should eq "foo" 6 | end 7 | 8 | def test_set_foreground : Nil 9 | style = ACON::Formatter::NullStyle.new 10 | style.foreground = :red 11 | style.apply("foo").should eq "foo" 12 | end 13 | 14 | def test_set_background : Nil 15 | style = ACON::Formatter::NullStyle.new 16 | style.background = :red 17 | style.apply("foo").should eq "foo" 18 | end 19 | 20 | def test_options : Nil 21 | style = ACON::Formatter::NullStyle.new 22 | 23 | style.add_option :bold 24 | style.apply("foo").should eq "foo" 25 | 26 | style.remove_option :bold 27 | style.apply("foo").should eq "foo" 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/formatter/output_formatter_style_stack_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | describe ACON::Formatter::OutputStyleStack do 4 | it "#<<" do 5 | stack = ACON::Formatter::OutputStyleStack.new 6 | stack << ACON::Formatter::OutputStyle.new :white, :black 7 | stack << (s2 = ACON::Formatter::OutputStyle.new :yellow, :blue) 8 | 9 | stack.current.should eq s2 10 | 11 | stack << (s3 = ACON::Formatter::OutputStyle.new :green, :red) 12 | 13 | stack.current.should eq s3 14 | end 15 | 16 | describe "#pop" do 17 | it "returns the oldest style" do 18 | stack = ACON::Formatter::OutputStyleStack.new 19 | stack << (s1 = ACON::Formatter::OutputStyle.new :white, :black) 20 | stack << (s2 = ACON::Formatter::OutputStyle.new :yellow, :blue) 21 | 22 | stack.pop.should eq s2 23 | stack.pop.should eq s1 24 | end 25 | 26 | it "returns the default style if empty" do 27 | stack = ACON::Formatter::OutputStyleStack.new 28 | style = ACON::Formatter::OutputStyle.new 29 | 30 | stack.pop.should eq style 31 | end 32 | 33 | it "allows popping a specific style" do 34 | stack = ACON::Formatter::OutputStyleStack.new 35 | stack << (s1 = ACON::Formatter::OutputStyle.new :white, :black) 36 | stack << (s2 = ACON::Formatter::OutputStyle.new :yellow, :blue) 37 | stack << ACON::Formatter::OutputStyle.new :green, :red 38 | 39 | stack.pop(s2).should eq s2 40 | stack.pop.should eq s1 41 | end 42 | 43 | it "invalid pop" do 44 | stack = ACON::Formatter::OutputStyleStack.new 45 | stack << ACON::Formatter::OutputStyle.new :white, :black 46 | 47 | expect_raises ACON::Exception::InvalidArgument, "Provided style is not present in the stack." do 48 | stack.pop ACON::Formatter::OutputStyle.new :yellow, :blue 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/helper/abstract_question_helper_test_case.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | abstract struct AbstractQuestionHelperTest < ASPEC::TestCase 4 | def initialize 5 | @helper_set = ACON::Helper::HelperSet.new ACON::Helper::Formatter.new 6 | 7 | @output = ACON::Output::IO.new IO::Memory.new, decorated: false 8 | end 9 | 10 | protected def with_input(data : String, interactive : Bool = true, & : ACON::Input::Interface -> Nil) : Nil 11 | input_stream = IO::Memory.new data 12 | input = ACON::Input::Hash.new 13 | input.stream = input_stream 14 | input.interactive = interactive 15 | 16 | yield input 17 | end 18 | 19 | protected def assert_output_contains(string : String, normalize : Bool = false) : Nil 20 | stream = @output.io 21 | stream.rewind 22 | 23 | output = stream.to_s 24 | 25 | if normalize 26 | output = output.gsub EOL, "\n" 27 | end 28 | 29 | output.should contain string 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/helper/formatter_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | private def normalize(input : String) : String 4 | input.gsub EOL, "\n" 5 | end 6 | 7 | describe ACON::Helper::Formatter do 8 | it "#format_section" do 9 | ACON::Helper::Formatter.new.format_section("cli", "some text to display").should eq "[cli] some text to display" 10 | end 11 | 12 | describe "#format_block" do 13 | it "formats" do 14 | formatter = ACON::Helper::Formatter.new 15 | 16 | formatter.format_block("Some text to display", "error").should eq " Some text to display " 17 | formatter.format_block({"Some text to display", "foo bar"}, "error").should eq " Some text to display \n foo bar " 18 | formatter.format_block("Some text to display", "error", true).should eq normalize <<-BLOCK 19 | 20 | Some text to display 21 | 22 | BLOCK 23 | end 24 | 25 | it "formats with diacritic letters" do 26 | formatter = ACON::Helper::Formatter.new 27 | 28 | formatter.format_block("Du texte à afficher", "error", true).should eq normalize <<-BLOCK 29 | 30 | Du texte à afficher 31 | 32 | BLOCK 33 | end 34 | 35 | pending "formats with double with characters" do 36 | end 37 | 38 | it "escapes < within the block" do 39 | ACON::Helper::Formatter.new.format_block("some info", "error", true).should eq normalize <<-BLOCK 40 | 41 | \\some info\\ 42 | 43 | BLOCK 44 | end 45 | end 46 | 47 | describe "#truncate" do 48 | it "with shorter length than message with suffix" do 49 | formatter = ACON::Helper::Formatter.new 50 | message = "testing wrapping" 51 | 52 | formatter.truncate(message, 4).should eq "test..." 53 | formatter.truncate(message, 15).should eq "testing wrappin..." 54 | formatter.truncate(message, 16).should eq "testing wrapping..." 55 | formatter.truncate("zażółć gęślą jaźń", 12).should eq "zażółć gęślą..." 56 | end 57 | 58 | it "with custom suffix" do 59 | ACON::Helper::Formatter.new.truncate("testing truncate", 4, "!").should eq "test!" 60 | end 61 | 62 | it "with longer length than message with suffix" do 63 | ACON::Helper::Formatter.new.truncate("test", 10).should eq "test" 64 | end 65 | 66 | it "with negative length" do 67 | formatter = ACON::Helper::Formatter.new 68 | message = "testing truncate" 69 | 70 | formatter.truncate(message, -5).should eq "testing tru..." 71 | formatter.truncate(message, -100).should eq "..." 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /spec/helper/helper_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | struct HelperTest < ASPEC::TestCase 4 | @[TestWith( 5 | {0, "< 1 sec"}, 6 | {1, "1 sec"}, 7 | {2, "2 secs"}, 8 | {59, "59 secs"}, 9 | {60, "1 min"}, 10 | {61, "1 min"}, 11 | {119, "1 min"}, 12 | {120, "2 mins"}, 13 | {121, "2 mins"}, 14 | {4.minutes, "4 mins"}, 15 | {3599, "59 mins"}, 16 | {3600, "1 hr"}, 17 | {7199, "1 hr"}, 18 | {7200, "2 hrs"}, 19 | {7201, "2 hrs"}, 20 | {86399, "23 hrs"}, 21 | {86400, "1 day"}, 22 | {86401, "1 day"}, 23 | {172_799, "1 day"}, 24 | {172_800, "2 days"}, 25 | {172_801, "2 days"}, 26 | )] 27 | def test_format_time(seconds : Int32 | Time::Span, expected : String) : Nil 28 | ACON::Helper.format_time(seconds).should eq expected 29 | end 30 | 31 | @[TestWith( 32 | {"abc", "abc"}, 33 | {"abc", "abc"}, 34 | {"a\033[1;36mbc", "abc"}, 35 | {"a\033]8;;http://url\033\\b\033]8;;\033\\c", "abc"}, 36 | )] 37 | def test_remove_docoration(decorated_text : String, expected : String) : Nil 38 | ACON::Helper.remove_decoration(ACON::Formatter::Output.new, decorated_text).should eq expected 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/helper/output_wrapper_spec.cr: -------------------------------------------------------------------------------- 1 | struct OutputWrapperTest < ASPEC::TestCase 2 | def test_wrap_no_cut : Nil 3 | ACON::Helper::OutputWrapper.new.wrap( 4 | "Árvíztűrőtükörfúrógép https://github.com/crystal/crystal Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent vestibulum nulla quis urna maximus porttitor. Donec ullamcorper risus at libero ornare efficitur.", 5 | 20, 6 | ).should eq <<-TEXT 7 | Árvíztűrőtükörfúrógé 8 | p https://github.com/crystal/crystal Lorem ipsum 9 | dolor sit amet, 10 | consectetur 11 | adipiscing elit. 12 | Praesent vestibulum 13 | nulla quis urna 14 | maximus porttitor. 15 | Donec ullamcorper 16 | risus at libero 17 | ornare efficitur. 18 | TEXT 19 | end 20 | 21 | def test_wrap_with_cut : Nil 22 | ACON::Helper::OutputWrapper.new(true).wrap( 23 | "Árvíztűrőtükörfúrógép https://github.com/crystal/crystal Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent vestibulum nulla quis urna maximus porttitor. Donec ullamcorper risus at libero ornare efficitur.", 24 | 20, 25 | ).should eq <<-TEXT 26 | Árvíztűrőtükörfúrógé 27 | p 28 | https://github.com/c 29 | rystal/crystal Lorem 30 | ipsum dolor sit 31 | amet, consectetur 32 | adipiscing elit. 33 | Praesent vestibulum 34 | nulla quis urna 35 | maximus porttitor. 36 | Donec ullamcorper 37 | risus at libero 38 | ornare efficitur. 39 | TEXT 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/helper/table_style_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | struct TableStyleSpec < ASPEC::TestCase 4 | def test_getter_setters : Nil 5 | style = ACON::Helper::Table::Style 6 | .new 7 | .align(:right) 8 | .border_format("BF") 9 | .padding_char('c') 10 | .header_title_format("HTF") 11 | .footer_title_format("FTF") 12 | .cell_header_format("CHF") 13 | .cell_row_format("CRF") 14 | .cell_row_content_format("CRCF") 15 | .horizontal_border_chars('o', 'i') 16 | .vertical_border_chars('v', 'u') 17 | .default_crossing_char('x') 18 | 19 | style.align.should eq ACON::Helper::Table::Alignment::RIGHT 20 | style.border_format.should eq "BF" 21 | style.padding_char.should eq 'c' 22 | style.header_title_format.should eq "HTF" 23 | style.footer_title_format.should eq "FTF" 24 | style.cell_header_format.should eq "CHF" 25 | style.cell_row_format.should eq "CRF" 26 | style.cell_row_content_format.should eq "CRCF" 27 | style.border_chars.should eq({"o", "v", "i", "u"}) 28 | style.crossing_chars.should eq({"x", "x", "x", "x", "x", "x", "x", "x", "x", "x", "x", "x"}) 29 | 30 | style.crossing_chars("c", "tl", "tm", "tr", "mr", "br", "bm", "bl", "ml", "tlb", "tmb", "trb") 31 | style.crossing_chars.should eq({"c", "tl", "tm", "tr", "mr", "br", "bm", "bl", "ml", "tlb", "tmb", "trb"}) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/input/argument_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | describe ACON::Input::Argument do 4 | describe ".new" do 5 | it "disallows blank names" do 6 | expect_raises ACON::Exception::InvalidArgument, "An argument name cannot be blank." do 7 | ACON::Input::Argument.new "" 8 | end 9 | 10 | expect_raises ACON::Exception::InvalidArgument, "An argument name cannot be blank." do 11 | ACON::Input::Argument.new " " 12 | end 13 | end 14 | end 15 | 16 | describe "#default=" do 17 | describe "when the argument is required" do 18 | it "raises if not nil" do 19 | argument = ACON::Input::Argument.new "foo", :required 20 | 21 | expect_raises ACON::Exception::Logic, "Cannot set a default value when the argument is required." do 22 | argument.default = "bar" 23 | end 24 | end 25 | 26 | it "allows nil" do 27 | ACON::Input::Argument.new("foo", :required).default = nil 28 | end 29 | end 30 | 31 | describe "array" do 32 | it "nil value" do 33 | argument = ACON::Input::Argument.new "foo", ACON::Input::Argument::Mode[:optional, :is_array] 34 | argument.default = nil 35 | argument.default.should eq [] of String 36 | end 37 | 38 | it "non array" do 39 | argument = ACON::Input::Argument.new "foo", ACON::Input::Argument::Mode[:optional, :is_array] 40 | 41 | expect_raises ACON::Exception::Logic, "Default value for an array argument must be an array." do 42 | argument.default = "bar" 43 | end 44 | end 45 | end 46 | end 47 | 48 | describe "#complete" do 49 | it "with an array" do 50 | values = ["foo", "bar"] 51 | suggestions = ACON::Completion::Suggestions.new 52 | 53 | argument = ACON::Input::Argument.new "foo", suggested_values: values 54 | 55 | argument.has_completion?.should be_true 56 | 57 | argument.complete ACON::Completion::Input.new, suggestions 58 | 59 | suggestions.suggested_values.map(&.value).should eq ["foo", "bar"] 60 | end 61 | 62 | it "with an block" do 63 | values = ["foo", "bar"] 64 | suggestions = ACON::Completion::Suggestions.new 65 | callback = Proc(ACON::Completion::Input, Array(String)).new { values } 66 | 67 | argument = ACON::Input::Argument.new "foo", suggested_values: callback 68 | 69 | argument.has_completion?.should be_true 70 | 71 | argument.complete ACON::Completion::Input.new, suggestions 72 | 73 | suggestions.suggested_values.map(&.value).should eq ["foo", "bar"] 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /spec/input/value/array_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | 3 | describe ACON::Input::Value::Array do 4 | describe ".new" do 5 | it "without args" do 6 | array = ACON::Input::Value::Array.new 7 | array.value.should be_empty 8 | array.should be_empty 9 | end 10 | 11 | it "with args" do 12 | array = ACON::Input::Value::Array.from_array [1, "foo", false] 13 | array.value.size.should eq 3 14 | array << 10 15 | array.value.size.should eq 4 16 | end 17 | end 18 | 19 | it "#to_s" do 20 | ACON::Input::Value::Array 21 | .from_array([1, "foo", false]) 22 | .to_s 23 | .should eq %(1,foo,false) 24 | end 25 | 26 | describe "#get" do 27 | it "non-nilable" do 28 | ACON::Input::Value::Array 29 | .from_array(arr = [1, 2, 3]) 30 | .get(Array(Int32)) 31 | .should eq arr 32 | end 33 | 34 | it "nilable" do 35 | ACON::Input::Value::Array 36 | .from_array(arr = ["foo", "bar"]) 37 | .get(Array(String)?) 38 | .should eq arr 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/input/value/nil_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | 3 | describe ACON::Input::Value::Number do 4 | describe "#get" do 5 | it Bool do 6 | expect_raises ACON::Exception::Logic, "'123' is not a valid 'Bool'." do 7 | ACON::Input::Value::Number.new(123).get Bool 8 | end 9 | end 10 | 11 | it String do 12 | val = ACON::Input::Value::Number.new(123).get String 13 | typeof(val).should eq String 14 | val.should eq "123" 15 | end 16 | 17 | it Int do 18 | val = ACON::Input::Value::Number.new(123).get Int32 19 | typeof(val).should eq Int32 20 | val.should eq 123 21 | 22 | val = ACON::Input::Value::Number.new(123_u8).get UInt8 23 | typeof(val).should eq UInt8 24 | val.should eq 123_u8 25 | end 26 | 27 | it Float do 28 | val = ACON::Input::Value::Number.new(4.69).get Float32 29 | typeof(val).should eq Float32 30 | val.should eq 4.69_f32 31 | 32 | val = ACON::Input::Value::Number.new(4.69).get Float64 33 | typeof(val).should eq Float64 34 | val.should eq 4.69 35 | end 36 | 37 | describe Array do 38 | it String do 39 | expect_raises ACON::Exception::Logic, "'123' is not a valid 'Array(String)'." do 40 | ACON::Input::Value::Number.new(123).get Array(String) 41 | end 42 | end 43 | 44 | it Int32 do 45 | expect_raises ACON::Exception::Logic, "'123' is not a valid '(Array(Int32) | Nil)'." do 46 | ACON::Input::Value::Number.new(123).get Array(Int32)? 47 | end 48 | end 49 | 50 | it Bool do 51 | expect_raises ACON::Exception::Logic, "'123' is not a valid 'Array(Bool)'." do 52 | ACON::Input::Value::Number.new(123).get Array(Bool) 53 | end 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/output/io_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | struct IOTest < ASPEC::TestCase 4 | @io : IO::Memory 5 | 6 | def initialize 7 | @io = IO::Memory.new 8 | end 9 | 10 | def tear_down : Nil 11 | @io.clear 12 | end 13 | 14 | def test_do_write : Nil 15 | output = ACON::Output::IO.new @io 16 | output.puts "foo" 17 | output.print "bar" 18 | output.to_s.should eq "foo#{EOL}bar" 19 | end 20 | 21 | def test_do_write_var_args : Nil 22 | output = ACON::Output::IO.new @io 23 | output.puts "foo", "bar" 24 | output.print "biz", "baz" 25 | output.to_s.should eq "foo#{EOL}bar#{EOL}bizbaz" 26 | end 27 | 28 | def test_decorated_dumb_term : Nil 29 | with_isolated_env do 30 | ENV["TERM"] = "dumb" 31 | ACON::Output::IO.new(@io).decorated?.should be_false 32 | end 33 | end 34 | 35 | def test_decorated_no_color : Nil 36 | with_isolated_env do 37 | ENV["NO_COLOR"] = "true" 38 | ENV["COLORTERM"] = "truecolor" 39 | ACON::Output::IO.new(@io).decorated?.should be_false 40 | end 41 | end 42 | 43 | def test_decorated_no_color_empty : Nil 44 | with_isolated_env do 45 | ENV["NO_COLOR"] = "" 46 | ENV["COLORTERM"] = "truecolor" 47 | ACON::Output::IO.new(@io).decorated?.should be_true 48 | end 49 | end 50 | 51 | def test_decorated_force_color : Nil 52 | with_isolated_env do 53 | ENV["FORCE_COLOR"] = "true" 54 | ACON::Output::IO.new(@io).decorated?.should be_true 55 | end 56 | end 57 | 58 | def test_decorated_force_color_empty : Nil 59 | with_isolated_env do 60 | ENV["FORCE_COLOR"] = "" 61 | ACON::Output::IO.new(@io).decorated?.should be_false 62 | end 63 | end 64 | 65 | def test_decorated_supported_term : Nil 66 | with_isolated_env do 67 | ENV["TERM"] = "xterm-256color" 68 | ACON::Output::IO.new(@io).decorated?.should be_true 69 | end 70 | end 71 | 72 | def test_decorated_colorterm : Nil 73 | with_isolated_env do 74 | ENV["COLORTERM"] = "truecolor" 75 | ACON::Output::IO.new(@io).decorated?.should be_true 76 | end 77 | end 78 | 79 | def test_decorated_ansicon : Nil 80 | with_isolated_env do 81 | ENV["ANSICON"] = "1" 82 | ACON::Output::IO.new(@io).decorated?.should be_true 83 | end 84 | end 85 | 86 | def test_decorated_conemuansi : Nil 87 | with_isolated_env do 88 | ENV["ConEmuANSI"] = "ON" 89 | ACON::Output::IO.new(@io).decorated?.should be_true 90 | end 91 | end 92 | 93 | def test_decorated_term_program_hyper : Nil 94 | with_isolated_env do 95 | ENV["TERM_PROGRAM"] = "Hyper" 96 | ACON::Output::IO.new(@io).decorated?.should be_true 97 | end 98 | end 99 | 100 | def test_decorated_term_program_non_hyper : Nil 101 | with_isolated_env do 102 | ENV["TERM_PROGRAM"] = "WezTerm" 103 | ACON::Output::IO.new(@io).decorated?.should be_false 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /spec/output/null_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | struct NullSpec < ASPEC::TestCase 4 | def test_verbosity : Nil 5 | ACON::Output::Null.new.verbosity.silent?.should be_true 6 | end 7 | 8 | def test_formatter : Nil 9 | output = ACON::Output::Null.new 10 | output.formatter.should be_a ACON::Formatter::Null 11 | output.formatter = ACON::Formatter::Output.new 12 | output.formatter.should be_a ACON::Formatter::Null 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/output/output_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | private class MockOutput < ACON::Output 4 | getter output : String = "" 5 | 6 | def clear : Nil 7 | @output = "" 8 | end 9 | 10 | protected def do_write(message : String, new_line : Bool) : Nil 11 | @output += message 12 | @output += "\n" if new_line 13 | end 14 | end 15 | 16 | struct OutputTest < ASPEC::TestCase 17 | def test_write_verbosity_quiet : Nil 18 | output = MockOutput.new :quiet 19 | output.puts "foo" 20 | output.output.should be_empty 21 | end 22 | 23 | def test_write_array_messages : Nil 24 | output = MockOutput.new 25 | output.puts ["foo", "bar"] 26 | output.output.should eq "foo\nbar\n" 27 | end 28 | 29 | @[DataProvider("message_provider")] 30 | def test_write_raw_message(message : String, output_type : ACON::Output::Type, expected : String) : Nil 31 | output = MockOutput.new 32 | output.puts message, output_type: output_type 33 | output.output.should eq expected 34 | end 35 | 36 | def message_provider : Tuple 37 | { 38 | {"foo", ACON::Output::Type::RAW, "foo\n"}, 39 | {"foo", ACON::Output::Type::PLAIN, "foo\n"}, 40 | } 41 | end 42 | 43 | def test_write_non_decorated : Nil 44 | output = MockOutput.new 45 | output.decorated = false 46 | output.puts "foo" 47 | output.output.should eq "foo\n" 48 | end 49 | 50 | def test_write_decorated : Nil 51 | foo_style = ACON::Formatter::OutputStyle.new :yellow, :red, :blink 52 | output = MockOutput.new 53 | output.formatter.has_style?("FOO").should be_false 54 | output.formatter.set_style "FOO", foo_style 55 | output.formatter.has_style?("FOO").should be_true 56 | output.decorated = true 57 | output.puts "foo" 58 | output.output.should eq "\e[33;41;5mfoo\e[0m\n" 59 | end 60 | 61 | def test_write_decorated_invalid_style : Nil 62 | output = MockOutput.new 63 | output.puts "foo" 64 | output.output.should eq "foo\n" 65 | end 66 | 67 | @[DataProvider("verbosity_provider")] 68 | def test_write_with_verbosity(verbosity : ACON::Output::Verbosity, expected : String) : Nil 69 | output = MockOutput.new 70 | 71 | output.verbosity = verbosity 72 | output.print "1" 73 | output.print "2", :quiet 74 | output.print "3", :normal 75 | output.print "4", :verbose 76 | output.print "5", :very_verbose 77 | output.print "6", :debug 78 | 79 | output.output.should eq expected 80 | end 81 | 82 | def verbosity_provider : Tuple 83 | { 84 | {ACON::Output::Verbosity::SILENT, ""}, 85 | {ACON::Output::Verbosity::QUIET, "2"}, 86 | {ACON::Output::Verbosity::NORMAL, "123"}, 87 | {ACON::Output::Verbosity::VERBOSE, "1234"}, 88 | {ACON::Output::Verbosity::VERY_VERBOSE, "12345"}, 89 | {ACON::Output::Verbosity::DEBUG, "123456"}, 90 | } 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /spec/question/choice_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | struct ChoiceQuestionTest < ASPEC::TestCase 4 | def test_new_empty_choices : Nil 5 | expect_raises ACON::Exception::Logic, "Choice questions must have at least 1 choice available." do 6 | ACON::Question::Choice.new "A question", Array(String).new 7 | end 8 | end 9 | 10 | def test_custom_validator : Nil 11 | question = ACON::Question::Choice.new( 12 | "A question", 13 | [ 14 | "First response", 15 | "Second response", 16 | "Third response", 17 | "Fourth response", 18 | ] 19 | ) 20 | 21 | question.validator do 22 | "FOO" 23 | end 24 | 25 | {"First response", "First response ", " First response", " First response "}.each do |answer| 26 | if validator = question.validator 27 | actual = validator.call answer 28 | 29 | actual.should eq "FOO" 30 | end 31 | end 32 | end 33 | 34 | def test_validator_exact_match : Nil 35 | question = ACON::Question::Choice.new( 36 | "A question", 37 | [ 38 | "First response", 39 | "Second response", 40 | "Third response", 41 | "Fourth response", 42 | ] 43 | ) 44 | 45 | {"First response", "First response ", " First response", " First response "}.each do |answer| 46 | if validator = question.validator 47 | validator.call(answer).should eq "First response" 48 | end 49 | end 50 | end 51 | 52 | def test_validator_index_match : Nil 53 | question = ACON::Question::Choice.new( 54 | "A question", 55 | [ 56 | "First response", 57 | "Second response", 58 | "Third response", 59 | "Fourth response", 60 | ] 61 | ) 62 | 63 | {"0"}.each do |answer| 64 | if validator = question.validator 65 | validator.call(answer).should eq "First response" 66 | end 67 | end 68 | end 69 | 70 | def test_non_trimmable : Nil 71 | question = ACON::Question::Choice.new( 72 | "A question", 73 | [ 74 | "First response ", 75 | " Second response", 76 | " Third response ", 77 | ] 78 | ) 79 | 80 | question.trimmable = false 81 | 82 | if validator = question.validator 83 | validator.not_nil!.call(" Third response ").should eq " Third response " 84 | end 85 | end 86 | 87 | @[DataProvider("hash_choice_provider")] 88 | def test_validator_hash_choices(answer : String, expected : String) : Nil 89 | question = ACON::Question::Choice.new( 90 | "A question", 91 | { 92 | "0" => "First choice", 93 | "foo" => "Foo", 94 | "99" => "N°99", 95 | } 96 | ) 97 | 98 | if validator = question.validator 99 | validator.call(answer).should eq expected 100 | end 101 | end 102 | 103 | def hash_choice_provider : Hash 104 | { 105 | "'0' choice by key" => {"0", "First choice"}, 106 | "'0' choice by value" => {"First choice", "First choice"}, 107 | "select by key" => {"foo", "Foo"}, 108 | "select by value" => {"Foo", "Foo"}, 109 | "select by key, numeric key" => {"99", "N°99"}, 110 | "select by value, numeric key" => {"N°99", "N°99"}, 111 | } 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /spec/question/confirmation_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | struct ConfirmationQuestionTest < ASPEC::TestCase 4 | @[DataProvider("normalizer_provider")] 5 | def test_default_regex(default : Bool, answers : Array, expected : Bool) : Nil 6 | question = ACON::Question::Confirmation.new "A question", default 7 | 8 | answers.each do |answer| 9 | normalizer = question.normalizer.not_nil! 10 | actual = normalizer.call answer 11 | actual.should eq expected 12 | end 13 | end 14 | 15 | def normalizer_provider : Tuple 16 | { 17 | { 18 | true, 19 | ["y", "Y", "yes", "YES", "yEs", ""], 20 | true, 21 | }, 22 | { 23 | true, 24 | ["n", "N", "no", "NO", "nO", "foo", "1", "0"], 25 | false, 26 | }, 27 | { 28 | false, 29 | ["y", "Y", "yes", "YES", "yEs"], 30 | true, 31 | }, 32 | { 33 | false, 34 | ["n", "N", "no", "NO", "nO", "foo", "1", "0", ""], 35 | false, 36 | }, 37 | } 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/question/multiple_choice_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | struct MultipleChoiceQuestionTest < ASPEC::TestCase 4 | def test_new_empty_choices : Nil 5 | expect_raises ACON::Exception::Logic, "Choice questions must have at least 1 choice available." do 6 | ACON::Question::MultipleChoice.new "A question", Array(String).new 7 | end 8 | end 9 | 10 | def test_non_trimmable : Nil 11 | question = ACON::Question::MultipleChoice(String).new( 12 | "A question", 13 | [ 14 | "First response ", 15 | " Second response", 16 | " Third response ", 17 | ] 18 | ) 19 | 20 | question.trimmable = false 21 | 22 | question.validator.not_nil!.call("First response , Second response").should eq ["First response ", " Second response"] 23 | end 24 | 25 | @[DataProvider("hash_choice_provider")] 26 | def test_validator_hash_choices(answer : String, expected : Array) : Nil 27 | question = ACON::Question::MultipleChoice.new( 28 | "A question", 29 | { 30 | "0" => "First choice", 31 | "foo" => "Foo", 32 | "99" => "N°99", 33 | } 34 | ) 35 | 36 | question.validator.not_nil!.call(answer).should eq expected 37 | end 38 | 39 | def hash_choice_provider : Hash 40 | { 41 | "'0' choice by key - multiple" => {"0,Foo", ["First choice", "Foo"]}, 42 | "'0' choice by key- single" => {"foo", ["Foo"]}, 43 | "select by value, numeric key" => {"N°99,foo,First choice", ["N°99", "Foo", "First choice"]}, 44 | } 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/question/question_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | private class QuestionCommand < ACON::Command 4 | protected def configure : Nil 5 | self 6 | .name("question:command") 7 | end 8 | 9 | protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status 10 | name = self.helper(ACON::Helper::Question).ask input, output, ACON::Question(String?).new "What is your name?", nil 11 | 12 | output.puts "Your name is: #{name}" 13 | 14 | ACON::Command::Status::SUCCESS 15 | end 16 | end 17 | 18 | struct QuestionTest < ASPEC::TestCase 19 | @question : ACON::Question(String?) 20 | 21 | def initialize 22 | @question = ACON::Question(String?).new "Test Question", nil 23 | end 24 | 25 | def test_default : Nil 26 | @question.default.should be_nil 27 | default = ACON::Question(String).new("Test Question", "FOO").default 28 | default.should eq "FOO" 29 | typeof(default).should eq String 30 | end 31 | 32 | def test_hidden_autocompleter_callback : Nil 33 | @question.autocompleter_callback do 34 | [] of String 35 | end 36 | 37 | expect_raises ACON::Exception::Logic, "A hidden question cannot use the autocompleter" do 38 | @question.hidden = true 39 | end 40 | end 41 | 42 | @[DataProvider("autocompleter_values_provider")] 43 | def test_get_set_autocompleter_values(values : Indexable | Hash, expected : Array(String)) : Nil 44 | @question.autocompleter_values = values 45 | 46 | @question.autocompleter_values.should eq expected 47 | end 48 | 49 | def autocompleter_values_provider : Hash 50 | { 51 | "tuple" => { 52 | {"a", "b", "c"}, 53 | ["a", "b", "c"], 54 | }, 55 | "array" => { 56 | ["a", "b", "c"], 57 | ["a", "b", "c"], 58 | }, 59 | "string key hash" => { 60 | {"a" => "b", "c" => "d"}, 61 | ["a", "c", "b", "d"], 62 | }, 63 | "int key hash" => { 64 | {0 => "b", 1 => "d"}, 65 | ["b", "d"], 66 | }, 67 | } 68 | end 69 | 70 | def test_custom_normalizer : Nil 71 | question = ACON::Question(String).new "A question", "" 72 | 73 | question.normalizer do |val| 74 | val.upcase 75 | end 76 | 77 | if normalizer = question.normalizer 78 | normalizer.call("foo").should eq "FOO" 79 | end 80 | end 81 | 82 | def test_with_inputs : Nil 83 | command = QuestionCommand.new 84 | command.helper_set = ACON::Helper::HelperSet.new ACON::Helper::Question.new 85 | tester = ACON::Spec::CommandTester.new command 86 | tester.inputs "Jim" 87 | tester.execute 88 | tester.display.should eq "What is your name?Your name is: Jim#{EOL}" 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /spec/spec_helper.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | 3 | require "../src/athena-console" 4 | require "../src/spec" 5 | 6 | require "athena-spec" 7 | require "athena-clock/spec" 8 | 9 | require "./fixtures/commands/io" 10 | require "./fixtures/**" 11 | 12 | # Spec by default disables colorize with `TERM=dumb`. 13 | # Override that given there are specs based on ansi output. 14 | Colorize.enabled = true 15 | 16 | struct MockCommandLoader 17 | include Athena::Console::Loader::Interface 18 | 19 | def initialize( 20 | *, 21 | @command_or_exception : ACON::Command | ::Exception? = nil, 22 | @has : Bool = true, 23 | @names : Array(String) | ::Exception = [] of String, 24 | ) 25 | end 26 | 27 | def get(name : String) : ACON::Command 28 | case v = @command_or_exception 29 | in ::Exception then raise v 30 | in ACON::Command then v 31 | in Nil then raise "BUG: no command or exception was set" 32 | end 33 | end 34 | 35 | def has?(name : String) : Bool 36 | @has 37 | end 38 | 39 | def names : Array(String) 40 | case v = @names 41 | in ::Exception then raise v 42 | in Array(String) then v 43 | end 44 | end 45 | end 46 | 47 | def with_isolated_env(&) : Nil 48 | old_values = ENV.dup 49 | begin 50 | ENV.clear 51 | 52 | yield 53 | ensure 54 | ENV.clear 55 | old_values.each do |key, old_value| 56 | ENV[key] = old_value 57 | end 58 | end 59 | end 60 | 61 | ASPEC.run_all 62 | -------------------------------------------------------------------------------- /spec/terminal_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | struct TerminalTest < ASPEC::TestCase 4 | @col_size : Int32? 5 | @line_size : Int32? 6 | 7 | def initialize 8 | @col_size = ENV["COLUMNS"]?.try &.to_i? 9 | @line_size = ENV["LINES"]?.try &.to_i? 10 | end 11 | 12 | def tear_down : Nil 13 | ENV.delete "COLUMNS" 14 | ENV.delete "LINES" 15 | end 16 | 17 | def test_height_width : Nil 18 | ENV["COLUMNS"] = "100" 19 | ENV["LINES"] = "50" 20 | 21 | terminal = ACON::Terminal.new 22 | terminal.width.should eq 100 23 | terminal.height.should eq 50 24 | 25 | ENV["COLUMNS"] = "120" 26 | ENV["LINES"] = "60" 27 | 28 | terminal = ACON::Terminal.new 29 | terminal.width.should eq 120 30 | terminal.height.should eq 60 31 | terminal.size.should eq({120, 60}) 32 | end 33 | 34 | def test_zero_values : Nil 35 | ENV["COLUMNS"] = "0" 36 | ENV["LINES"] = "0" 37 | 38 | terminal = ACON::Terminal.new 39 | terminal.width.should eq 0 40 | terminal.height.should eq 0 41 | terminal.size.should eq({0, 0}) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /src/annotations.cr: -------------------------------------------------------------------------------- 1 | module Athena::Console::Annotations 2 | # Annotation containing metadata related to an `ACON::Command`. 3 | # This is the preferred way of configuring a command. 4 | # 5 | # ``` 6 | # @[ACONA::AsCommand("add", description: "Sums two numbers, optionally making making the sum negative")] 7 | # class AddCommand < ACON::Command 8 | # # ... 9 | # end 10 | # ``` 11 | # 12 | # ## Configuration 13 | # 14 | # Various fields can be used within this annotation to control various aspects of the command. 15 | # All fields are optional unless otherwise noted. 16 | # 17 | # ### name 18 | # 19 | # **Type:** `String` - **required** 20 | # 21 | # The name of the command. 22 | # May be provided as either an explicit named argument, or the first positional argument. 23 | # See `ACON::Command#name`. 24 | # 25 | # ### description 26 | # 27 | # **Type:** `String` 28 | # 29 | # A short sentence describing the function of the command. 30 | # See `ACON::Command#description`. 31 | # 32 | # ### hidden 33 | # 34 | # **Type:** `Bool` 35 | # 36 | # If this command should be hidden from the command list. 37 | # See `ACON::Command#hidden?`. 38 | # 39 | # ### aliases 40 | # 41 | # **Type:** `Enumerable(String)` 42 | # 43 | # Alternate names this command may be invoked by. 44 | # See `ACON::Command#aliases`. 45 | annotation AsCommand; end 46 | end 47 | -------------------------------------------------------------------------------- /src/athena-console.cr: -------------------------------------------------------------------------------- 1 | require "ecr" 2 | require "semantic_version" 3 | 4 | require "athena-clock" 5 | 6 | require "./annotations" 7 | require "./application" 8 | require "./command" 9 | require "./cursor" 10 | require "./terminal" 11 | 12 | require "./commands/*" 13 | require "./completion/**" 14 | require "./descriptor/*" 15 | require "./exception/*" 16 | require "./formatter/*" 17 | require "./helper/*" 18 | require "./input/*" 19 | require "./loader/*" 20 | require "./output/*" 21 | require "./question/*" 22 | require "./style/*" 23 | 24 | # Convenience alias to make referencing `Athena::Console` types easier. 25 | alias ACON = Athena::Console 26 | 27 | # Convenience alias to make referencing `ACON::Annotations` types easier. 28 | alias ACONA = ACON::Annotations 29 | 30 | # Allows the creation of CLI based commands 31 | module Athena::Console 32 | VERSION = "0.4.1" 33 | 34 | # Contains all the `Athena::Console` based annotations. 35 | module Annotations; end 36 | 37 | # Includes the commands that come bundled with `Athena::Console`. 38 | module Commands; end 39 | 40 | # Includes types related to Athena's [tab completion][Athena::Console::Input::Interface--argumentoption-value-completion] features. 41 | module Completion; end 42 | 43 | # Both acts as a namespace for exceptions related to the `Athena::Console` component, as well as a way to check for exceptions from the component. 44 | # Exposes a `#code` method that represents the exit code of a command invocation. 45 | module Exception 46 | # Returns the exit code that should be used for this exception. 47 | getter code : Int32 48 | end 49 | 50 | # Contains types related to lazily loading commands. 51 | module Loader; end 52 | end 53 | -------------------------------------------------------------------------------- /src/commands/generic.cr: -------------------------------------------------------------------------------- 1 | # A generic implementation of `ACON::Command` that is instantiated with a block that will be executed as part of the `#execute` method. 2 | # 3 | # This is the command class used as part of `ACON::Application#register`. 4 | class Athena::Console::Commands::Generic < Athena::Console::Command 5 | alias Proc = ::Proc(ACON::Input::Interface, ACON::Output::Interface, ACON::Command, ACON::Command::Status) 6 | 7 | def initialize(name : String, &@callback : ACON::Commands::Generic::Proc) 8 | super name 9 | end 10 | 11 | protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status 12 | @callback.call input, output, self 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /src/commands/help.cr: -------------------------------------------------------------------------------- 1 | # Displays information for a given command. 2 | @[Athena::Console::Annotations::AsCommand("help", description: "Display help for a command")] 3 | class Athena::Console::Commands::Help < Athena::Console::Command 4 | # :nodoc: 5 | setter command : ACON::Command? = nil 6 | 7 | protected def configure : Nil 8 | self.ignore_validation_errors 9 | 10 | self 11 | .name("help") 12 | .argument("command_name", description: "The command name", default: "help") { ACON::Descriptor::Application.new(self.application).commands.keys } 13 | .option("format", value_mode: :required, description: "The output format (txt)", default: "txt") { ACON::Helper::Descriptor.new.formats } 14 | .option("raw", value_mode: :none, description: "To output raw command help") 15 | .help( 16 | <<-HELP 17 | The %command.name% command displays help for a given command: 18 | 19 | %command.full_name% list 20 | 21 | To display the list of available commands, please use the list command. 22 | HELP 23 | ) 24 | end 25 | 26 | protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status 27 | if @command.nil? 28 | @command = self.application.find input.argument("command_name", String) 29 | end 30 | 31 | ACON::Helper::Descriptor.new.describe( 32 | output, 33 | @command.not_nil!, 34 | ACON::Descriptor::Context.new( 35 | format: input.option("format", String), 36 | raw_text: input.option("raw", Bool), 37 | ) 38 | ) 39 | 40 | @command = nil 41 | 42 | ACON::Command::Status::SUCCESS 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /src/commands/lazy.cr: -------------------------------------------------------------------------------- 1 | # :nodoc: 2 | class Athena::Console::Commands::Lazy < Athena::Console::Command 3 | @command : Proc(ACON::Command) | ACON::Command 4 | @enabled : Bool 5 | 6 | delegate :run, 7 | :merge_application_definition, 8 | :definition, 9 | :native_definition, 10 | :argument, 11 | :option, 12 | :process_title, 13 | :help, 14 | :processed_help, 15 | :synopsis, 16 | :usage, 17 | :usages, 18 | :helper, 19 | to: self.command 20 | 21 | def initialize( 22 | name : String, 23 | aliases : Enumerable(String), 24 | description : String, 25 | hidden : Bool, 26 | @command : Proc(ACON::Command), 27 | @enabled : Bool = true, 28 | ) 29 | self 30 | .name(name) 31 | .aliases(aliases) 32 | .hidden(hidden) 33 | .description(description) 34 | end 35 | 36 | # :inherit: 37 | def application=(application : ACON::Application?) : Nil 38 | if (cmd = @command).is_a? ACON::Command 39 | cmd.application = application 40 | end 41 | 42 | super 43 | end 44 | 45 | # :inherit: 46 | def helper_set=(helper_set : ACON::Helper::HelperSet) : Nil 47 | if (cmd = @command).is_a? ACON::Command 48 | cmd.helper_set = helper_set 49 | end 50 | 51 | super 52 | end 53 | 54 | # :inherit: 55 | def enabled? : Bool 56 | @enabled || self.command.enabled? 57 | end 58 | 59 | protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status 60 | raise NotImplementedError.new "Use #run instead." 61 | end 62 | 63 | def command : ACON::Command 64 | if (cmd = @command).is_a? ACON::Command 65 | return cmd 66 | end 67 | 68 | command = @command = cmd.call 69 | command.application = self.application? 70 | 71 | if hs = self.helper_set 72 | command.helper_set = hs 73 | end 74 | 75 | command 76 | .name(self.name) 77 | .aliases(self.aliases) 78 | .hidden(self.hidden?) 79 | .description(self.description) 80 | 81 | command.definition 82 | 83 | command 84 | end 85 | 86 | def complete(input : ACON::Completion::Input, suggestions : ACON::Completion::Suggestions) : Nil 87 | self.command.complete input, suggestions 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /src/commands/list.cr: -------------------------------------------------------------------------------- 1 | # Lists the available commands, optionally only including those in a specific namespace. 2 | @[Athena::Console::Annotations::AsCommand("list", description: "List available commands")] 3 | class Athena::Console::Commands::List < Athena::Console::Command 4 | protected def configure : Nil 5 | self 6 | .argument("namespace", description: "Only list commands in this namespace") { ACON::Descriptor::Application.new(self.application).namespaces.keys } 7 | .option("raw", value_mode: :none, description: "To output raw command list") 8 | .option("format", value_mode: :required, description: "The output format (txt)", default: "txt") { ACON::Helper::Descriptor.new.formats } 9 | .option("short", value_mode: :none, description: "To skip describing command's arguments") 10 | .help( 11 | <<-HELP 12 | The %command.name% command lists all commands: 13 | 14 | %command.full_name% 15 | 16 | You can also display the commands for a specific namespace: 17 | 18 | %command.full_name% test 19 | 20 | It's also possible to get raw list of commands (useful for embedding command runner): 21 | 22 | %command.full_name% --raw 23 | HELP 24 | ) 25 | end 26 | 27 | protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status 28 | ACON::Helper::Descriptor.new.describe( 29 | output, 30 | self.application, 31 | ACON::Descriptor::Context.new( 32 | format: input.option("format", String), 33 | raw_text: input.option("raw", Bool), 34 | namespace: input.argument("namespace", String?), 35 | short: input.option("short", Bool) 36 | ) 37 | ) 38 | 39 | ACON::Command::Status::SUCCESS 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /src/completion/output/bash.cr: -------------------------------------------------------------------------------- 1 | require "./interface" 2 | 3 | # :nodoc: 4 | struct Athena::Console::Completion::Output::Bash < Athena::Console::Completion::Output::Interface 5 | # :nodoc: 6 | record Script, command_name : String, version : Int32 do 7 | ECR.def_to_s "#{__DIR__}/completion.bash" 8 | end 9 | 10 | def write(suggestions : ACON::Completion::Suggestions, output : ACON::Output::Interface) : Nil 11 | values = suggestions.suggested_values.map &.to_s 12 | 13 | suggestions.suggested_options.each do |option| 14 | values << "--#{option.name}" 15 | 16 | if option.negatable? 17 | values << "--no-#{option.name}" 18 | end 19 | end 20 | 21 | output.puts values.join "\n" 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /src/completion/output/completion.fish: -------------------------------------------------------------------------------- 1 | # Adapted from https://github.com/symfony/symfony/blob/503a7b3cb62fb6de70176b07bd1c4242e3addc5b/src/Symfony/Component/Console/Resources/completion.fish 2 | # Crystal doesn\'t get the script as the first arg, so remove it and decrement c by 1 to compensate 3 | 4 | function _athena_<%= @command_name %> 5 | set athena_cmd (commandline -o) 6 | set c (math (count (commandline -oc)) - 1) 7 | 8 | set completecmd "$athena_cmd[1]" "_complete" "--no-interaction" "-sfish" "-a<%= @version %>" 9 | 10 | for i in $athena_cmd[2..] 11 | if [ $i != "" ] 12 | set completecmd $completecmd "-i$i" 13 | end 14 | end 15 | 16 | set completecmd $completecmd "-c$c" 17 | 18 | set sfcomplete ($completecmd) 19 | 20 | for i in $sfcomplete 21 | echo $i 22 | end 23 | end 24 | 25 | complete -c '<%= @command_name %>' -a '(_athena_<%= @command_name %>)' -f 26 | -------------------------------------------------------------------------------- /src/completion/output/completion.zsh: -------------------------------------------------------------------------------- 1 | #compdef <%= @command_name %> 2 | 3 | # 4 | # zsh completions for <%= @command_name %> 5 | # 6 | # References: 7 | # - https://github.com/symfony/symfony/blob/503a7b3cb62fb6de70176b07bd1c4242e3addc5b/src/Symfony/Component/Console/Resources/completion.zsh 8 | 9 | _athena_<%= @command_name %>() { 10 | local lastParam flagPrefix requestComp out comp 11 | local -a completions 12 | 13 | # The user could have moved the cursor backwards on the command-line. 14 | # We need to trigger completion from the $CURRENT location, so we need 15 | # to truncate the command-line ($words) up to the $CURRENT location. 16 | # (We cannot use $CURSOR as its value does not work when a command is an alias.) 17 | words=("${=words[1,CURRENT]}") lastParam=${words[-1]} 18 | 19 | # For zsh, when completing a flag with an = (e.g., <%= @command_name %> -n=) 20 | # completions must be prefixed with the flag 21 | setopt local_options BASH_REMATCH 22 | if [[ "${lastParam}" =~ '-.*=' ]]; then 23 | # We are dealing with a flag with an = 24 | flagPrefix="-P ${BASH_REMATCH}" 25 | fi 26 | 27 | # Prepare the command to obtain completions 28 | # Crystal doesn\'t get the script as the first arg, so skip it when iterating over `words` and decrement CURRENT by 2 instead of 1 to compensate 29 | requestComp="${words[0]} ${words[1]} _complete --no-interaction -szsh -a<%= @version %> -c$((CURRENT-2))" i="" 30 | for w in ${words[@]:1}; do 31 | w=$(printf -- '%b' "$w") 32 | # remove quotes from typed values 33 | quote="${w:0:1}" 34 | if [ "$quote" = \' ]; then 35 | w="${w%\'}" 36 | w="${w#\'}" 37 | elif [ "$quote" = \" ]; then 38 | w="${w%\"}" 39 | w="${w#\"}" 40 | fi 41 | # empty values are ignored 42 | if [ ! -z "$w" ]; then 43 | i="${i}-i${w} " 44 | fi 45 | done 46 | 47 | # Ensure at least 1 input 48 | if [ "${i}" = "" ]; then 49 | requestComp="${requestComp} -i\" \"" 50 | else 51 | requestComp="${requestComp} ${i}" 52 | fi 53 | 54 | # Use eval to handle any environment variables and such 55 | out=$(eval ${requestComp} 2>/dev/null) 56 | 57 | while IFS='\n' read -r comp; do 58 | if [ -n "$comp" ]; then 59 | # If requested, completions are returned with a description. 60 | # The description is preceded by a TAB character. 61 | # For zsh\'s _describe, we need to use a : instead of a TAB. 62 | # We first need to escape any : as part of the completion itself. 63 | comp=${comp//:/\\:} 64 | local tab=$(printf '\t') 65 | comp=${comp//$tab/:} 66 | completions+=${comp} 67 | fi 68 | done < <(printf "%s\n" "${out[@]}") 69 | 70 | # Let inbuilt _describe handle completions 71 | eval _describe "completions" completions $flagPrefix 72 | return $? 73 | } 74 | 75 | compdef _athena_<%= @command_name %> <%= @command_name %> ./<%= @command_name %> 76 | -------------------------------------------------------------------------------- /src/completion/output/fish.cr: -------------------------------------------------------------------------------- 1 | require "./interface" 2 | 3 | # :nodoc: 4 | struct Athena::Console::Completion::Output::Fish < Athena::Console::Completion::Output::Interface 5 | # :nodoc: 6 | record Script, command_name : String, version : Int32 do 7 | ECR.def_to_s "#{__DIR__}/completion.fish" 8 | end 9 | 10 | def write(suggestions : ACON::Completion::Suggestions, output : ACON::Output::Interface) : Nil 11 | values = suggestions.suggested_values.map &.to_s 12 | 13 | suggestions.suggested_options.each do |option| 14 | values << "--#{option.name}" 15 | 16 | if option.negatable? 17 | values << "--no-#{option.name}" 18 | end 19 | end 20 | 21 | output.print values.join "\n" 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /src/completion/output/interface.cr: -------------------------------------------------------------------------------- 1 | require "../suggestions" 2 | 3 | # :nodoc: 4 | module Athena::Console::Completion::Output 5 | abstract struct Interface 6 | # Returns a string representation of the args passed to the command. 7 | abstract def write(suggestions : ACON::Completion::Suggestions, output : ACON::Output::Interface) : Nil 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /src/completion/output/zsh.cr: -------------------------------------------------------------------------------- 1 | # :nodoc: 2 | struct Athena::Console::Completion::Output::Zsh < Athena::Console::Completion::Output::Interface 3 | # :nodoc: 4 | record Script, command_name : String, version : Int32 do 5 | ECR.def_to_s "#{__DIR__}/completion.zsh" 6 | end 7 | 8 | def write(suggestions : ACON::Completion::Suggestions, output : ACON::Output::Interface) : Nil 9 | values = suggestions.suggested_values.map do |v| 10 | "#{v.value}#{(desc = v.description.presence) ? "\t#{desc}" : ""}" 11 | end 12 | 13 | suggestions.suggested_options.each do |option| 14 | values << "--#{option.name}#{(desc = option.description.presence) ? "\t#{desc}" : ""}" 15 | 16 | if option.negatable? 17 | values << "--no-#{option.name}#{(desc = option.description.presence) ? "\t#{desc}" : ""}" 18 | end 19 | end 20 | 21 | output.print "#{values.join "\n"}\n" 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /src/completion/suggestions.cr: -------------------------------------------------------------------------------- 1 | # Stores all the suggested values/options for the current `ACON::Completion::Input`. 2 | class Athena::Console::Completion::Suggestions 3 | # Represents a single suggested values, plus optional description. 4 | record SuggestedValue, value : String, description : String = "" do 5 | def to_s(io : IO) : Nil 6 | @value.to_s io 7 | end 8 | end 9 | 10 | # Returns an array of the suggested `ACON::Input::Option`s. 11 | getter suggested_options = [] of ACON::Input::Option 12 | 13 | # Returns an array of the `ACON::Completion::Suggestions::SuggestedValue`s. 14 | getter suggested_values = [] of ACON::Completion::Suggestions::SuggestedValue 15 | 16 | # Adds each of the provided *values* to `#suggested_values`. 17 | def suggest_values(*values : String) : self 18 | self.suggest_values values 19 | end 20 | 21 | # Adds each of the provided *values* to `#suggested_values`. 22 | def suggest_values(values : Enumerable(String)) : self 23 | values.each do |option| 24 | self.suggest_value option 25 | end 26 | 27 | self 28 | end 29 | 30 | # Adds the provided *value*, and optional *description* to `#suggested_values`. 31 | def suggest_value(value : String, description : String = "") : self 32 | self.suggest_value SuggestedValue.new value, description 33 | end 34 | 35 | # Adds the provided *value* to `#suggested_values`. 36 | def suggest_value(value : ACON::Completion::Suggestions::SuggestedValue) : self 37 | @suggested_values << value 38 | 39 | self 40 | end 41 | 42 | # Adds each of the provided *options* to `#suggested_options`. 43 | def suggest_options(options : ::Enumerable(ACON::Input::Option)) : self 44 | options.each do |option| 45 | self.suggest_option option 46 | end 47 | 48 | self 49 | end 50 | 51 | # Adds the provided *option* to `#suggested_options`. 52 | def suggest_option(option : ACON::Input::Option) : self 53 | @suggested_options << option 54 | 55 | self 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /src/descriptor/context.cr: -------------------------------------------------------------------------------- 1 | class Athena::Console::Descriptor::Context 2 | property format : String 3 | property? raw_text : Bool 4 | property? raw_output : Bool? 5 | property namespace : String? 6 | property total_width : Int32? 7 | property? short : Bool 8 | 9 | def initialize( 10 | @format : String = "txt", 11 | @raw_text : Bool = false, 12 | @raw_output : Bool? = nil, 13 | @namespace : String? = nil, 14 | @total_width : Int32? = nil, 15 | @short : Bool = false, 16 | ) 17 | end 18 | 19 | def_clone 20 | end 21 | -------------------------------------------------------------------------------- /src/descriptor/descriptor.cr: -------------------------------------------------------------------------------- 1 | require "./interface" 2 | 3 | # :nodoc: 4 | abstract class Athena::Console::Descriptor 5 | include Athena::Console::Descriptor::Interface 6 | 7 | getter! output : ACON::Output::Interface 8 | 9 | def describe(output : ACON::Output::Interface, object : _, context : ACON::Descriptor::Context) : Nil 10 | @output = output 11 | 12 | self.describe object, context 13 | end 14 | 15 | protected abstract def describe(application : ACON::Application, context : ACON::Descriptor::Context) : Nil 16 | protected abstract def describe(command : ACON::Command, context : ACON::Descriptor::Context) : Nil 17 | protected abstract def describe(definition : ACON::Input::Definition, context : ACON::Descriptor::Context) : Nil 18 | protected abstract def describe(argument : ACON::Input::Argument, context : ACON::Descriptor::Context) : Nil 19 | protected abstract def describe(option : ACON::Input::Option, context : ACON::Descriptor::Context) : Nil 20 | 21 | protected def describe(obj : _, context : ACON::Descriptor::Context) : Nil 22 | raise "BUG: Failed to describe #{obj}" 23 | end 24 | 25 | protected def write(content : String, decorated : Bool = false) : Nil 26 | self.output.print content, output_type: decorated ? Athena::Console::Output::Type::NORMAL : Athena::Console::Output::Type::RAW 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /src/descriptor/interface.cr: -------------------------------------------------------------------------------- 1 | module Athena::Console::Descriptor::Interface 2 | abstract def describe(output : ACON::Output::Interface, object : _, context : ACON::Descriptor::Context) : Nil 3 | end 4 | -------------------------------------------------------------------------------- /src/exception/command_not_found.cr: -------------------------------------------------------------------------------- 1 | class Athena::Console::Exception::CommandNotFound < ArgumentError 2 | include Athena::Console::Exception 3 | 4 | getter alternatives : Array(String) 5 | 6 | def initialize(message : String, @alternatives : Array(String) = [] of String, @code : Int32 = 0) 7 | super message 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /src/exception/invalid_argument.cr: -------------------------------------------------------------------------------- 1 | class Athena::Console::Exception::InvalidArgument < ArgumentError 2 | include Athena::Console::Exception 3 | 4 | def initialize(message : String, @code : Int32 = 0) 5 | super message 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /src/exception/invalid_option.cr: -------------------------------------------------------------------------------- 1 | class Athena::Console::Exception::InvalidOption < ArgumentError 2 | include Athena::Console::Exception 3 | 4 | def initialize(message : String, @code : Int32 = 0) 5 | super message 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /src/exception/logic.cr: -------------------------------------------------------------------------------- 1 | # Represents a code logic error that should lead directly to a fix in your code. 2 | class Athena::Console::Exception::Logic < ::Exception 3 | include Athena::Console::Exception 4 | 5 | def initialize(message : String, @code : Int32 = 0, cause : ::Exception? = nil) 6 | super message, cause 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /src/exception/missing_input.cr: -------------------------------------------------------------------------------- 1 | require "./runtime" 2 | 3 | class Athena::Console::Exception::MissingInput < Athena::Console::Exception::Runtime 4 | end 5 | -------------------------------------------------------------------------------- /src/exception/namespace_not_found.cr: -------------------------------------------------------------------------------- 1 | class Athena::Console::Exception::NamespaceNotFound < Athena::Console::Exception::CommandNotFound 2 | end 3 | -------------------------------------------------------------------------------- /src/exception/runtime.cr: -------------------------------------------------------------------------------- 1 | class Athena::Console::Exception::Runtime < RuntimeError 2 | include Athena::Console::Exception 3 | 4 | def initialize(message : String, @code : Int32 = 0, cause : ::Exception? = nil) 5 | super message, cause 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /src/ext/terminal.cr: -------------------------------------------------------------------------------- 1 | {% if flag?(:win32) %} 2 | lib LibC 3 | STDOUT_HANDLE = 0xFFFFFFF5 4 | 5 | struct Point 6 | x : UInt16 7 | y : UInt16 8 | end 9 | 10 | struct SmallRect 11 | left : UInt16 12 | top : UInt16 13 | right : UInt16 14 | bottom : UInt16 15 | end 16 | 17 | struct ScreenBufferInfo 18 | dwSize : Point 19 | dwCursorPosition : Point 20 | wAttributes : UInt16 21 | srWindow : SmallRect 22 | dwMaximumWindowSize : Point 23 | end 24 | 25 | alias Handle = Void* 26 | alias ScreenBufferInfoPtr = ScreenBufferInfo* 27 | 28 | fun GetConsoleScreenBufferInfo(handle : Handle, info : ScreenBufferInfoPtr) : Bool 29 | fun GetStdHandle(handle : UInt32) : Handle 30 | end 31 | {% else %} 32 | lib LibC 33 | struct Winsize 34 | ws_row : UShort 35 | ws_col : UShort 36 | ws_xpixel : UShort 37 | ws_ypixel : UShort 38 | end 39 | 40 | # TIOCGWINSZ is a platform dependent magic number passed to ioctl that requests the current terminal window size. 41 | # Values lifted from https://github.com/crystal-term/screen/blob/ea51ee8d1f6c286573c41a7e784d31c80af7b9bb/src/term-screen.cr#L86-L88. 42 | {% begin %} 43 | {% if flag?(:darwin) || flag?(:bsd) %} 44 | TIOCGWINSZ = 0x40087468 45 | {% elsif flag?(:unix) %} 46 | TIOCGWINSZ = 0x5413 47 | {% else %} # Solaris 48 | TIOCGWINSZ = 0x5468 49 | {% end %} 50 | {% end %} 51 | 52 | fun ioctl(fd : Int, request : ULong, ...) : Int 53 | end 54 | {% end %} 55 | -------------------------------------------------------------------------------- /src/formatter/interface.cr: -------------------------------------------------------------------------------- 1 | require "./output_style_interface" 2 | 3 | # A container that stores and applies `ACON::Formatter::OutputStyleInterface`. 4 | # Is responsible for formatting outputted messages as per their styles. 5 | module Athena::Console::Formatter::Interface 6 | # Sets if output messages should be decorated. 7 | abstract def decorated=(@decorated : Bool) 8 | 9 | # Returns `true` if output messages will be decorated, otherwise `false`. 10 | abstract def decorated? : Bool 11 | 12 | # Assigns the provided *style* to the provided *name*. 13 | abstract def set_style(name : String, style : ACON::Formatter::OutputStyleInterface) : Nil 14 | 15 | # Returns `true` if `self` has a style with the provided *name*, otherwise `false`. 16 | abstract def has_style?(name : String) : Bool 17 | 18 | # Returns an `ACON::Formatter::OutputStyleInterface` with the provided *name*. 19 | abstract def style(name : String) : ACON::Formatter::OutputStyleInterface 20 | 21 | # Formats the provided *message* according to the stored styles. 22 | abstract def format(message : String?) : String 23 | end 24 | -------------------------------------------------------------------------------- /src/formatter/null.cr: -------------------------------------------------------------------------------- 1 | require "./interface" 2 | 3 | # :nodoc: 4 | class Athena::Console::Formatter::Null 5 | include Athena::Console::Formatter::Interface 6 | 7 | @style : ACON::Formatter::NullStyle? = nil 8 | 9 | def decorated=(@decorated : Bool) 10 | end 11 | 12 | def decorated? : Bool 13 | false 14 | end 15 | 16 | def set_style(name : String, style : ACON::Formatter::OutputStyleInterface) : Nil 17 | end 18 | 19 | def has_style?(name : String) : Bool 20 | false 21 | end 22 | 23 | def style(name : String) : ACON::Formatter::OutputStyleInterface 24 | @style ||= ACON::Formatter::NullStyle.new 25 | end 26 | 27 | def format(message : String?) : String 28 | message 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /src/formatter/null_style.cr: -------------------------------------------------------------------------------- 1 | # :nodoc: 2 | class Athena::Console::Formatter::NullStyle 3 | include Athena::Console::Formatter::OutputStyleInterface 4 | 5 | # :inherit: 6 | def foreground=(foreground : Colorize::Color) 7 | end 8 | 9 | # :inherit: 10 | def background=(background : Colorize::Color) 11 | end 12 | 13 | # :inherit: 14 | def add_option(option : Colorize::Mode) : Nil 15 | end 16 | 17 | # :inherit: 18 | def remove_option(option : Colorize::Mode) : Nil 19 | end 20 | 21 | # :inherit: 22 | def apply(text : String) : String 23 | text 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /src/formatter/output_formatter_style_stack.cr: -------------------------------------------------------------------------------- 1 | # :nodoc: 2 | struct Athena::Console::Formatter::OutputStyleStack 3 | property empty_style : ACON::Formatter::OutputStyleInterface 4 | 5 | @styles = Array(ACON::Formatter::OutputStyleInterface).new 6 | 7 | def initialize(@empty_style : ACON::Formatter::OutputStyleInterface = ACON::Formatter::OutputStyle.new) 8 | self.reset 9 | end 10 | 11 | def reset : Nil 12 | @styles.clear 13 | end 14 | 15 | def <<(style : ACON::Formatter::OutputStyleInterface) : Nil 16 | @styles << style 17 | end 18 | 19 | def pop(style : ACON::Formatter::OutputStyleInterface? = nil) : ACON::Formatter::OutputStyleInterface 20 | return @empty_style if @styles.empty? 21 | 22 | return @styles.pop if style.nil? 23 | 24 | @styles.reverse_each.each_with_index do |stacked_style, idx| 25 | if style.apply("") == stacked_style.apply("") 26 | @styles = @styles[0...idx] 27 | 28 | return stacked_style 29 | end 30 | end 31 | 32 | raise ACON::Exception::InvalidArgument.new "Provided style is not present in the stack." 33 | end 34 | 35 | def current : ACON::Formatter::OutputStyleInterface 36 | @styles.last? || @empty_style 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /src/formatter/output_style.cr: -------------------------------------------------------------------------------- 1 | require "colorize" 2 | require "./output_style_interface" 3 | 4 | # Default implementation of `ACON::Formatter::OutputStyleInterface`. 5 | struct Athena::Console::Formatter::OutputStyle 6 | include Athena::Console::Formatter::OutputStyleInterface 7 | 8 | # :inherit: 9 | setter foreground : Colorize::Color = :default 10 | 11 | # :inherit: 12 | setter background : Colorize::Color = :default 13 | 14 | # :inherit: 15 | setter options : Colorize::Mode = :none 16 | 17 | # Sets the `href` that `self` should link to. 18 | setter href : String? = nil 19 | 20 | # :nodoc: 21 | getter? handles_href_gracefully : Bool do 22 | "JetBrains-JediTerm" != ENV["TERMINAL_EMULATOR"]? && (!ENV.has_key?("KONSOLE_VERSION") || ENV["KONSOLE_VERSION"].to_i > 201_100) 23 | end 24 | 25 | def initialize(foreground : Colorize::Color | String = :default, background : Colorize::Color | String = :default, @options : Colorize::Mode = :none) 26 | self.foreground = foreground 27 | self.background = background 28 | end 29 | 30 | # :inherit: 31 | def add_option(option : Colorize::Mode) : Nil 32 | @options |= option 33 | end 34 | 35 | # :ditto: 36 | def add_option(option : String) : Nil 37 | self.add_option Colorize::Mode.parse option 38 | end 39 | 40 | # :inherit: 41 | def background=(color : String) 42 | if hex_value = color.lchop? '#' 43 | r, g, b = hex_value.hexbytes 44 | return @background = Colorize::ColorRGB.new r, g, b 45 | end 46 | 47 | @background = Colorize::ColorANSI.parse color 48 | end 49 | 50 | # :inherit: 51 | def foreground=(foreground : String) 52 | if hex_value = foreground.lchop? '#' 53 | r, g, b = hex_value.hexbytes 54 | return @foreground = Colorize::ColorRGB.new r, g, b 55 | end 56 | 57 | @foreground = Colorize::ColorANSI.parse foreground 58 | end 59 | 60 | # :inherit: 61 | def remove_option(option : Colorize::Mode) : Nil 62 | @options ^= option 63 | end 64 | 65 | # :ditto: 66 | def remove_option(option : String) : Nil 67 | self.remove_option Colorize::Mode.parse option 68 | end 69 | 70 | # :inherit: 71 | def apply(text : String) : String 72 | if (href = @href) && self.handles_href_gracefully? 73 | text = "\e]8;;#{href}\e\\#{text}\e]8;;\e\\" 74 | end 75 | 76 | color = Colorize::Object(String) 77 | .new(text) 78 | .fore(@foreground) 79 | .back(@background) 80 | 81 | if options = @options 82 | options.each do |mode| 83 | color.mode mode 84 | end 85 | end 86 | 87 | color.to_s 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /src/formatter/output_style_interface.cr: -------------------------------------------------------------------------------- 1 | require "colorize" 2 | 3 | # Output styles represent reusable formatting information that can be used when formatting output messages. 4 | # `Athena::Console` comes bundled with a few common styles including: 5 | # 6 | # * error 7 | # * info 8 | # * comment 9 | # * question 10 | # 11 | # Whenever you output text via an `ACON::Output::Interface`, you can surround the text with tags to color its output. For example: 12 | # 13 | # ``` 14 | # # Green text 15 | # output.puts "foo" 16 | # 17 | # # Yellow text 18 | # output.puts "foo" 19 | # 20 | # # Black text on a cyan background 21 | # output.puts "foo" 22 | # 23 | # # White text on a red background 24 | # output.puts "foo" 25 | # ``` 26 | # 27 | # ## Custom Styles 28 | # 29 | # Custom styles can also be defined/used: 30 | # 31 | # ``` 32 | # my_style = ACON::Formatter::OutputStyle.new :red, "#f87b05", Colorize::Mode[:bold, :underline] 33 | # output.formatter.set_style "fire", my_style 34 | # 35 | # output.puts "foo" 36 | # ``` 37 | # 38 | # ### Global Custom Styles 39 | # 40 | # You can also make your style global by extending `ACON::Application` and adding it within the `#configure_io` method: 41 | # 42 | # ``` 43 | # class MyCustomApplication < ACON::Application 44 | # protected def configure_io(input : ACON::Input::Interface, output : ACON::Output::Interface) : Nil 45 | # super 46 | # 47 | # my_style = ACON::Formatter::OutputStyle.new :red, "#f87b05", Colorize::Mode[:bold, :underline] 48 | # output.formatter.set_style "fire", my_style 49 | # end 50 | # end 51 | # ``` 52 | # 53 | # ## Inline Styles 54 | # 55 | # Styles can also be defined inline when printing a message: 56 | # 57 | # ``` 58 | # # Using named colors 59 | # output.puts "foo" 60 | # 61 | # # Using hexadecimal colors 62 | # output.puts "foo" 63 | # 64 | # # Black text on a cyan background 65 | # output.puts "foo" 66 | # 67 | # # Bold text on a yellow background 68 | # output.puts "foo" 69 | # 70 | # # Bold text with underline. 71 | # output.puts "foo" 72 | # ``` 73 | # 74 | # ## Clickable Links 75 | # 76 | # Commands can use the special `href` tag to display links within the console. 77 | # 78 | # ``` 79 | # output.puts "Athena" 80 | # ``` 81 | # 82 | # If your terminal [supports](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda) it, you would be able to click 83 | # the text and have it open in your default browser. Otherwise, you will see it as regular text. 84 | module Athena::Console::Formatter::OutputStyleInterface 85 | # Sets the foreground color of `self`. 86 | abstract def foreground=(foreground : Colorize::Color) 87 | 88 | # Sets the background color of `self`. 89 | abstract def background=(background : Colorize::Color) 90 | 91 | # Adds a text mode to `self`. 92 | abstract def add_option(option : Colorize::Mode) : Nil 93 | 94 | # Removes a text mode to `self`. 95 | abstract def remove_option(option : Colorize::Mode) : Nil 96 | 97 | # Applies `self` to the provided *text*. 98 | abstract def apply(text : String) : String 99 | end 100 | -------------------------------------------------------------------------------- /src/formatter/wrappable_interface.cr: -------------------------------------------------------------------------------- 1 | require "./interface" 2 | 3 | # Extension of `ACON::Formatter::Interface` that supports word wrapping. 4 | module Athena::Console::Formatter::WrappableInterface 5 | include Athena::Console::Formatter::Interface 6 | 7 | # Formats the provided *message* according to the defined styles, wrapping it at the provided *width*. 8 | # A width of `0` means no wrapping. 9 | abstract def format_and_wrap(message : String?, width : Int32) : String 10 | end 11 | -------------------------------------------------------------------------------- /src/helper/athena_question.cr: -------------------------------------------------------------------------------- 1 | abstract class Athena::Console::Helper; end 2 | 3 | require "./question" 4 | 5 | # Extension of `ACON::Helper::Question` that provides more structured output. 6 | # 7 | # See `ACON::Style::Athena`. 8 | class Athena::Console::Helper::AthenaQuestion < Athena::Console::Helper::Question 9 | protected def write_error(output : ACON::Output::Interface, error : ::Exception) : Nil 10 | if output.is_a? ACON::Style::Athena 11 | output.new_line 12 | output.error error.message || "" 13 | 14 | return 15 | end 16 | 17 | super 18 | end 19 | 20 | # ameba:disable Metrics/CyclomaticComplexity 21 | protected def write_prompt(output : ACON::Output::Interface, question : ACON::Question::Base) : Nil 22 | text = ACON::Formatter::Output.escape_trailing_backslash question.question 23 | default = question.default 24 | 25 | if question.multi_line? 26 | text = "#{text} (press #{self.eof_shortcut} to continue)" 27 | end 28 | 29 | text = if default.nil? 30 | " #{text}:" 31 | elsif question.is_a? ACON::Question::Confirmation 32 | %( #{text} (yes/no) [#{default ? "yes" : "no"}]:) 33 | elsif question.is_a? ACON::Question::MultipleChoice 34 | choices = question.choices 35 | default = case default 36 | when String then default.split(',').map! do |item| 37 | if idx = item.to_i? 38 | item = idx 39 | end 40 | 41 | choices[item]? || item.to_s 42 | end 43 | else 44 | [default] 45 | end 46 | 47 | %( #{text} [#{ACON::Formatter::Output.escape default.join(", ")}]:) 48 | elsif question.is_a? ACON::Question::Choice 49 | choices = question.choices 50 | 51 | " #{text} [#{ACON::Formatter::Output.escape default.to_s}]:" 52 | else 53 | " #{text} [#{ACON::Formatter::Output.escape default.to_s}]:" 54 | end 55 | 56 | output.puts text 57 | 58 | prompt = " > " 59 | 60 | if question.is_a? ACON::Question::AbstractChoice 61 | output.puts self.format_choice_question_choices question, "comment" 62 | 63 | prompt = question.prompt 64 | end 65 | 66 | output.print prompt 67 | end 68 | 69 | private def eof_shortcut : String 70 | # TODO: Windows uses Ctrl+Z + Enter 71 | 72 | "Ctrl+D" 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /src/helper/descriptor_helper.cr: -------------------------------------------------------------------------------- 1 | # :nodoc: 2 | class Athena::Console::Helper::Descriptor < Athena::Console::Helper 3 | @descriptors = Hash(String, ACON::Descriptor::Interface).new 4 | 5 | def initialize 6 | self.register "txt", ACON::Descriptor::Text.new 7 | end 8 | 9 | def describe(output : ACON::Output::Interface, object : _, context : ACON::Descriptor::Context) : Nil 10 | raise ACON::Exception::InvalidArgument.new "Unsupported format #{context.format}." unless descriptor = @descriptors[context.format]? 11 | 12 | descriptor.describe output, object, context 13 | end 14 | 15 | def register(format : String, descriptor : ACON::Descriptor::Interface) : self 16 | @descriptors[format] = descriptor 17 | 18 | self 19 | end 20 | 21 | def formats : Array(String) 22 | @descriptors.keys 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /src/helper/formatter.cr: -------------------------------------------------------------------------------- 1 | # Provides additional ways to format output messages than `ACON::Formatter::OutputStyle` can do alone, such as: 2 | # 3 | # * Printing messages in a section 4 | # * Printing messages in a block 5 | # * Print truncated messages. 6 | # 7 | # The provided methods return a `String` which could then be passed to `ACON::Output::Interface#print` or `ACON::Output::Interface#puts`. 8 | class Athena::Console::Helper::Formatter < Athena::Console::Helper 9 | # Prints the provided *message* in the provided *section*. 10 | # Optionally allows setting the *style* of the section. 11 | # 12 | # ```text 13 | # [SomeSection] Here is some message related to that section 14 | # ``` 15 | # 16 | # ``` 17 | # output.puts formatter.format_section "SomeSection", "Here is some message related to that section" 18 | # ``` 19 | def format_section(section : String, message : String, style : String = "info") : String 20 | "<#{style}>[#{section}] #{message}" 21 | end 22 | 23 | # Prints the provided *messages* in a block formatted according to the provided *style*, with a total width a bit more than the longest line. 24 | # 25 | # The *large* options adds additional padding, one blank line above and below the messages, and 2 more spaces on the left and right. 26 | # 27 | # ``` 28 | # output.puts formatter.format_block({"Error!", "Something went wrong"}, "error", true) 29 | # ``` 30 | def format_block(messages : String | Enumerable(String), style : String, large : Bool = false) 31 | messages = messages.is_a?(String) ? {messages} : messages 32 | 33 | len = 0 34 | lines = [] of String 35 | 36 | messages.each do |message| 37 | message = ACON::Formatter::Output.escape message 38 | lines << (large ? " #{message} " : " #{message} ") 39 | len = Math.max (message.size + (large ? 4 : 2)), len 40 | end 41 | 42 | messages = large ? [" " * len] : [] of String 43 | 44 | lines.each do |line| 45 | messages << %(#{line}#{" " * (len - line.delete('\\').size)}) 46 | end 47 | 48 | if large 49 | messages << " " * len 50 | end 51 | 52 | messages.each_with_index do |line, idx| 53 | messages[idx] = "<#{style}>#{line}" 54 | end 55 | 56 | messages.join '\n' 57 | end 58 | 59 | # Truncates the provided *message* to be at most *length* characters long, 60 | # with the optional *suffix* appended to the end. 61 | # 62 | # ``` 63 | # message = "This is a very long message, which should be truncated" 64 | # truncated_message = formatter.truncate message, 7 65 | # output.puts truncated_message # => This is... 66 | # ``` 67 | # 68 | # If *length* is negative, it will start truncating from the end. 69 | # 70 | # ``` 71 | # message = "This is a very long message, which should be truncated" 72 | # truncated_message = formatter.truncate message, -4 73 | # output.puts truncated_message # => This is a very long message, which should be trunc... 74 | # ``` 75 | def truncate(message : String, length : Int, suffix : String = "...") : String 76 | computed_length = length - self.class.width suffix 77 | 78 | if computed_length > self.class.width message 79 | return message 80 | end 81 | 82 | "#{message[0...length]}#{suffix}" 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /src/helper/helper.cr: -------------------------------------------------------------------------------- 1 | require "./interface" 2 | 3 | # Contains `ACON::Helper::Interface` implementations that can be used to help with various tasks. 4 | # Such as asking questions, customizing the output format, or generating tables. 5 | # 6 | # This class also acts as a base type that implements common functionality between each helper. 7 | abstract class Athena::Console::Helper 8 | include Athena::Console::Helper::Interface 9 | 10 | private TIME_FORMATS = { 11 | {0, "< 1 sec", nil}, 12 | {1, "1 sec", nil}, 13 | {2, "secs", 1}, 14 | {60, "1 min", nil}, 15 | {120, "mins", 60}, 16 | {3_600, "1 hr", nil}, 17 | {7_200, "hrs", 3_600}, 18 | {86_400, "1 day", nil}, 19 | {172_800, "days", 86_400}, 20 | } 21 | 22 | # Formats the provided *span* of time as a human readable string. 23 | # 24 | # ``` 25 | # ACON::Helper.format_time 10.seconds # => "10 secs" 26 | # ACON::Helper.format_time 4.minutes # => "4 mins" 27 | # ACON::Helper.format_time 74.minutes # => "1 hr" 28 | # ``` 29 | def self.format_time(span : Time::Span) : String 30 | self.format_time span.total_seconds 31 | end 32 | 33 | # Formats the provided *seconds* as a human readable string. 34 | # 35 | # ``` 36 | # ACON::Helper.format_time 10 # => "10 secs" 37 | # ACON::Helper.format_time 240 # => "4 mins" 38 | # ACON::Helper.format_time 4400 # => "1 hr" 39 | # ``` 40 | def self.format_time(seconds : Number) : String 41 | TIME_FORMATS.each_with_index do |format, idx| 42 | min_seconds, label, max_seconds = format 43 | 44 | next unless seconds >= min_seconds 45 | 46 | if ((next_format = TIME_FORMATS[idx + 1]?) && (seconds < next_format[0])) || idx == TIME_FORMATS.size - 1 47 | return label if max_seconds.nil? 48 | 49 | return "#{(seconds // max_seconds).to_i} #{label}" 50 | end 51 | end 52 | 53 | raise "BUG: Unable to format time: #{seconds}." 54 | end 55 | 56 | # Returns a new string with all of its ANSI formatting removed. 57 | def self.remove_decoration(formatter : ACON::Formatter::Interface, string : String) : String 58 | is_decorated = formatter.decorated? 59 | formatter.decorated = false 60 | 61 | # Remove <...> formatting 62 | string = formatter.format string 63 | 64 | # Remove already formatted characters 65 | string = string.gsub /\033\[[^m]*m/, "" 66 | 67 | # Remove terminal hyperlinks 68 | string = string.gsub /\033]8;[^;]*;[^\033]*\033\\/, "" 69 | 70 | formatter.decorated = is_decorated 71 | 72 | string 73 | end 74 | 75 | # Returns the width of a string; where the width is how many character positions the string will use. 76 | # 77 | # TODO: Support double width chars. 78 | def self.width(string : String) : Int32 79 | string.size 80 | end 81 | 82 | property helper_set : ACON::Helper::HelperSet? = nil 83 | end 84 | -------------------------------------------------------------------------------- /src/helper/helper_set.cr: -------------------------------------------------------------------------------- 1 | # The container that stores various `ACON::Helper::Interface` implementations, keyed by their class. 2 | # 3 | # Each application includes a default helper set, but additional ones may be added. 4 | # See `ACON::Application#helper_set`. 5 | # 6 | # These helpers can be accessed from within a command via the `ACON::Command#helper` method. 7 | class Athena::Console::Helper::HelperSet 8 | @helpers = Hash(ACON::Helper.class, ACON::Helper::Interface).new 9 | 10 | def self.new(*helpers : ACON::Helper::Interface) : self 11 | helper_set = new 12 | helpers.each do |helper| 13 | helper_set << helper 14 | end 15 | helper_set 16 | end 17 | 18 | def initialize(@helpers : Hash(ACON::Helper.class, ACON::Helper::Interface) = Hash(ACON::Helper.class, ACON::Helper::Interface).new); end 19 | 20 | # Adds the provided *helper* to `self`. 21 | def <<(helper : ACON::Helper::Interface) : Nil 22 | @helpers[helper.class] = helper 23 | 24 | helper.helper_set = self 25 | end 26 | 27 | # Returns `true` if `self` has a helper for the provided *helper_class*, otherwise `false`. 28 | def has?(helper_class : ACON::Helper.class) : Bool 29 | @helpers.has_key? helper_class 30 | end 31 | 32 | # Returns the helper of the provided *helper_class*, or `nil` if it is not defined. 33 | def []?(helper_class : T.class) : T? forall T 34 | {% T.raise "Helper class type '#{T}' is not an 'ACON::Helper::Interface'." unless T <= ACON::Helper::Interface %} 35 | 36 | @helpers[helper_class]?.as? T 37 | end 38 | 39 | # Returns the helper of the provided *helper_class*, or raises if it is not defined. 40 | def [](helper_class : T.class) : T forall T 41 | self.[helper_class]? || raise ACON::Exception::InvalidArgument.new "The helper '#{helper_class}' is not defined." 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /src/helper/interface.cr: -------------------------------------------------------------------------------- 1 | module Athena::Console::Helper::Interface 2 | # Sets the `ACON::Helper::HelperSet` related to `self`. 3 | abstract def helper_set=(helper_set : ACON::Helper::HelperSet?) 4 | 5 | # Returns the `ACON::Helper::HelperSet` related to `self`, if any. 6 | abstract def helper_set : ACON::Helper::HelperSet? 7 | end 8 | -------------------------------------------------------------------------------- /src/helper/output_wrapper.cr: -------------------------------------------------------------------------------- 1 | # :nodoc: 2 | # 3 | # Adapted from https://github.com/symfony/symfony/blob/fbf6f56ca7321e28d9a4368e18b9da683c296046/src/Symfony/Component/Console/Helper/OutputWrapper.php 4 | struct Athena::Console::Helper::OutputWrapper 5 | def initialize(@allow_cut_urls : Bool = false); end 6 | 7 | def wrap(text : String, width : Int32, separator : String = "\n") : String 8 | return text if width.zero? 9 | 10 | row_pattern = if @allow_cut_urls 11 | %r((?:<(?:(?:[a-z](?:[^\\<>]*+ | \\.)*)|/(?:[a-z][^<>]*+)?)>|.){1,#{width}}) 12 | else 13 | %r((?:<(?:(?:[a-z](?:[^\\<>]*+ | \\.)*)|/(?:[a-z][^<>]*+)?)>|.|https?://\S+){1,#{width}}) 14 | end 15 | 16 | pattern = %r((?:((?>(#{row_pattern.source})((?<=[^\S\r\n])[^\S\r\n]?|(?=\r?\n)|$|[^\S\r\n]))|(#{row_pattern.source}))(?:\r?\n)?|(?:\r?\n|$)))imx 17 | 18 | text 19 | .gsub(pattern, "\\0#{separator}") 20 | .rstrip(separator) 21 | .gsub " #{separator}", separator 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /src/helper/table_cell_style.cr: -------------------------------------------------------------------------------- 1 | # Represents the styling for a specific `ACON::Helper::Table::Cell`. 2 | struct Athena::Console::Helper::Table::CellStyle 3 | # Returns the foreground color for this cell. 4 | # 5 | # Can be any color string supported via [ACON::Formatter::OutputStyleInterface][Athena::Console::Formatter::OutputStyleInterface--inline-styles], 6 | # e.g. named (`"red"`) or hexadecimal (`"#38bdc2"`) colors. 7 | getter foreground : String 8 | 9 | # Returns the background color for this cell. 10 | # 11 | # Can be any color string supported via [ACON::Formatter::OutputStyleInterface][Athena::Console::Formatter::OutputStyleInterface--inline-styles], 12 | # e.g. named (`"red"`) or hexadecimal (`"#38bdc2"`) colors. 13 | getter background : String 14 | 15 | # How the text should be aligned in the cell. 16 | # 17 | # See `ACON::Helper::Table::Alignment`. 18 | getter align : ACON::Helper::Table::Alignment 19 | 20 | # A `sprintf` format string representing the content of the cell. 21 | # Should have a single `%s` representing the cell's value. 22 | # 23 | # Can be used to reuse [custom style tags][Athena::Console::Formatter::OutputStyleInterface--custom-styles]. 24 | # E.g. `"%s"`. 25 | getter format : String? 26 | 27 | def initialize( 28 | @foreground : String = "default", 29 | @background : String = "default", 30 | @align : ACON::Helper::Table::Alignment = :left, 31 | @format : String? = nil, 32 | ) 33 | end 34 | 35 | protected def tag : String 36 | "fg=#{@foreground};bg=#{@background}" 37 | end 38 | 39 | protected def pad(string : String, width : Int32, padding_char) : String 40 | case @align 41 | in .left? then string.ljust width, padding_char 42 | in .right? then string.rjust width, padding_char 43 | in .center? then string.center width, padding_char 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /src/input/streamable.cr: -------------------------------------------------------------------------------- 1 | # An extension of `ACON::Input::Interface` that supports input stream [IOs](https://crystal-lang.org/api/IO.html). 2 | # 3 | # Allows customizing where the input data is read from. 4 | # Defaults to `STDIN`. 5 | module Athena::Console::Input::Streamable 6 | include Athena::Console::Input::Interface 7 | 8 | # Returns the input stream. 9 | abstract def stream : IO? 10 | 11 | # Sets the input stream. 12 | abstract def stream=(@stream : IO?) 13 | end 14 | -------------------------------------------------------------------------------- /src/input/string_line.cr: -------------------------------------------------------------------------------- 1 | # An `ACON::Input::Interface` based on a command line string. 2 | class Athena::Console::Input::StringLine < Athena::Console::Input::ARGV 3 | private REGEX_UNQUOTED_STRING = /([^\s\\]+?)/ 4 | private REGEX_QUOTED_STRING = /(?:"([^"\\]*(?:\\.[^"\\]*)*)"|\'([^\'\\]*(?:\\.[^\'\\]*)*)\')/ 5 | 6 | def initialize(input : String) 7 | super [] of String 8 | 9 | @tokens = self.tokenize input 10 | end 11 | 12 | private def tokenize(input : String) : Array(String) 13 | tokens = [] of String 14 | length = input.size 15 | idx = 0 16 | token = "" 17 | 18 | while idx < length 19 | if '\\' == input[idx] 20 | idx += 1 21 | token += input[idx]? || "" 22 | idx += 1 23 | next 24 | end 25 | 26 | match = if m = input.match /\G\s+/, idx 27 | unless token.blank? 28 | tokens << token 29 | token = "" 30 | end 31 | 32 | m 33 | elsif m = input.match /\G([^="\'\s]+?)(=?)(#{REGEX_QUOTED_STRING}+)/, idx 34 | token += %(#{m[1]}#{m[2]}#{m[3][1...-1].gsub(/("\'|\'"|\'\'|\"\")/, "").gsub(/\\'/, {"\\'" => "'"})}) 35 | m 36 | elsif m = input.match /\G#{REGEX_QUOTED_STRING}/, idx 37 | token += m[0][1...-1].gsub(/\\'/, {"\\'" => "'"}) 38 | m 39 | elsif m = input.match /\G#{REGEX_UNQUOTED_STRING}/, idx 40 | token += m[1] 41 | m 42 | else 43 | raise ACON::Exception::InvalidArgument.new "Unable to parse input neat '... #{input[idx, 10]} ...'." 44 | end 45 | 46 | idx += match[0].size 47 | end 48 | 49 | tokens << token unless token.blank? 50 | 51 | tokens 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /src/input/value/array.cr: -------------------------------------------------------------------------------- 1 | abstract struct Athena::Console::Input::Value; end 2 | 3 | # :nodoc: 4 | struct Athena::Console::Input::Value::Array < Athena::Console::Input::Value 5 | getter value : ::Array(Athena::Console::Input::Value) 6 | 7 | def self.from_array(array : ::Array) : self 8 | new(array.map { |item| ACON::Input::Value.from_value item }) 9 | end 10 | 11 | def self.new(value) 12 | new [ACON::Input::Value.from_value value] 13 | end 14 | 15 | def self.new 16 | new [] of ACON::Input::Value 17 | end 18 | 19 | def initialize(@value : ::Array(Athena::Console::Input::Value)); end 20 | 21 | def <<(value) 22 | @value << ACON::Input::Value.from_value value 23 | end 24 | 25 | def get(type : ::Array(T).class) : ::Array(T) forall T 26 | arr = ::Array(T).new 27 | 28 | @value.each do |v| 29 | arr << v.get T 30 | end 31 | 32 | arr 33 | end 34 | 35 | def get(type : ::Array(T)?.class) : ::Array(T)? forall T 36 | arr = ::Array(T).new 37 | 38 | @value.each do |v| 39 | arr << v.get T 40 | end 41 | 42 | arr || nil 43 | end 44 | 45 | def to_s(io : IO) : ::Nil 46 | @value.join io, ',' 47 | end 48 | 49 | # :nodoc: 50 | forward_missing_to @value 51 | end 52 | -------------------------------------------------------------------------------- /src/input/value/bool.cr: -------------------------------------------------------------------------------- 1 | # :nodoc: 2 | struct Athena::Console::Input::Value::Bool < Athena::Console::Input::Value 3 | getter value : ::Bool 4 | 5 | def initialize(@value : ::Bool); end 6 | 7 | def get(type : ::Bool.class) : ::Bool 8 | @value 9 | end 10 | 11 | def get(type : ::Bool?.class) : ::Bool? 12 | @value.try do |v| 13 | return v 14 | end 15 | 16 | nil 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /src/input/value/nil.cr: -------------------------------------------------------------------------------- 1 | # :nodoc: 2 | struct Athena::Console::Input::Value::Nil < Athena::Console::Input::Value 3 | def value : ::Nil; end 4 | end 5 | -------------------------------------------------------------------------------- /src/input/value/number.cr: -------------------------------------------------------------------------------- 1 | # :nodoc: 2 | struct Athena::Console::Input::Value::Number < Athena::Console::Input::Value 3 | getter value : ::Number::Primitive 4 | 5 | def initialize(@value : ::Number::Primitive); end 6 | 7 | {% for type in ::Number::Primitive.union_types %} 8 | def get(type : {{type.id}}.class) : {{type.id}} 9 | {{type.id}}.new @value 10 | end 11 | 12 | def get(type : {{type.id}}?.class) : {{type.id}}? 13 | {{type.id}}.new(@value) || nil 14 | end 15 | {% end %} 16 | end 17 | -------------------------------------------------------------------------------- /src/input/value/string.cr: -------------------------------------------------------------------------------- 1 | # :nodoc: 2 | struct Athena::Console::Input::Value::String < Athena::Console::Input::Value 3 | getter value : ::String 4 | 5 | def initialize(@value : ::String); end 6 | 7 | def get(type : ::Bool.class) : ::Bool 8 | raise ACON::Exception::Logic.new "'#{@value}' is not a valid 'Bool'." unless @value.in? "true", "false" 9 | 10 | @value == "true" 11 | end 12 | 13 | def get(type : ::Bool?.class) : ::Bool? 14 | (@value == "true").try do |v| 15 | raise ACON::Exception::Logic.new "'#{@value}' is not a valid 'Bool?'." unless @value.in? "true", "false" 16 | return v 17 | end 18 | 19 | nil 20 | end 21 | 22 | def get(type : ::Array(T).class) : ::Array(T) forall T 23 | Array.from_array(@value.split(',')).get ::Array(T) 24 | end 25 | 26 | def get(type : ::Array(T)?.class) : ::Array(T)? forall T 27 | Array.from_array(@value.split(',')).get ::Array(T)? 28 | end 29 | 30 | {% for type in ::Number::Primitive.union_types %} 31 | def get(type : {{type.id}}.class) : {{type.id}} 32 | {{type.id}}.new @value 33 | rescue ArgumentError 34 | raise ACON::Exception::Logic.new "'#{@value}' is not a valid '#{{{type.id}}}'." 35 | end 36 | 37 | def get(type : {{type.id}}?.class) : {{type.id}}? 38 | {{type.id}}.new(@value) || nil 39 | rescue ArgumentError 40 | raise ACON::Exception::Logic.new "'#{@value}' is not a valid '#{{{type.id}}}'." 41 | end 42 | {% end %} 43 | end 44 | -------------------------------------------------------------------------------- /src/input/value/value.cr: -------------------------------------------------------------------------------- 1 | # :nodoc: 2 | abstract struct Athena::Console::Input::Value 3 | def self.from_value(value : T) : self forall T 4 | case value 5 | when ACON::Input::Value then value 6 | when ::Nil then ACON::Input::Value::Nil.new 7 | when ::String then ACON::Input::Value::String.new value 8 | when ::Number then ACON::Input::Value::Number.new value 9 | when ::Bool then ACON::Input::Value::Bool.new value 10 | when ::Array then ACON::Input::Value::Array.from_array value 11 | else 12 | raise "Unsupported type: #{T}." 13 | end 14 | end 15 | 16 | def get(type : ::String.class) : ::String 17 | self.to_s 18 | end 19 | 20 | def get(type : ::String?.class) : ::String? 21 | self.to_s.presence 22 | end 23 | 24 | def get(type : T.class) forall T 25 | raise ACON::Exception::Logic.new "'#{self.value}' is not a valid '#{T}'." 26 | end 27 | 28 | def to_s(io : IO) : ::Nil 29 | self.value.to_s io 30 | end 31 | 32 | abstract def value 33 | end 34 | -------------------------------------------------------------------------------- /src/loader/factory.cr: -------------------------------------------------------------------------------- 1 | require "./interface" 2 | 3 | # A default implementation of `ACON::Loader::Interface` that accepts a `Hash(String, Proc(ACON::Command))`. 4 | # 5 | # A factory could then be set on the `ACON::Application`: 6 | # 7 | # ``` 8 | # application = MyCustomApplication.new "My CLI" 9 | # 10 | # application.command_loader = Athena::Console::Loader::Factory.new({ 11 | # "command1" => Proc(ACON::Command).new { Command1.new }, 12 | # "app:create-user" => Proc(ACON::Command).new { CreateUserCommand.new }, 13 | # }) 14 | # 15 | # application.run 16 | # ``` 17 | struct Athena::Console::Loader::Factory 18 | include Athena::Console::Loader::Interface 19 | 20 | @factories : Hash(String, Proc(ACON::Command)) 21 | 22 | def initialize(@factories : Hash(String, Proc(ACON::Command))); end 23 | 24 | # :inherit: 25 | def get(name : String) : ACON::Command 26 | if factory = @factories[name]? 27 | factory.call 28 | else 29 | raise ACON::Exception::CommandNotFound.new "Command '#{name}' does not exist." 30 | end 31 | end 32 | 33 | # :inherit: 34 | def has?(name : String) : Bool 35 | @factories.has_key? name 36 | end 37 | 38 | # :inherit: 39 | def names : Array(String) 40 | @factories.keys 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /src/loader/interface.cr: -------------------------------------------------------------------------------- 1 | # Normally the `ACON::Application#add` method requires instances of each command to be provided. 2 | # `ACON::Loader::Interface` provides a way to lazily instantiate only the command(s) being called, 3 | # which can be more performant since not every command needs instantiated. 4 | module Athena::Console::Loader::Interface 5 | # Returns an `ACON::Command` with the provided *name*. 6 | # Raises `ACON::Exception::CommandNotFound` if it is not defined. 7 | abstract def get(name : String) : ACON::Command 8 | 9 | # Returns `true` if `self` has a command with the provided *name*, otherwise `false`. 10 | abstract def has?(name : String) : Bool 11 | 12 | # Returns all of the command names defined within `self`. 13 | abstract def names : Array(String) 14 | end 15 | -------------------------------------------------------------------------------- /src/output/console_output.cr: -------------------------------------------------------------------------------- 1 | abstract class Athena::Console::Output; end 2 | 3 | require "./console_output_interface" 4 | require "./io" 5 | 6 | # An `ACON::Output::ConsoleOutputInterface` that wraps `STDOUT` and `STDERR`. 7 | class Athena::Console::Output::ConsoleOutput < Athena::Console::Output::IO 8 | include Athena::Console::Output::ConsoleOutputInterface 9 | 10 | # Sets the `ACON::Output::Interface` that represents `STDERR`. 11 | setter stderr : ACON::Output::Interface 12 | @console_section_outputs = Array(ACON::Output::Section).new 13 | 14 | def initialize( 15 | verbosity : ACON::Output::Verbosity = :normal, 16 | decorated : Bool? = nil, 17 | formatter : ACON::Formatter::Interface? = nil, 18 | ) 19 | super STDOUT, verbosity, decorated, formatter 20 | 21 | @stderr = ACON::Output::IO.new STDERR, verbosity, decorated, @formatter 22 | actual_decorated = self.decorated? 23 | 24 | if decorated.nil? 25 | self.decorated = actual_decorated && @stderr.decorated? 26 | end 27 | end 28 | 29 | # :inherit: 30 | def section : ACON::Output::Section 31 | ACON::Output::Section.new( 32 | self.io, 33 | @console_section_outputs, 34 | self.verbosity, 35 | self.decorated?, 36 | self.formatter 37 | ) 38 | end 39 | 40 | # :inherit: 41 | def error_output : ACON::Output::Interface 42 | @stderr 43 | end 44 | 45 | # :inherit: 46 | def error_output=(@stderr : ACON::Output::Interface) : Nil 47 | end 48 | 49 | # :inherit: 50 | def decorated=(decorated : Bool) : Nil 51 | super 52 | @stderr.decorated = decorated 53 | end 54 | 55 | # :inherit: 56 | def formatter=(formatter : Bool) : Nil 57 | super 58 | @stderr.formatter = formatter 59 | end 60 | 61 | # :inherit: 62 | def verbosity=(verbosity : ACON::Output::Verbosity) : Nil 63 | super 64 | @stderr.verbosity = verbosity 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /src/output/console_output_interface.cr: -------------------------------------------------------------------------------- 1 | require "./interface" 2 | 3 | # Extension of `ACON::Output::Interface` that adds additional functionality for terminal based outputs. 4 | module Athena::Console::Output::ConsoleOutputInterface 5 | # include Athena::Console::Output::Interface 6 | 7 | # Returns an `ACON::Output::Interface` that represents `STDERR`. 8 | abstract def error_output : ACON::Output::Interface 9 | 10 | # Sets the `ACON::Output::Interface` that represents `STDERR`. 11 | abstract def error_output=(stderr : ACON::Output::Interface) : Nil 12 | 13 | abstract def section : ACON::Output::Section 14 | end 15 | -------------------------------------------------------------------------------- /src/output/interface.cr: -------------------------------------------------------------------------------- 1 | # `Athena::Console` uses a dedicated interface for representing an output destination. 2 | # This allows it to have multiple more specialized implementations as opposed to 3 | # being tightly coupled to `STDOUT` or a raw [IO](https://crystal-lang.org/api/IO.html). 4 | # This interface represents the methods that _must_ be implemented, however implementations can add additional functionality. 5 | # 6 | # The most common implementations include `ACON::Output::ConsoleOutput` which is based on `STDOUT` and `STDERR`, 7 | # and `ACON::Output::Null` which can be used when you want to silent all output, such as for tests. 8 | # 9 | # Each output's `ACON::Output::Verbosity` and output `ACON::Output::Type` can also be configured on a per message basis. 10 | module Athena::Console::Output::Interface 11 | # Outputs the provided *message* followed by a new line. 12 | # The *verbosity* and/or *output_type* parameters can be used to control when and how the *message* is printed. 13 | abstract def puts(message : String | Enumerable(String), verbosity : ACON::Output::Verbosity = :normal, output_type : ACON::Output::Type = :normal) : Nil 14 | 15 | # Outputs the provided *message*. 16 | # The *verbosity* and/or *output_type* parameters can be used to control when and how the *message* is printed. 17 | abstract def print(message : String | Enumerable(String), verbosity : ACON::Output::Verbosity = :normal, output_type : ACON::Output::Type = :normal) : Nil 18 | 19 | # Returns the minimum `ACON::Output::Verbosity` required for a message to be printed. 20 | abstract def verbosity : ACON::Output::Verbosity 21 | 22 | # Set the minimum `ACON::Output::Verbosity` required for a message to be printed. 23 | abstract def verbosity=(verbosity : ACON::Output::Verbosity) : Nil 24 | 25 | # Returns `true` if printed messages should have their decorations applied. 26 | # I.e. `ACON::Formatter::OutputStyleInterface`. 27 | abstract def decorated? : Bool 28 | 29 | # Sets if printed messages should be *decorated*. 30 | abstract def decorated=(decorated : Bool) : Nil 31 | 32 | # Returns the `ACON::Formatter::Interface` used by `self`. 33 | abstract def formatter : ACON::Formatter::Interface 34 | 35 | # Sets the `ACON::Formatter::Interface` used by `self`. 36 | abstract def formatter=(formatter : ACON::Formatter::Interface) : Nil 37 | end 38 | -------------------------------------------------------------------------------- /src/output/io.cr: -------------------------------------------------------------------------------- 1 | # An `ACON::Output::Interface` implementation that wraps an [IO](https://crystal-lang.org/api/IO.html). 2 | class Athena::Console::Output::IO < Athena::Console::Output 3 | property io : ::IO 4 | 5 | delegate :to_s, to: @io 6 | 7 | def initialize( 8 | @io : ::IO, 9 | verbosity : ACON::Output::Verbosity? = :normal, 10 | decorated : Bool? = nil, 11 | formatter : ACON::Formatter::Interface? = nil, 12 | ) 13 | decorated = self.has_color_support? if decorated.nil? 14 | 15 | super verbosity, decorated, formatter 16 | end 17 | 18 | protected def do_write(message : String, new_line : Bool) : Nil 19 | message += EOL if new_line 20 | 21 | @io.print message 22 | end 23 | 24 | private def io_do_write(message : String, new_line : Bool) : Nil 25 | message += EOL if new_line 26 | 27 | @io.print message 28 | end 29 | 30 | private def has_color_support? : Bool 31 | # Respect https://no-color.org. 32 | return false if ENV["NO_COLOR"]?.presence 33 | 34 | # Respect https://force-color.org. 35 | return true if ENV["FORCE_COLOR"]?.presence 36 | 37 | if "Hyper" == ENV["TERM_PROGRAM"]? || 38 | ENV.has_key?("COLORTERM") || 39 | ENV.has_key?("ANSICON") || 40 | "ON" == ENV["ConEmuANSI"]? 41 | return true 42 | end 43 | 44 | return @io.tty? unless term = ENV["TERM"]? 45 | 46 | return false if "dumb" == term 47 | 48 | # See https://github.com/chalk/supports-color/blob/d4f413efaf8da045c5ab440ed418ef02dbb28bf1/index.js#L157 49 | term.matches? /^((screen|xterm|vt100|vt220|putty|rxvt|ansi|cygwin|linux).*)|(.*-256(color)?(-bce)?)$/ 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /src/output/null.cr: -------------------------------------------------------------------------------- 1 | require "./interface" 2 | 3 | # An `ACON::Output::Interface` that does not output anything, such as for tests. 4 | class Athena::Console::Output::Null 5 | include Athena::Console::Output::Interface 6 | 7 | @formatter : ACON::Formatter::Interface? = nil 8 | 9 | # :inherit: 10 | def puts(message : String | Enumerable(String), verbosity : ACON::Output::Verbosity = :normal, output_type : ACON::Output::Type = :normal) : Nil 11 | end 12 | 13 | # :inherit: 14 | def print(message : String | Enumerable(String), verbosity : ACON::Output::Verbosity = :normal, output_type : ACON::Output::Type = :normal) : Nil 15 | end 16 | 17 | def puts(message, verbosity : ACON::Output::Verbosity = :normal, output_type : ACON::Output::Type = :normal) : Nil 18 | end 19 | 20 | def print(message, verbosity : ACON::Output::Verbosity = :normal, output_type : ACON::Output::Type = :normal) : Nil 21 | end 22 | 23 | # :inherit: 24 | def verbosity : ACON::Output::Verbosity 25 | ACON::Output::Verbosity::SILENT 26 | end 27 | 28 | # :inherit: 29 | def verbosity=(verbosity : ACON::Output::Verbosity) : Nil 30 | end 31 | 32 | # :inherit: 33 | def decorated=(decorated : Bool) : Nil 34 | end 35 | 36 | # :inherit: 37 | def decorated? : Bool 38 | false 39 | end 40 | 41 | # :inherit: 42 | def formatter : ACON::Formatter::Interface 43 | @formatter ||= ACON::Formatter::Null.new 44 | end 45 | 46 | # :inherit: 47 | def formatter=(formatter : ACON::Formatter::Interface) : Nil 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /src/output/output.cr: -------------------------------------------------------------------------------- 1 | require "./interface" 2 | 3 | # Common base implementation of `ACON::Output::Interface`. 4 | abstract class Athena::Console::Output 5 | include Athena::Console::Output::Interface 6 | 7 | @formatter : ACON::Formatter::Interface 8 | @verbosity : ACON::Output::Verbosity 9 | 10 | def initialize( 11 | verbosity : ACON::Output::Verbosity? = :normal, 12 | decorated : Bool = false, 13 | formatter : ACON::Formatter::Interface? = nil, 14 | ) 15 | @verbosity = verbosity || ACON::Output::Verbosity::NORMAL 16 | @formatter = formatter || ACON::Formatter::Output.new 17 | @formatter.decorated = decorated 18 | end 19 | 20 | # :inherit: 21 | def verbosity : ACON::Output::Verbosity 22 | @verbosity 23 | end 24 | 25 | # :inherit: 26 | def verbosity=(@verbosity : ACON::Output::Verbosity) : Nil 27 | end 28 | 29 | # :inherit: 30 | def formatter : ACON::Formatter::Interface 31 | @formatter 32 | end 33 | 34 | # :inherit: 35 | def formatter=(@formatter : ACON::Formatter::Interface) : Nil 36 | end 37 | 38 | # :inherit: 39 | def decorated? : Bool 40 | @formatter.decorated? 41 | end 42 | 43 | # :inherit: 44 | def decorated=(decorated : Bool) : Nil 45 | @formatter.decorated = decorated 46 | end 47 | 48 | # :inherit: 49 | def puts(*messages : String) : Nil 50 | self.puts messages 51 | end 52 | 53 | # :inherit: 54 | def puts(message : String | Enumerable(String), verbosity : ACON::Output::Verbosity = :normal, output_type : ACON::Output::Type = :normal) : Nil 55 | self.write message, true, verbosity, output_type 56 | end 57 | 58 | # :inherit: 59 | def puts(message : _, verbosity : ACON::Output::Verbosity = :normal, output_type : ACON::Output::Type = :normal) : Nil 60 | self.puts message.to_s, verbosity, output_type 61 | end 62 | 63 | # :inherit: 64 | def print(*messages : String) : Nil 65 | self.print messages 66 | end 67 | 68 | # :inherit: 69 | def print(message : String | Enumerable(String), verbosity : ACON::Output::Verbosity = :normal, output_type : ACON::Output::Type = :normal) : Nil 70 | self.write message, false, verbosity, output_type 71 | end 72 | 73 | # :inherit: 74 | def print(message : _, verbosity : ACON::Output::Verbosity = :normal, output_type : ACON::Output::Type = :normal) : Nil 75 | self.print message.to_s, verbosity, output_type 76 | end 77 | 78 | protected def write( 79 | message : String | Enumerable(String), 80 | new_line : Bool, 81 | verbosity : ACON::Output::Verbosity, 82 | output_type : ACON::Output::Type, 83 | ) 84 | messages = message.is_a?(String) ? {message} : message 85 | 86 | return if verbosity > self.verbosity 87 | 88 | messages.each do |m| 89 | self.do_write( 90 | case output_type 91 | in .normal? then @formatter.format m 92 | in .plain? then @formatter.format(m).gsub(/(?:<\/?[^>]*>)|(?:[\n]?)/, "") # TODO: Use a more robust strip_tags implementation. 93 | in .raw? then m 94 | end, 95 | new_line 96 | ) 97 | end 98 | end 99 | 100 | protected abstract def do_write(message : String, new_line : Bool) : Nil 101 | end 102 | -------------------------------------------------------------------------------- /src/output/sized_buffer.cr: -------------------------------------------------------------------------------- 1 | # :nodoc: 2 | class Athena::Console::Output::SizedBuffer < Athena::Console::Output 3 | @buffer : String = "" 4 | @max_length : Int32 5 | 6 | def initialize( 7 | @max_length : Int32, 8 | verbosity : ACON::Output::Verbosity? = :normal, 9 | decorated : Bool = false, 10 | formatter : ACON::Formatter::Interface? = nil, 11 | ) 12 | if @max_length < 0 13 | raise ACON::Exception::InvalidArgument.new "'#{self.class}#max_length' must be a positive, got: '#{@max_length}'." 14 | end 15 | 16 | super verbosity, decorated, formatter 17 | end 18 | 19 | def fetch : String 20 | content = @buffer 21 | 22 | @buffer = "" 23 | 24 | content 25 | end 26 | 27 | protected def do_write(message : String, new_line : Bool) : Nil 28 | @buffer += message 29 | 30 | @buffer += EOL if new_line 31 | 32 | @buffer = @buffer.chars.last(@max_length).join 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /src/output/type.cr: -------------------------------------------------------------------------------- 1 | # Determines how a message should be printed. 2 | # 3 | # When you output a message via `ACON::Output::Interface#puts` or `ACON::Output::Interface#print`, they also provide a way to set the output type it should be printed: 4 | # 5 | # ``` 6 | # protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status 7 | # output.puts "Some Message", output_type: :raw 8 | # 9 | # ACON::Command::Status::SUCCESS 10 | # end 11 | # ``` 12 | enum Athena::Console::Output::Type 13 | # Normal output, with any styles applied to format the text. 14 | NORMAL 15 | 16 | # Output style tags as is without formatting the string. 17 | RAW 18 | 19 | # Strip any style tags and only output the actual text. 20 | PLAIN 21 | end 22 | -------------------------------------------------------------------------------- /src/output/verbosity.cr: -------------------------------------------------------------------------------- 1 | # Verbosity levels determine which messages will be displayed, essentially the same idea as [Log::Severity](https://crystal-lang.org/api/Log/Severity.html) but for console output. 2 | # 3 | # For example: 4 | # 5 | # ```sh 6 | # # Output nothing 7 | # ./console my-command --silent 8 | # 9 | # # Output only errors 10 | # ./console my-command -q 11 | # ./console my-command --quiet 12 | # 13 | # # Display only useful output 14 | # ./console my-command 15 | # 16 | # # Increase the verbosity of messages 17 | # ./console my-command -v 18 | # 19 | # # Also display non-essential information 20 | # ./console my-command -vv 21 | # 22 | # # Display all messages, such as for debugging 23 | # ./console my-command -vvv 24 | # ``` 25 | # 26 | # As used in the previous example, the verbosity can be controlled on a command invocation basis using a CLI option, 27 | # but may also be globally set via the `SHELL_VERBOSITY` environmental variable. 28 | # 29 | # When you output a message via `ACON::Output::Interface#puts` or `ACON::Output::Interface#print`, they also provide a way to set the verbosity at which that message should print: 30 | # 31 | # ``` 32 | # protected def execute(input : ACON::Input::Interface, output : ACON::Output::Interface) : ACON::Command::Status 33 | # # Via conditional logic 34 | # if output.verbosity.verbose? 35 | # output.puts "Obj class: #{obj.class}" 36 | # end 37 | # 38 | # # Inline within the method 39 | # output.puts "Only print this in verbose mode or higher", verbosity: :verbose 40 | # 41 | # ACON::Command::Status::SUCCESS 42 | # end 43 | # ``` 44 | # 45 | # TIP: The full stack trace of an exception is printed in `ACON::Output::Verbosity::VERBOSE` mode or higher. 46 | enum Athena::Console::Output::Verbosity 47 | # Silences all output. 48 | # Equivalent to `--silent` CLI option or `SHELL_VERBOSITY=-2`. 49 | SILENT = -2 50 | 51 | # Output only errors. 52 | # Equivalent to `-q`, `--quiet` CLI options or `SHELL_VERBOSITY=-1`. 53 | QUIET = -1 54 | 55 | # Normal behavior, display only useful messages. 56 | # Equivalent not providing any CLI options or `SHELL_VERBOSITY=0`. 57 | NORMAL = 0 58 | 59 | # Increase the verbosity of messages. 60 | # Equivalent to `-v`, `--verbose=1` CLI options or `SHELL_VERBOSITY=1`. 61 | VERBOSE = 1 62 | 63 | # Display all the informative non-essential messages. 64 | # Equivalent to `-vv`, `--verbose=2` CLI options or `SHELL_VERBOSITY=2`. 65 | VERY_VERBOSE = 2 66 | 67 | # Display all messages, such as for debugging. 68 | # Equivalent to `-vvv`, `--verbose=3` CLI options or `SHELL_VERBOSITY=3`. 69 | DEBUG = 3 70 | end 71 | -------------------------------------------------------------------------------- /src/question/abstract_choice.cr: -------------------------------------------------------------------------------- 1 | class Athena::Console::Question(T); end 2 | 3 | require "./base" 4 | 5 | # Base type of choice based questions. 6 | # See each subclass for more information. 7 | abstract class Athena::Console::Question::AbstractChoice(T, ChoiceType) 8 | include Athena::Console::Question::Base(T?) 9 | 10 | # Returns the possible choices. 11 | getter choices : Hash(String | Int32, T) 12 | 13 | # Returns the message to display if the provided answer is not a valid choice. 14 | getter error_message : String = "Value '%s' is invalid." 15 | 16 | # Returns/sets the prompt to use for the question. 17 | # The prompt being the character(s) before the user input. 18 | property prompt : String = " > " 19 | 20 | # See [Validating the Answer][Athena::Console::Question--validating-the-answer]. 21 | property validator : Proc(T?, ChoiceType)? = nil 22 | 23 | def self.new(question : String, choices : Indexable(T), default : Int | T | Nil = nil) 24 | choices_hash = Hash(String | Int32, T).new 25 | 26 | choices.each_with_index do |choice, idx| 27 | choices_hash[idx] = choice 28 | end 29 | 30 | new question, choices_hash, (default.is_a?(Int) ? choices[default]? : default) 31 | end 32 | 33 | def initialize(question : String, choices : Hash(String | Int32, T), default : T? = nil) 34 | super question, default 35 | 36 | raise ACON::Exception::Logic.new "Choice questions must have at least 1 choice available." if choices.empty? 37 | 38 | @choices = choices.transform_keys &.as String | Int32 39 | 40 | self.validator = ->default_validator(T?) 41 | self.autocompleter_values = choices 42 | end 43 | 44 | def error_message=(@error_message : String) : self 45 | self.validator = ->default_validator(T?) 46 | 47 | self 48 | end 49 | 50 | # Sets the validator callback to the provided block. 51 | # See [Validating the Answer][Athena::Console::Question--validating-the-answer]. 52 | def validator(&@validator : T? -> ChoiceType) : Nil 53 | end 54 | 55 | private def selected_choices(answer : String?) : Array(T) 56 | selected_choices = self.parse_answers answer 57 | 58 | if @trimmable 59 | selected_choices.map! &.strip 60 | end 61 | 62 | valid_choices = [] of String 63 | selected_choices.each do |value| 64 | results = [] of String 65 | 66 | @choices.each do |key, choice| 67 | results << key.to_s if choice == value 68 | end 69 | 70 | raise ACON::Exception::InvalidArgument.new %(The provided answer is ambiguous. Value should be one of #{results.join(" or ") { |i| "'#{i}'" }}.) if results.size > 1 71 | 72 | result = @choices.find { |(k, v)| v == value || k.to_s == value }.try &.first.to_s 73 | 74 | # If none of the keys are a string, assume the original choice values were an Indexable. 75 | if @choices.keys.none?(String) && result 76 | result = @choices[result.to_i] 77 | elsif @choices.has_key? value 78 | result = @choices[value] 79 | elsif @choices.has_key? result 80 | result = @choices[result] 81 | end 82 | 83 | if result.nil? 84 | raise ACON::Exception::InvalidArgument.new sprintf(@error_message, value) 85 | end 86 | 87 | valid_choices << result 88 | end 89 | 90 | valid_choices 91 | end 92 | 93 | protected abstract def default_validator(answer : T?) : ChoiceType 94 | protected abstract def parse_answers(answer : T?) : Array(String) 95 | end 96 | -------------------------------------------------------------------------------- /src/question/choice.cr: -------------------------------------------------------------------------------- 1 | require "./abstract_choice" 2 | 3 | # A question whose answer _MUST_ be within a set of predefined answers. 4 | # If the user enters an invalid answer, an error is displayed and they are prompted again. 5 | # 6 | # ``` 7 | # question = ACON::Question::Choice.new "What is your favorite color?", {"red", "blue", "green"} 8 | # color = helper.ask input, output, question 9 | # ``` 10 | # 11 | # This would display something like the following: 12 | # 13 | # ```sh 14 | # What is your favorite color? 15 | # [0] red 16 | # [1] blue 17 | # [2] green 18 | # > 19 | # ``` 20 | # 21 | # The user would be able to enter the name of the color, or the index associated with it. E.g. `blue` or `2` for `green`. 22 | # If a `Hash` is used as the choices, the key of each choice is used instead of its index. 23 | # 24 | # Similar to `ACON::Question`, the third argument can be set to set the default choice. 25 | # This value can also either be the actual value, or the index/key of the related choice. 26 | # 27 | # ``` 28 | # question = ACON::Question::Choice.new "What is your favorite color?", {"c1" => "red", "c2" => "blue", "c3" => "green"}, "c2" 29 | # color = helper.ask input, output, question 30 | # ``` 31 | # 32 | # Which would display something like : 33 | # 34 | # ```sh 35 | # What is your favorite color? 36 | # [c1] red 37 | # [c2] blue 38 | # [c3] green 39 | # > 40 | # ``` 41 | class Athena::Console::Question::Choice(T) < Athena::Console::Question::AbstractChoice(T, T?) 42 | protected def default_validator(answer : T?) : T? 43 | self.selected_choices(answer).first? 44 | end 45 | 46 | protected def parse_answers(answer : T?) : Array(String) 47 | [answer || ""] 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /src/question/confirmation.cr: -------------------------------------------------------------------------------- 1 | # Allows prompting the user to confirm an action. 2 | # 3 | # ``` 4 | # question = ACON::Question::Confirmation.new "Continue with this action?", false 5 | # 6 | # if !helper.ask input, output, question 7 | # return ACON::Command::Status::SUCCESS 8 | # end 9 | # 10 | # # ... 11 | # ``` 12 | # 13 | # In this example the user will be asked if they wish to `Continue with this action`. 14 | # The `#ask` method will return `true` if the user enters anything starting with `y`, otherwise `false`. 15 | class Athena::Console::Question::Confirmation < Athena::Console::Question(Bool) 16 | @true_answer_regex : Regex 17 | 18 | # Creates a new instance of self with the provided *question* string. 19 | # The *default* parameter represents the value to return if no valid input was entered. 20 | # The *true_answer_regex* can be used to customize the pattern used to determine if the user's input evaluates to `true`. 21 | def initialize(question : String, default : Bool = true, @true_answer_regex : Regex = /^y/i) 22 | super question, default 23 | 24 | self.normalizer = ->default_normalizer(String | Bool) 25 | end 26 | 27 | private def default_normalizer(answer : String | Bool) : Bool 28 | if answer.is_a? Bool 29 | return answer 30 | end 31 | 32 | answer_is_true = answer.matches? @true_answer_regex 33 | 34 | if false == @default 35 | return !answer.blank? && answer_is_true 36 | end 37 | 38 | answer.empty? || answer_is_true 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /src/question/multiple_choice.cr: -------------------------------------------------------------------------------- 1 | require "./abstract_choice" 2 | 3 | # Similar to `ACON::Question::Choice`, but allows for more than one answer to be selected. 4 | # This question accepts a comma separated list of answers. 5 | # 6 | # ``` 7 | # question = ACON::Question::MultipleChoice.new "What is your favorite color?", {"red", "blue", "green"} 8 | # answer = helper.ask input, output, question 9 | # ``` 10 | # 11 | # This question is also similar to `ACON::Question::Choice` in that you can provide either the index, key, or value of the choice. 12 | # For example submitting `green,0` would result in `["green", "red"]` as the value of `answer`. 13 | class Athena::Console::Question::MultipleChoice(T) < Athena::Console::Question::AbstractChoice(T, Array(T)) 14 | protected def default_validator(answer : T?) : Array(T) 15 | self.selected_choices answer 16 | end 17 | 18 | protected def parse_answers(answer : T?) : Array(String) 19 | answer.try(&.split(',')) || [""] 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /src/spec/expectations/command_is_successful.cr: -------------------------------------------------------------------------------- 1 | # :nodoc: 2 | struct Athena::Console::Spec::Expectations::CommandIsSuccessful 3 | def match(actual_value : ::ACON::Command::Status?) : Bool 4 | ACON::Command::Status::SUCCESS == actual_value 5 | end 6 | 7 | def failure_message(actual_value : ::ACON::Command::Status?) : String 8 | "The command was unsuccessful" 9 | end 10 | 11 | def negative_failure_message(actual_value : ::ACON::Command::Status?) : String 12 | "The command was unsuccessful" 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /src/style/interface.cr: -------------------------------------------------------------------------------- 1 | # Represents a "style" that provides a way to abstract _how_ to format console input/output data 2 | # such that you can reduce the amount of code needed, and to standardize the appearance. 3 | # 4 | # See `ACON::Style::Athena`. 5 | # 6 | # ## Custom Styles 7 | # 8 | # Custom styles can also be created by implementing this interface, and optionally extending from `ACON::Style::Output` 9 | # which makes the style an `ACON::Output::Interface` as well as defining part of this interface. 10 | # Then you could simply instantiate your custom style within a command as you would `ACON::Style::Athena`. 11 | module Athena::Console::Style::Interface 12 | # Helper method for asking `ACON::Question` questions. 13 | abstract def ask(question : String, default : _) 14 | 15 | # Helper method for asking hidden `ACON::Question` questions. 16 | abstract def ask_hidden(question : String) 17 | 18 | # Formats and prints the provided *messages* within a caution block. 19 | abstract def caution(messages : String | Enumerable(String)) : Nil 20 | 21 | # Formats and prints the provided *messages* within a comment block. 22 | abstract def comment(messages : String | Enumerable(String)) : Nil 23 | 24 | # Helper method for asking `ACON::Question::Confirmation` questions. 25 | abstract def confirm(question : String, default : Bool = true) : Bool 26 | 27 | # Formats and prints the provided *messages* within a error block. 28 | abstract def error(messages : String | Enumerable(String)) : Nil 29 | 30 | # Helper method for asking `ACON::Question::Choice` questions. 31 | abstract def choice(question : String, choices : Indexable | Hash, default = nil) 32 | 33 | # Formats and prints the provided *messages* within a info block. 34 | abstract def info(messages : String | Enumerable(String)) : Nil 35 | 36 | # Formats and prints a bulleted list containing the provided *elements*. 37 | abstract def listing(elements : Enumerable) : Nil 38 | 39 | # Prints *count* empty new lines. 40 | abstract def new_line(count : Int32 = 1) : Nil 41 | 42 | # Formats and prints the provided *messages* within a note block. 43 | abstract def note(messages : String | Enumerable(String)) : Nil 44 | 45 | # Creates a section header with the provided *message*. 46 | abstract def section(message : String) : Nil 47 | 48 | # Formats and prints the provided *messages* within a success block. 49 | abstract def success(messages : String | Enumerable(String)) : Nil 50 | 51 | # Formats and prints the provided *messages* as text. 52 | abstract def text(messages : String | Enumerable(String)) : Nil 53 | 54 | # Formats and prints *message* as a title. 55 | abstract def title(message : String) : Nil 56 | 57 | # Formats and prints a table based on the provided *headers* and *rows*, followed by a new line. 58 | abstract def table(headers : Enumerable, rows : Enumerable) : Nil 59 | 60 | # Starts an internal `ACON::Helper::ProgressBar`, optionally with the provided *max* amount of steps. 61 | abstract def progress_start(max : Int32? = nil) : Nil 62 | 63 | # Advances the internal `ACON::Helper::ProgressBar` *by* the provided amount of steps. 64 | abstract def progress_advance(by step : Int32 = 1) : Nil 65 | 66 | # Completes the internal `ACON::Helper::ProgressBar`. 67 | abstract def progress_finish : Nil 68 | 69 | # Formats and prints the provided *messages* within a warning block. 70 | abstract def warning(messages : String | Enumerable(String)) : Nil 71 | end 72 | -------------------------------------------------------------------------------- /src/style/output.cr: -------------------------------------------------------------------------------- 1 | require "./interface" 2 | 3 | # Base implementation of `ACON::Style::Interface` and `ACON::Output::Interface` that provides logic common to all styles. 4 | abstract class Athena::Console::Style::Output 5 | include Athena::Console::Style::Interface 6 | include Athena::Console::Output::Interface 7 | 8 | @output : ACON::Output::Interface 9 | 10 | def initialize(@output : ACON::Output::Interface); end 11 | 12 | # See `ACON::Output::Interface#decorated?`. 13 | def decorated? : Bool 14 | @output.decorated? 15 | end 16 | 17 | # See `ACON::Output::Interface#decorated=`. 18 | def decorated=(decorated : Bool) : Nil 19 | @output.decorated = decorated 20 | end 21 | 22 | # See `ACON::Output::Interface#formatter`. 23 | def formatter : ACON::Formatter::Interface 24 | @output.formatter 25 | end 26 | 27 | # See `ACON::Output::Interface#formatter=`. 28 | def formatter=(formatter : ACON::Formatter::Interface) : Nil 29 | @output.formatter = formatter 30 | end 31 | 32 | # :inherit: 33 | def new_line(count : Int32 = 1) : Nil 34 | @output.print EOL * count 35 | end 36 | 37 | # Creates and returns an `ACON::Helper::ProgressBar`, optionally with the provided *max* amount of steps. 38 | def create_progress_bar(max : Int32? = nil) : ACON::Helper::ProgressBar 39 | ACON::Helper::ProgressBar.new @output, max 40 | end 41 | 42 | # See `ACON::Output::Interface#puts`. 43 | def puts(message, verbosity : ACON::Output::Verbosity = :normal, output_type : ACON::Output::Type = :normal) : Nil 44 | @output.puts message, verbosity, output_type 45 | end 46 | 47 | # See `ACON::Output::Interface#print`. 48 | def print(message, verbosity : ACON::Output::Verbosity = :normal, output_type : ACON::Output::Type = :normal) : Nil 49 | @output.print message, verbosity, output_type 50 | end 51 | 52 | # See `ACON::Output::Interface#verbosity`. 53 | def verbosity : ACON::Output::Verbosity 54 | @output.verbosity 55 | end 56 | 57 | # See `ACON::Output::Interface#verbosity=`. 58 | def verbosity=(verbosity : ACON::Output::Verbosity) : Nil 59 | @output.verbosity = verbosity 60 | end 61 | 62 | protected def error_output : ACON::Output::Interface 63 | unless (output = @output).is_a? ACON::Output::ConsoleOutputInterface 64 | return @output 65 | end 66 | 67 | output.error_output 68 | end 69 | end 70 | --------------------------------------------------------------------------------