dry-cli-help 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ class CLI
5
+ module Help
6
+ module Screens
7
+ # The help printed for a registry level: `mycli`, `mycli -h`, and a group
8
+ # such as `mycli db` whose node has no command of its own.
9
+ class Listing < Base
10
+ SECTIONS = %i[banner usage commands options epilogue].freeze
11
+
12
+ # @param result [Dry::CLI::CommandRegistry::LookupResult] the level to list
13
+ def initialize(result:, **)
14
+ super(**)
15
+ @result = result
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :result
21
+
22
+ def top_level?
23
+ result.names.empty?
24
+ end
25
+
26
+ def banner?
27
+ top_level? || config.banner_on_subcommands
28
+ end
29
+
30
+ def epilogue?
31
+ top_level?
32
+ end
33
+
34
+ def render_usage
35
+ program = format.paint(ProgramName.call(result.names), :command)
36
+ section(:usage, ["#{INDENT}#{program} COMMAND [OPTIONS]"])
37
+ end
38
+
39
+ def render_commands
40
+ blocks = []
41
+ ungrouped = command_rows.except(*grouped).values
42
+ blocks << section(:commands, definitions(ungrouped)) if ungrouped.any?
43
+
44
+ config.groups.each do |name, paths|
45
+ rows = paths.filter_map { command_rows[it] }
46
+ blocks << [format.title(name), *definitions(rows)] if rows.any?
47
+ end
48
+
49
+ blocks.inject { |above, below| above + [""] + below }
50
+ end
51
+
52
+ def render_options
53
+ section(:options, definitions([HELP, *option_rows]))
54
+ end
55
+
56
+ def aligned_rows
57
+ command_rows.values + option_rows
58
+ end
59
+
60
+ def grouped
61
+ @grouped ||= config.groups.flat_map(&:last)
62
+ end
63
+
64
+ # Commands keyed by full path, so a group can name "db migrate".
65
+ def command_rows
66
+ @command_rows ||= entries.each_with_object({}) do |(name, node, aliases), rows|
67
+ next if name.start_with?("-")
68
+
69
+ term = [name, *aliases.reject { it.start_with?("-") }].join(", ")
70
+ rows[[*result.names, name].join(" ")] = Row.new(term:, text: describe_node(node))
71
+ end
72
+ end
73
+
74
+ # A command reachable as `--version` or `-v` reads as an option, so it
75
+ # lists under Options by its dashed names.
76
+ def option_rows
77
+ @option_rows ||= entries.filter_map do |name, node, aliases|
78
+ dashed = [name, *aliases].select { it.start_with?("-") }
79
+ next if dashed.empty?
80
+
81
+ term = dashed.sort_by { [it.length, it] }.join(", ")
82
+ Row.new(term:, text: describe_node(node), term_style: :option)
83
+ end
84
+ end
85
+
86
+ def entries
87
+ @entries ||= visible(result.children).map do |name, node|
88
+ [name, node, aliases_of(node)]
89
+ end
90
+ end
91
+
92
+ # dry-cli files an alias on the parent node, pointing at the child.
93
+ def aliases_of(node)
94
+ node.parent.aliases.filter_map { |name, target| name if target.equal?(node) }
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "io/console"
4
+
5
+ module Dry
6
+ class CLI
7
+ module Help
8
+ # The width of the terminal help prints to.
9
+ module Terminal
10
+ FALLBACK_WIDTH = 80
11
+
12
+ module_function
13
+
14
+ # `COLUMNS` wins over the console, so a user or a test can pin the width.
15
+ #
16
+ # @param env [Hash, ENV]
17
+ # @param console [IO, nil] the controlling terminal, nil without one
18
+ # @return [Integer]
19
+ def width(env: ENV, console: IO.console)
20
+ from_env(env) || from_console(console) || FALLBACK_WIDTH
21
+ end
22
+
23
+ # @return [Integer, nil]
24
+ def from_env(env)
25
+ columns = Integer(env.fetch("COLUMNS", ""), exception: false)
26
+ columns if columns&.positive?
27
+ end
28
+
29
+ # @return [Integer, nil]
30
+ def from_console(console)
31
+ columns = console&.winsize&.last
32
+ columns if columns&.positive?
33
+ rescue SystemCallError
34
+ nil
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ class CLI
5
+ module Help
6
+ # Word wrapping for descriptions, epilogues and anything else a user writes.
7
+ module Text
8
+ module_function
9
+
10
+ # Split text into lines no wider than `width`.
11
+ #
12
+ # Paragraphs split on blank lines and reflow, so a heredoc wraps to the
13
+ # terminal instead of keeping the line breaks of the editor it was
14
+ # written in. A line starting with whitespace prints verbatim, which
15
+ # keeps indented lists and code intact. Consecutive blank lines collapse
16
+ # to one. A word longer than `width` gets a line to itself.
17
+ #
18
+ # @param text [String, nil]
19
+ # @param width [Integer, nil] nil keeps every line as written
20
+ # @return [Array<String>] lines, with "" between paragraphs
21
+ def lines(text, width)
22
+ source = text.to_s.sub(/\A(?:[ \t]*\n)+/, "").rstrip
23
+ return source.lines(chomp: true) if width.nil?
24
+
25
+ reflow(source, width)
26
+ end
27
+
28
+ # @return [Array<String>]
29
+ def reflow(source, width)
30
+ output = []
31
+ paragraph = []
32
+ flush = lambda do
33
+ output.concat(fill(paragraph.join(" "), width)) unless paragraph.empty?
34
+ paragraph.clear
35
+ end
36
+
37
+ source.each_line(chomp: true) do |line|
38
+ if line.strip.empty?
39
+ flush.call
40
+ output << "" unless output.last == ""
41
+ elsif line.start_with?(" ", "\t")
42
+ flush.call
43
+ output << line.rstrip
44
+ else
45
+ paragraph << line.strip
46
+ end
47
+ end
48
+ flush.call
49
+ output
50
+ end
51
+
52
+ # Greedy fill of one paragraph.
53
+ # @return [Array<String>]
54
+ def fill(text, width)
55
+ text.split.each_with_object([]) do |word, lines|
56
+ if lines.empty? || lines.last.length + 1 + word.length > width
57
+ lines << word.dup
58
+ else
59
+ lines.last << " " << word
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ # Reopened rather than defined: dry-cli declares `class CLI`, not a module,
5
+ # and getting that wrong raises TypeError the moment both are loaded.
6
+ #
7
+ # Declared here without requiring dry-cli, because the gemspec loads this
8
+ # file at build time when the dependency may not be installed. dry-cli's CLI
9
+ # inherits from Object, so an empty reopening is compatible either way.
10
+ class CLI
11
+ module Help
12
+ VERSION = "0.1.0"
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/cli"
4
+ require "pastel"
5
+
6
+ require_relative "help/version"
7
+ require_relative "help/colors"
8
+ require_relative "help/terminal"
9
+ require_relative "help/text"
10
+ require_relative "help/configuration"
11
+ require_relative "help/formatter"
12
+ require_relative "help/screens/base"
13
+ require_relative "help/screens/listing"
14
+ require_relative "help/screens/command"
15
+ require_relative "help/integration"
16
+
17
+ module Dry
18
+ class CLI
19
+ # Configurable, wrapped and colored help screens for dry-cli.
20
+ #
21
+ # Requiring this file changes the help every Dry::CLI in the process prints.
22
+ # Settings come from {.configure} for the whole process, and from a
23
+ # registry's `help` block for that registry.
24
+ module Help
25
+ class << self
26
+ # @yieldparam config [Configuration] the process-wide settings
27
+ # @return [Configuration]
28
+ def configure
29
+ yield config
30
+ config
31
+ end
32
+
33
+ # @return [Configuration] the process-wide settings
34
+ def config
35
+ @config ||= Configuration.new
36
+ end
37
+
38
+ # Forget every process-wide setting.
39
+ # @return [void]
40
+ def reset!
41
+ @config = nil
42
+ end
43
+
44
+ # The settings a registry renders with: its own `help` block over the
45
+ # process-wide settings. A single command passed to Dry::CLI.new has no
46
+ # `help` block, so it renders with the process-wide settings alone.
47
+ #
48
+ # @param registry [Module, Class, Dry::CLI::Command, nil]
49
+ # @return [Configuration]
50
+ def config_for(registry)
51
+ own = registry.help_config if registry.respond_to?(:help_config)
52
+ own ? config.merge(own) : config
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ Dry::CLI.prepend(Dry::CLI::Help::Integration::CLIMethods)
60
+ Dry::CLI::Registry.include(Dry::CLI::Help::Integration::RegistryMethods)
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "dry/cli/help"
@@ -0,0 +1,88 @@
1
+ module Dry
2
+ class CLI
3
+ module Help
4
+ VERSION: String
5
+
6
+ def self.configure: () { (Configuration) -> void } -> Configuration
7
+ def self.config: () -> Configuration
8
+ def self.reset!: () -> void
9
+ def self.config_for: (untyped registry) -> Configuration
10
+
11
+ module Colors
12
+ STYLES: Array[Symbol]
13
+ SETTINGS: Array[bool | Symbol]
14
+
15
+ def self.enabled: () -> (bool | Symbol)
16
+ def self.enabled=: (bool | Symbol) -> void
17
+ def self.pastel: () -> untyped
18
+ def self.enabled_for?: (bool | Symbol setting, untyped io) -> bool
19
+
20
+ def pastel: () -> untyped
21
+ end
22
+
23
+ module Terminal
24
+ FALLBACK_WIDTH: Integer
25
+
26
+ def self.width: (?env: Hash[String, String], ?console: untyped) -> Integer
27
+ end
28
+
29
+ module Text
30
+ def self.lines: (String? text, Integer? width) -> Array[String]
31
+ end
32
+
33
+ class Configuration
34
+ SECTIONS: Array[Symbol]
35
+ HEADINGS: Hash[Symbol, String]
36
+ STYLES: Hash[Symbol, Array[Symbol]]
37
+ DEFAULTS: Hash[Symbol, untyped]
38
+ MIN_WIDTH: Integer
39
+
40
+ def initialize: (?Hash[Symbol, untyped] values) -> void
41
+
42
+ def title: (?String? value) -> String?
43
+ def title=: (String?) -> void
44
+ def description: (?String? value) -> String?
45
+ def description=: (String?) -> void
46
+ def epilogue: (?String? value) -> String?
47
+ def epilogue=: (String?) -> void
48
+ def color: (?(bool | Symbol) value) -> (bool | Symbol)
49
+ def color=: (bool | Symbol) -> void
50
+ def wrap: (?bool value) -> bool
51
+ def wrap=: (bool) -> void
52
+ def width: (?(Integer | Symbol) value) -> (Integer | Symbol)
53
+ def width=: (Integer | Symbol) -> void
54
+ def margin: (?Integer value) -> Integer
55
+ def margin=: (Integer) -> void
56
+ def exit_code_without_arguments: (?Integer value) -> Integer
57
+ def exit_code_without_arguments=: (Integer) -> void
58
+ def banner_on_subcommands: (?bool value) -> bool
59
+ def banner_on_subcommands=: (bool) -> void
60
+ def heading_case: (?Symbol value) -> Symbol
61
+ def heading_case=: (Symbol) -> void
62
+ def command_order: (?Symbol value) -> Symbol
63
+ def command_order=: (Symbol) -> void
64
+
65
+ def heading: (Symbol section, String text) -> String
66
+ def headings: () -> Hash[Symbol, String]
67
+ def style: (Symbol element, *Symbol names) -> Array[Symbol]
68
+ def styles: () -> Hash[Symbol, Array[Symbol]]
69
+ def group: (String name, *(String | Array[String]) commands) -> Array[[String, Array[String]]]
70
+ def groups: () -> Array[[String, Array[String]]]
71
+ def sections: (*Symbol names) -> Array[Symbol]
72
+ def sections=: (Array[Symbol]) -> void
73
+ def hide: (*Symbol names) -> Array[Symbol]
74
+ def hidden: () -> Array[Symbol]
75
+ def visible_sections: () -> Array[Symbol]
76
+ def wrap_width: (Integer terminal_width) -> Integer?
77
+ def merge: (Configuration other) -> Configuration
78
+ end
79
+
80
+ module Integration
81
+ module RegistryMethods
82
+ def help: () ?{ (Configuration) -> void } -> Configuration
83
+ def help_config: () -> Configuration?
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
metadata ADDED
@@ -0,0 +1,99 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dry-cli-help
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Konstantin Gredeskoul
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: dry-cli
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 1.1.1
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '2'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: 1.1.1
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '2'
32
+ - !ruby/object:Gem::Dependency
33
+ name: pastel
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - "~>"
37
+ - !ruby/object:Gem::Version
38
+ version: '0.8'
39
+ type: :runtime
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - "~>"
44
+ - !ruby/object:Gem::Version
45
+ version: '0.8'
46
+ description: Adds a title, description, epilogue, command groups, section ordering,
47
+ terminal-width wrapping and ANSI colors to the help dry-cli prints, configured through
48
+ a help block on the registry.
49
+ email:
50
+ - kigster@gmail.com
51
+ executables: []
52
+ extensions: []
53
+ extra_rdoc_files: []
54
+ files:
55
+ - CHANGELOG.md
56
+ - LICENSE.txt
57
+ - README.md
58
+ - Rakefile
59
+ - SPECIFICATION.md
60
+ - lib/dry-cli-help.rb
61
+ - lib/dry/cli/help.rb
62
+ - lib/dry/cli/help/colors.rb
63
+ - lib/dry/cli/help/configuration.rb
64
+ - lib/dry/cli/help/formatter.rb
65
+ - lib/dry/cli/help/integration.rb
66
+ - lib/dry/cli/help/screens/base.rb
67
+ - lib/dry/cli/help/screens/command.rb
68
+ - lib/dry/cli/help/screens/listing.rb
69
+ - lib/dry/cli/help/terminal.rb
70
+ - lib/dry/cli/help/text.rb
71
+ - lib/dry/cli/help/version.rb
72
+ - sig/dry/cli/help.rbs
73
+ homepage: https://github.com/kigster/dry-cli-help
74
+ licenses:
75
+ - MIT
76
+ metadata:
77
+ allowed_push_host: https://rubygems.org
78
+ homepage_uri: https://github.com/kigster/dry-cli-help
79
+ source_code_uri: https://github.com/kigster/dry-cli-help
80
+ changelog_uri: https://github.com/kigster/dry-cli-help/blob/main/CHANGELOG.md
81
+ rubygems_mfa_required: 'true'
82
+ rdoc_options: []
83
+ require_paths:
84
+ - lib
85
+ required_ruby_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '4.0'
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ requirements: []
96
+ rubygems_version: 4.0.20
97
+ specification_version: 4
98
+ summary: Configurable, wrapped, colored help screens for dry-cli applications
99
+ test_files: []