diff --git a/core/shared/src/main/scala/com/monovore/decline/Help.scala b/core/shared/src/main/scala/com/monovore/decline/Help.scala index b4cc975a..0a310171 100644 --- a/core/shared/src/main/scala/com/monovore/decline/Help.scala +++ b/core/shared/src/main/scala/com/monovore/decline/Help.scala @@ -3,6 +3,8 @@ package com.monovore.decline import cats.Show import cats.data.NonEmptyList import cats.syntax.all._ +import com.monovore.decline.HelpRenderer.Plain +import com.monovore.decline.HelpRenderer.Colors case class Help( errors: List[String], @@ -34,6 +36,13 @@ object Help { Show.fromToString[Help] def fromCommand(parser: Command[_]): Help = { + fromCommand(parser, HelpRenderer.Plain) + + } + + def fromCommand(parser: Command[_], renderer: HelpRenderer): Help = { + + val theme = Theme.forRenderer(renderer) val commands = commandList(parser.options) @@ -41,26 +50,28 @@ object Help { if (commands.isEmpty) Nil else { val texts = commands.flatMap { command => - List(withIndent(4, command.name), withIndent(8, command.header)) + List(withIndent(4, theme.subcommandName(command.name)), withIndent(8, command.header)) } - List(("Subcommands:" :: texts).mkString("\n")) + List((theme.sectionHeading("Subcommands:") :: texts).mkString("\n")) } val optionsHelp = { - val optionsDetail = detail(parser.options) + val optionsDetail = detail(parser.options, theme) if (optionsDetail.isEmpty) Nil - else ("Options and flags:" :: optionsDetail).mkString("\n") :: Nil + else (theme.sectionHeading("Options and flags:") :: optionsDetail).mkString("\n") :: Nil } val envVarHelp = { - val envVarHelpLines = environmentVarHelpLines(parser.options).distinct + val envVarHelpLines = environmentVarHelpLines(parser.options, theme).distinct if (envVarHelpLines.isEmpty) Nil - else ("Environment Variables:" :: envVarHelpLines.map(" " ++ _)).mkString("\n") :: Nil + else + (theme.sectionHeading("Environment Variables:") :: envVarHelpLines.map(" " ++ _)) + .mkString("\n") :: Nil } Help( errors = Nil, - prefix = NonEmptyList(parser.name, Nil), + prefix = NonEmptyList.of(parser.name), usage = Usage.fromOpts(parser.options).flatMap { _.show }, body = parser.header :: (optionsHelp ::: envVarHelp ::: commandHelp) ) @@ -88,32 +99,50 @@ object Help { case _ => Nil } - def environmentVarHelpLines(opts: Opts[_]): List[String] = opts match { + def environmentVarHelpLines(opts: Opts[_]): List[String] = + environmentVarHelpLines(opts, PlainTheme) + + private def environmentVarHelpLines(opts: Opts[_], theme: Theme): List[String] = opts match { case Opts.Pure(_) => List() case Opts.Missing => List() - case Opts.HelpFlag(a) => environmentVarHelpLines(a) - case Opts.App(f, a) => environmentVarHelpLines(f) |+| environmentVarHelpLines(a) - case Opts.OrElse(a, b) => environmentVarHelpLines(a) |+| environmentVarHelpLines(b) + case Opts.HelpFlag(a) => environmentVarHelpLines(a, theme) + case Opts.App(f, a) => environmentVarHelpLines(f, theme) |+| environmentVarHelpLines(a, theme) + case Opts.OrElse(a, b) => + environmentVarHelpLines(a, theme) |+| environmentVarHelpLines(b, theme) case Opts.Single(opt) => List() case Opts.Repeated(opt) => List() - case Opts.Validate(a, _) => environmentVarHelpLines(a) + case Opts.Validate(a, _) => environmentVarHelpLines(a, theme) case Opts.Subcommand(_) => List() - case Opts.Env(name, help, metavar) => List(s"$name=<$metavar>", withIndent(4, help)) + case Opts.Env(name, help, metavar) => + List(theme.envName(name) + s"=<$metavar>", withIndent(4, help)) } - def detail(opts: Opts[_]): List[String] = + def detail(opts: Opts[_]): List[String] = detail(opts, PlainTheme) + + private def detail(opts: Opts[_], theme: Theme): List[String] = { + def optionName(name: String) = theme.optionName(name, Theme.ArgumentRenderingLocation.InOptions) + def metavarName(name: String) = theme.metavar(name, Theme.ArgumentRenderingLocation.InOptions) + optionList(opts) .getOrElse(Nil) .distinct .flatMap { case (Opt.Regular(names, metavar, help, _), _) => List( - withIndent(4, names.map(name => s"$name <$metavar>").mkString(", ")), + withIndent( + 4, + names.map(name => s"${optionName(name.toString)} <$metavar>").mkString(", ") + ), withIndent(8, help) ) case (Opt.Flag(names, help, _), _) => List( - withIndent(4, names.mkString(", ")), + withIndent( + 4, + names + .map(n => theme.optionName(n.toString(), Theme.ArgumentRenderingLocation.InOptions)) + .mkString(", ") + ), withIndent(8, help) ) case (Opt.OptionalOptArg(names, metavar, help, _), _) => @@ -122,8 +151,8 @@ object Help { 4, names .map { - case Opts.ShortName(flag) => s"-$flag[<$metavar>]" - case Opts.LongName(flag) => s"--$flag[=<$metavar>]" + case Opts.ShortName(flag) => optionName(s"-$flag") + metavarName(s"[<$metavar>]") + case Opts.LongName(flag) => optionName(s"--$flag") + metavarName(s"[=<$metavar>]") } .mkString(", ") ), @@ -131,6 +160,7 @@ object Help { ) case (Opt.Argument(_), _) => Nil } + } private def withIndent(indent: Int, s: String): String = // Predef.augmentString = work around scala/bug#11125 diff --git a/core/shared/src/main/scala/com/monovore/decline/HelpRenderer.scala b/core/shared/src/main/scala/com/monovore/decline/HelpRenderer.scala new file mode 100644 index 00000000..cdd04124 --- /dev/null +++ b/core/shared/src/main/scala/com/monovore/decline/HelpRenderer.scala @@ -0,0 +1,7 @@ +package com.monovore.decline + +sealed trait HelpRenderer extends Product with Serializable +object HelpRenderer { + case object Plain extends HelpRenderer + case object Colors extends HelpRenderer +} diff --git a/core/shared/src/main/scala/com/monovore/decline/Theme.scala b/core/shared/src/main/scala/com/monovore/decline/Theme.scala new file mode 100644 index 00000000..b3bbbfb8 --- /dev/null +++ b/core/shared/src/main/scala/com/monovore/decline/Theme.scala @@ -0,0 +1,48 @@ +package com.monovore.decline + +import cats.Show +import cats.data.NonEmptyList +import cats.syntax.all._ +import com.monovore.decline.HelpRenderer.Plain +import com.monovore.decline.HelpRenderer.Colors + +private[decline] trait Theme { + def sectionHeading(title: String): String = title + def subcommandName(title: String): String = title + def optionName(title: String, loc: Theme.ArgumentRenderingLocation): String = title + def metavar(title: String, loc: Theme.ArgumentRenderingLocation): String = title + def envName(title: String): String = title + def optionPlaceholder(title: String, loc: Theme.ArgumentRenderingLocation): String = title + def optionDescription(value: String): String = value +} + +private[decline] object Theme { + sealed trait ArgumentRenderingLocation extends Product with Serializable + object ArgumentRenderingLocation { + case object InUsage extends ArgumentRenderingLocation + case object InOptions extends ArgumentRenderingLocation + } + def forRenderer(hr: HelpRenderer): Theme = + hr match { + case Plain => PlainTheme + case Colors => ColorTheme + } +} + +private[decline] object PlainTheme extends Theme + +private[decline] object ColorTheme extends Theme { + override def sectionHeading(title: String): String = Console.YELLOW + Console.BOLD + title + Console.RESET + + override def optionName(title: String, loc: Theme.ArgumentRenderingLocation): String = + Console.BOLD + Console.GREEN + title + Console.RESET + + override def metavar(title: String, loc: Theme.ArgumentRenderingLocation): String = + Console.UNDERLINED + title + Console.RESET + + override def envName(title: String): String = + Console.BOLD + Console.GREEN + title + Console.RESET + + override def subcommandName(title: String): String = + Console.BOLD + Console.GREEN + title + Console.RESET +} diff --git a/core/shared/src/test/scala/com/monovore/decline/HelpSpec.scala b/core/shared/src/test/scala/com/monovore/decline/HelpSpec.scala index b9a7606a..68757397 100644 --- a/core/shared/src/test/scala/com/monovore/decline/HelpSpec.scala +++ b/core/shared/src/test/scala/com/monovore/decline/HelpSpec.scala @@ -42,6 +42,8 @@ class HelpSpec extends AnyWordSpec with Matchers { (first, second, third, flagOpt, flagOpts, subcommands).tupled } + println(Help.fromCommand(parser, HelpRenderer.Colors)) + Help.fromCommand(parser).toString should equal( """Usage: program [--first] [--second ] [--third ] [--flagOpt[=]] --flag[=] [--flag[=]]... run | diff --git a/project/build.properties b/project/build.properties index 6a9f0388..e8a1e246 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.7.3 +sbt.version=1.9.7 diff --git a/project/plugins.sbt b/project/plugins.sbt index bf5d8bc0..41f0c2b9 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -12,3 +12,7 @@ addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.0") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.9.3") addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.9.14") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.0") + +libraryDependencySchemes ++= Seq( + "org.scala-lang.modules" %% "scala-xml" % VersionScheme.Always +)