From 6d80b99e2496bde65fc68b940801c92b42196021 Mon Sep 17 00:00:00 2001 From: Ragmaanir Date: Tue, 30 Jan 2024 20:41:21 +0100 Subject: [PATCH] Use Colorize::ColorRGB, show help and highlighted error when namespace/command not found --- spec/interaction_spec.cr | 8 +++- spec/namespace_spec.cr | 18 ++++----- src/example.cr | 6 ++- src/kommando.cr | 1 + src/kommando/colors.cr | 14 +++++++ src/kommando/command.cr | 26 +++++++------ src/kommando/docker.cr | 4 +- src/kommando/interaction.cr | 73 ++++++++++++++++++++++++++----------- src/kommando/namespace.cr | 11 ++++-- 9 files changed, 109 insertions(+), 52 deletions(-) create mode 100644 src/kommando/colors.cr diff --git a/spec/interaction_spec.cr b/spec/interaction_spec.cr index 509466d..27d3a40 100644 --- a/spec/interaction_spec.cr +++ b/spec/interaction_spec.cr @@ -51,9 +51,12 @@ describe Kommando::Interaction::Session do def write(slice : Bytes) : Nil # STDOUT.puts ".write(#{String.new(slice).inspect})" raise "Expected :write, got #{current_action[0]}" if current_action[0] != :write + # raise "Invalid length: #{current_action[1].inspect} <=> #{String.new(slice).inspect}" if current_action[1].size != slice.size slice.size.times { |i| - raise "Unexpected output: #{String.new(slice).inspect} != #{current_action[1].inspect}" if slice[i] != current_action[1].byte_at(@string_idx) + if slice[i] != current_action[1].byte_at(@string_idx) + raise "Unexpected output: #{String.new(slice).inspect} != #{current_action[1].inspect}" + end @string_idx += 1 } advance if current_action[1].size == @string_idx @@ -61,7 +64,7 @@ describe Kommando::Interaction::Session do end def session(io, colorize = false) - Kommando::Interaction::Session.define(io, colorize) do |s| + Kommando::Interaction::Session.define(io, io, colorize) do |s| with s yield(s) end end @@ -146,6 +149,7 @@ describe Kommando::Interaction::Session do test "confirm" do io = CannedIO.build do write "Want to exit?\n" + write "[y, yes, n, no]\n" write "> " read "n\n" end diff --git a/spec/namespace_spec.cr b/spec/namespace_spec.cr index e8921f8..de17e01 100644 --- a/spec/namespace_spec.cr +++ b/spec/namespace_spec.cr @@ -91,11 +91,11 @@ describe Kommando::Namespace do assert run_and_capture(["help"]) == <<-STDOUT Commands: - \e[94m info \e[0m\e[90mPrints information\e[0m + \e[38;2;90;90;250m info \e[0m\e[38;2;100;100;100mPrints information\e[0m Namespaces: - \e[94mdb\e[0m + \e[38;2;90;90;250mdb\e[0m \n STDOUT end @@ -104,24 +104,24 @@ describe Kommando::Namespace do assert run_and_capture(["db", "help"]) == <<-STDOUT Commands: - \e[94m create \e[0m\e[90mCreate the database\e[0m - \e[94m migrate \e[0m\e[90mRun pending migrations\e[0m + \e[38;2;90;90;250m create \e[0m\e[38;2;100;100;100mCreate the database\e[0m + \e[38;2;90;90;250m migrate \e[0m\e[38;2;100;100;100mRun pending migrations\e[0m \n STDOUT end test "command help" do assert run_and_capture(["help", "info"]) == <<-STDOUT - \e[33minfo\e[0m: \e[90mPrints information\e[0m + \e[38;2;220;220;0minfo\e[0m: \e[38;2;100;100;100mPrints information\e[0m - Usage: \e[33minfo\e[0m \e[94mversion\e[0m \e[90m-option=value\e[0m + Usage: \e[38;2;220;220;0minfo\e[0m \e[38;2;90;90;250mversion\e[0m \e[38;2;100;100;100m-option=value\e[0m Positional: - \e[94m version \e[0m : \e[35mInt32 \e[0m + \e[38;2;90;90;250m version \e[0m : \e[38;2;205;0;205mInt32 \e[0m Options: - \e[94m dry \e[0m\e[36m-d\e[0m : \e[35mBool \e[0m \e[90mSimulate migration\e[0m - \e[94m verbose \e[0m\e[36m-v\e[0m : \e[35mBool \e[0m \e[90mMore detailed output\e[0m + \e[38;2;90;90;250m dry \e[0m\e[38;2;0;205;205m-d\e[0m : \e[38;2;205;0;205mBool \e[0m \e[38;2;100;100;100mSimulate migration\e[0m + \e[38;2;90;90;250m verbose \e[0m\e[38;2;0;205;205m-v\e[0m : \e[38;2;205;0;205mBool \e[0m \e[38;2;100;100;100mMore detailed output\e[0m \n STDOUT end diff --git a/src/example.cr b/src/example.cr index 58c6b1e..461a47f 100644 --- a/src/example.cr +++ b/src/example.cr @@ -5,7 +5,7 @@ class Example option(:income, Int32, "", default: 0, validate: ->(v : Int32) { v >= 0 && v <= 1_000_000 }) option(:ssid, String, "", format: /\A[0-9]{10}\z/) - option(:force, Bool, "Description", default: false) + option(:force, Bool, "Force the change", default: false) arg(:name, String) arg(:age, Int32, validate: ->(v : Int32) { (13..150).includes?(v) }) @@ -15,7 +15,9 @@ class Example end cli = Kommando::Namespace.root do - command Example + namespace("examples") do + command Example + end end cli.exec(ARGV) diff --git a/src/kommando.cr b/src/kommando.cr index 50769bc..c89764e 100644 --- a/src/kommando.cr +++ b/src/kommando.cr @@ -1,5 +1,6 @@ require "colorize" require "./kommando/version" +require "./kommando/colors" require "./kommando/errors" require "./kommando/parser" require "./kommando/docker" diff --git a/src/kommando/colors.cr b/src/kommando/colors.cr new file mode 100644 index 0000000..f495930 --- /dev/null +++ b/src/kommando/colors.cr @@ -0,0 +1,14 @@ +module Kommando + alias RGB = Colorize::ColorRGB + + GREEN = RGB.new(0, 220, 0) + RED = RGB.new(220, 0, 0) + YELLOW = RGB.new(220, 220, 0) + WHITE = RGB.new(255, 255, 255) + DARK_GRAY = RGB.new(100, 100, 100) + LIGHT_GRAY = RGB.new(200, 200, 200) + MAGENTA = RGB.new(205, 0, 205) + LIGHT_MAGENTA = RGB.new(220, 0, 220) + CYAN = RGB.new(0, 205, 205) + LIGHT_BLUE = RGB.new(90, 90, 250) +end diff --git a/src/kommando/command.cr b/src/kommando/command.cr index c341906..8d3ca56 100644 --- a/src/kommando/command.cr +++ b/src/kommando/command.cr @@ -103,7 +103,7 @@ module Kommando Tuple.new( {% for var in @type.instance_vars %} {% if ann = var.annotation(Kommando::Argument) %} - { pos: {{i}}, {{**ann.named_args}} }, + { pos: {{i}}, {{ann.named_args.double_splat}} }, {% i += 1 %} {% end %} {% end %} @@ -124,9 +124,9 @@ module Kommando end def self.describe(io : IO) - io << command_name.colorize(:yellow) + io << command_name.colorize(Kommando::YELLOW) io << ": " - io.puts description.colorize(:dark_gray) + io.puts description.colorize(Kommando::DARK_GRAY) io.puts io << "Usage: " @@ -144,37 +144,39 @@ module Kommando end def self.describe_usage(io : IO) - io << command_name.colorize(:yellow) + io << command_name.colorize(Kommando::YELLOW) io << " " - positionals.each { |a| io << a[:name].colorize(:light_blue) } + positionals.join(io, " ") { |a, io| + io << a[:name].colorize(Kommando::LIGHT_BLUE) + } io << " " - io << "-option=value".colorize(:dark_gray) + io << "-option=value".colorize(Kommando::DARK_GRAY) end def self.describe_positionals(io : IO) positionals.each { |a| - io << (" %-10s" % a[:name]).colorize(:light_blue) + io << (" %-10s" % a[:name]).colorize(Kommando::LIGHT_BLUE) io << " : " - io << ("%-8s" % a[:type]).colorize(:magenta) + io << ("%-8s" % a[:type]).colorize(Kommando::MAGENTA) io.puts } end def self.describe_options(io : IO) options.each { |name, o| - io << (" %-10s " % name).colorize(:light_blue) + io << (" %-10s " % name).colorize(Kommando::LIGHT_BLUE) shortcut = "" shortcut = ("-" + o[:short].to_s) if o[:short] - io << "%-2s" % shortcut.colorize(:cyan) + io << "%-2s" % shortcut.colorize(Kommando::CYAN) io << " : " - io << ("%-8s" % o[:type]).colorize(:magenta) + io << ("%-8s" % o[:type]).colorize(Kommando::MAGENTA) io << " " - io << o[:desc].colorize(:dark_gray) + io << o[:desc].colorize(Kommando::DARK_GRAY) io.puts } end diff --git a/src/kommando/docker.cr b/src/kommando/docker.cr index b2ca7cb..72494b0 100644 --- a/src/kommando/docker.cr +++ b/src/kommando/docker.cr @@ -28,12 +28,12 @@ module Kommando ) # if status.success? - # print "✓ ".colorize(:green) + # print "✓ ".colorize(GREEN) # # puts "#{name} [#{Docker.images(stdio.to_s).last}]" # # puts stdio.to_s.split("\n")[-3] # else - # print "× ".colorize(:red) + # print "× ".colorize(RED) # puts "#{name}" # puts stdio.to_s diff --git a/src/kommando/interaction.cr b/src/kommando/interaction.cr index 5c84318..8958c74 100644 --- a/src/kommando/interaction.cr +++ b/src/kommando/interaction.cr @@ -3,20 +3,28 @@ require "colorize" module Kommando module Interaction class Session - CONFIRM = %w{Y y Yes yes} - DENY = %w{N n No no} + CONFIRM = %w{y yes} + DENY = %w{n no} - @io : IO - delegate gets, to: @io + @outp : IO + @inp : IO getter? colorize : Bool - def self.define(io : IO, colorize : Bool = true) - s = new(io, colorize) + def self.define(outp : IO = STDOUT, inp : IO = STDIN, colorize : Bool = true) + s = new(outp, inp, colorize) with s yield(s) end - def initialize(@io, @colorize = true) + def initialize(@outp, @inp, @colorize = true) + end + + private def print_question(q : String) + wl(q, fg: CYAN) + end + + private def print_input_marker + w("> ", fg: YELLOW) end def read_until(&block : String -> T?) : T forall T @@ -53,12 +61,12 @@ module Kommando print_question(text) options.each_with_index { |(key, desc), i| - w("%2d" % (i + 1), fg: :cyan) + w("%2d" % (i + 1), fg: CYAN) w(" : ") - w(("%8s" % key), fg: :cyan) + w(("%8s" % key), fg: CYAN) if desc w(" : ") - w(desc, fg: :dark_gray) + w(desc, fg: DARK_GRAY) end br } @@ -81,16 +89,31 @@ module Kommando end def confirm(text : String, confirm : Array(String) = CONFIRM, deny : Array(String) = DENY) - ask(text) { |answer| - case answer + options = "[#{(CONFIRM + DENY).join(", ")}]" + + wl(text) + wl(options, fg: LIGHT_GRAY) + + read_until { |answer| + case answer.downcase when .in?(CONFIRM) then true when .in?(DENY) then false + else wl("Invalid input", fg: RED) end } + # print_question(text + "[#{(CONFIRM + DENY).join(", ")}]") + + # read_until { |a| + # case a + # when .in?(CONFIRM) then true + # when .in?(DENY) then false + # else print_question(text) + # end + # } end def read_string_once - gets || "" + @inp.gets || "" end def read_once(type : Int32.class | Float32.class) @@ -102,38 +125,46 @@ module Kommando end end - def print_question(q : String) - w(q, "\n", fg: :blue) + def cancel(s : String = "Cancelled") + wl(s, fg: YELLOW) + exit 0 end - def print_input_marker - w("> ", fg: :yellow) + def abort(s : String) + wl(s, fg: RED) + exit 1 end def br w("\n") end - def colorized_io(fg : Symbol? = nil, bg : Symbol? = nil, m : Colorize::Mode? = nil) + def colorized_io(fg : RGB? = nil, bg : RGB? = nil, m : Colorize::Mode? = nil) if colorize? c = Colorize.with c = c.fore(fg) if fg c = c.mode(m) if m c = c.back(bg) if bg - c.surround(@io) do |cio| + c.surround(@outp) do |cio| yield cio end else - yield @io + yield @outp end end - def w(*strs : String | Int32 | Nil, fg : Symbol? = nil, bg : Symbol? = nil, m : Colorize::Mode? = nil) + def w(*strs : String | Int32 | Nil, fg : RGB? = nil, bg : RGB? = nil, m : Colorize::Mode? = nil) colorized_io(fg, bg, m) do |cio| strs.each { |s| cio << s } end end + + def wl(*strs : String | Int32 | Nil, **options) + # w(*strs + {"\n"}, **options) + w(*strs, **options) + br + end end end end diff --git a/src/kommando/namespace.cr b/src/kommando/namespace.cr index 34ca6a6..0646a3b 100644 --- a/src/kommando/namespace.cr +++ b/src/kommando/namespace.cr @@ -66,7 +66,10 @@ module Kommando elsif ns = @namespaces[arg]? ns.run(args, io) else - raise "Unrecognized command or namespace: #{arg.inspect}" + io.puts "Unrecognized command or namespace: #{arg.inspect}".colorize(RED) + io.puts + help([] of String, io) + exit 1 end end @@ -77,9 +80,9 @@ module Kommando io.puts @commands.each do |name, cmd| - io << (" %-16s" % name).colorize(:light_blue) + io << (" %-16s" % name).colorize(LIGHT_BLUE) - io << cmd.description.colorize(:dark_gray) + io << cmd.description.colorize(DARK_GRAY) io.puts end @@ -93,7 +96,7 @@ module Kommando @namespaces.each do |name, _ns| io << " " - io << name.colorize(:light_blue) + io << name.colorize(LIGHT_BLUE) io.puts end