Skip to content

Commit

Permalink
WIP: render colours in help
Browse files Browse the repository at this point in the history
  • Loading branch information
keynmol committed May 11, 2024
1 parent 9e676f3 commit 9e50b30
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 19 deletions.
66 changes: 48 additions & 18 deletions core/shared/src/main/scala/com/monovore/decline/Help.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -34,33 +36,42 @@ 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)

val commandHelp =
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)
)
Expand Down Expand Up @@ -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, _), _) =>
Expand All @@ -122,15 +151,16 @@ 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(", ")
),
withIndent(8, help)
)
case (Opt.Argument(_), _) => Nil
}
}

private def withIndent(indent: Int, s: String): String =
// Predef.augmentString = work around scala/bug#11125
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
48 changes: 48 additions & 0 deletions core/shared/src/main/scala/com/monovore/decline/Theme.scala
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 <integer>] [--third <integer>] [--flagOpt[=<string>]] --flag[=<string>] [--flag[=<string>]]... run
|
Expand Down
2 changes: 1 addition & 1 deletion project/build.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sbt.version=1.7.3
sbt.version=1.9.7
4 changes: 4 additions & 0 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

0 comments on commit 9e50b30

Please sign in to comment.