diff --git a/config.nims b/config.nims index aabef2b..b471031 100644 --- a/config.nims +++ b/config.nims @@ -26,7 +26,7 @@ task docs, "Deploy doc html + search index to public/ directory": when defined(clean): echo fmt"clearing {deployDir}" rmDir deployDir - for module in ["cligen", "chooser", "logging", "cli", "parseopt3"]: + for module in ["cligen", "chooser", "logging", "hwylcli", "parseopt3"]: selfExec fmt"{docCmd} --docRoot:{getCurrentDir()}/src/ src/hwylterm/{module}" selfExec fmt"{docCmd} --project --project src/{pkgName}.nim" docFixup(deployDir, pkgName) diff --git a/src/hwylterm.nim b/src/hwylterm.nim index 593702d..a18d4b2 100644 --- a/src/hwylterm.nim +++ b/src/hwylterm.nim @@ -3,7 +3,7 @@ see also these utility modules: - - [cli](./hwylterm/cli.html) + - [hwylcli](./hwylterm/hwylcli.html) - [cligen adapter](./hwylterm/cligen.html), requires [cligen](https://github.com/c-blake/cligen) - [chooser](./hwylterm/chooser.html) - [logging](./hwylterm/logging.html) diff --git a/src/hwylterm/bbansi.nim b/src/hwylterm/bbansi.nim index fab0ff6..13b84c5 100644 --- a/src/hwylterm/bbansi.nim +++ b/src/hwylterm/bbansi.nim @@ -254,7 +254,6 @@ func bb*(s: string): BbString = else: next - result.closeFinalSpan proc bb*(s: string, style: string): BbString = @@ -263,7 +262,9 @@ proc bb*(s: string, style: string): BbString = proc bb*(s: string, style: Color256): BbString = bb(s, $style) -proc `&`*(x: BbString, y: string): BbString = +proc bb*(s: BbString): BbString = s + +func `&`*(x: BbString, y: string): BbString = result = x result.plain &= y result.spans.add BbSpan(styles: @[], slice: [x.plain.len, result.plain.len - 1]) @@ -343,30 +344,9 @@ proc bbEcho*(args: varargs[string, `$`]) {.raises: [IOError]} = # NOTE: could move to standlone modules in the tools/ directory when isMainModule: - import ./[cli, parseopt3] - + import ./[hwylcli] const version = staticExec "git describe --tags --always --dirty=-dev" - - proc writeHelp() = - let help = $newHwylCli( - "[bold]bbansi[/] [[[green]args...[/]] [[[faint]-h|-v[/]]", - """ -bbansi "[[yellow] yellow text!" - -> [yellow] yellow text![/] -bbansi "[[bold red] bold red text[[/] plain text..." - -> [bold red] bold red text[/] plain text... -bbansi "[[red]some red[[/red] but all italic" --style:italic - -> [italic][red]some red[/red] but all italic[/italic] -""", - [ - ("h", "help", "show this help"), - ("v", "version", "show version"), - ("s", "style", "set style for string"), - ] - ) - echo help; quit 0 - - proc testCard() = + proc showTestCard() = for style in [ "bold", "faint", "italic", "underline", "blink", "reverse", "conceal", "strike" ]: @@ -379,52 +359,39 @@ bbansi "[[red]some red[[/red] but all italic" --style:italic echo "on ", color, " -> ", fmt"[on {color}]****".bb quit(QuitSuccess) - proc debug(bbs: BbString): string = + proc debugBb(bbs: BbString): string = echo "bbString(" echo " plain: ", bbs.plain echo " spans: ", bbs.spans echo " escaped: ", escape($bbs) echo ")" - proc writeVersion() = - echo bbfmt"[yellow]bbansi version[/][red] ->[/] [bold]{version}[/]" - quit 0 - - var - strArgs: seq[string] - style: string - showDebug: bool - var p = initOptParser( - shortNoVal = {'h','v'}, - longNoVal = @["help", "version", "testCard"] - ) - for kind, key, val in p.getopt(): - case kind - of cmdError: quit($(bb"[red]cli error[/]: " & p.message), 1) - of cmdEnd: assert(false) - of cmdShortOption, cmdLongOption: - case key - of "testCard" : testCard() - of "help" , "h": writeHelp() - of "version", "v": writeVersion() - of "style" , "s": - if val == "": - bbEcho "[red]ERROR[/]: expected value for -s/--style" - quit(QuitFailure) - style = val - of "debug": - showDebug = true - else: - bbEcho "[yellow]warning[/]: unexpected option/value -> ", key, ", ", val - of cmdArgument: strArgs.add key - if strArgs.len == 0: - writeHelp() - for arg in strArgs: - let styled = - if style != "": - arg.bb(style) - else: - arg.bb - echo styled - if showDebug: - echo debug(styled) + hwylCli: + name "bbansi" + settings NoArgsShowHelp + usage "[bold]bbansi[/] [[[green]args...[/]] [[[faint]-h|-V[/]]" + description """ + bbansi "[[yellow] yellow text!" + -> [yellow] yellow text![/] + bbansi "[[bold red] bold red text[[/] plain text..." + -> [bold red] bold red text[/] plain text... + bbansi "[[red]some red[[/red] but all italic" --style:italic + -> [italic][red]some red[/red] but all italic[/italic] + """ + version bbfmt"[yellow]bbansi version[/][red] ->[/] [bold]{version}[/]" + hidden debug, testCard + flags: + debug: + T bool + testCard: + T bool + style: + ? "set style for string" + - "s" + run: + if testCard: showTestCard() + for arg in args: + let styled = arg.bb(style) + echo styled + if debug: + echo debugBb(styled) diff --git a/src/hwylterm/chooser.nim b/src/hwylterm/chooser.nim index c5b5808..563cff1 100644 --- a/src/hwylterm/chooser.nim +++ b/src/hwylterm/chooser.nim @@ -127,56 +127,37 @@ proc choose*[T](things: openArray[T], height: Natural = 6): seq[T] = when isMainModule: - import ./[cli, parseopt3] - - proc writeHelp() = - echo newHwylCli( - "[bold]hwylchoose[/] [[[green]args...[/]] [[[faint]-h[/]]", - """ -hwylchoose a b c d -hwylchoose a,b,c,d -s , -hwylchoose a,b,c,d --seperator "," -""", - [ - ("h", "help", "show this help"), - ("s", "seperator", "seperator to split items"), - ] - ) - - var - posArgs: seq[string] - sep: string - var p = initOptParser( - shortNoVal = {'h'}, longNoVal = @["help", "demo"] - ) - for kind, key, val in p.getopt(): - case kind - of cmdError: quit($(bb"[red]cli error[/]: " & p.message), 1) - of cmdEnd: assert false - of cmdShortOption, cmdLongOption: - case key - of "help", "h": - writeHelp(); quit 0 - of "demo": - posArgs &= LowercaseLetters.toSeq().mapIt($it) - of "seperator","s": - if val == "": - echo bb"[red]ERROR[/]: expected value for --seperator" - quit QuitFailure - sep = val + import ./[hwylcli] + hwylcli: + name "hwylchoose" + settings NoArgsShowHelp + usage "[bold]hwylchoose[/] [[[green]args...[/]] [[[faint]-h[/]]" + description """ + hwylchoose a b c d + hwylchoose a,b,c,d -s , + hwylchoose a,b,c,d --seperator "," + hwylchoose --demo + """ + hidden demo + flags: + demo: + T bool + separator: + help "separator to split items" + short "s" + run: + var items: seq[string] + if demo: + items &= LowercaseLetters.toSeq().mapIt($it) else: - echo bb"[yellow]warning[/]: unexpected option/value -> ", key, ", ", val - of cmdArgument: - posArgs.add key - if posArgs.len == 0: quit "expected values to choose" - var items: seq[string] - if sep != "": - if posArgs.len != 1: quit "only pass one positional arg when using --sep" - items = posArgs[0].split(sep).mapIt(strip(it)) - else: - items = posArgs - let item = choose(items) - echo "selected: ", item + if separator != "": + if args.len != 1: quit "only pass one positional arg when using --separator" + items = args[0].split(separator).mapIt(strip(it)) + else: + items = args + + let item = choose(items) + echo "selected: ", item diff --git a/src/hwylterm/cli.nim b/src/hwylterm/cli.nim deleted file mode 100644 index 58e3457..0000000 --- a/src/hwylterm/cli.nim +++ /dev/null @@ -1,85 +0,0 @@ -##[ - # Cli -]## - -import std/[strutils] -import ./bbansi - -type - HwylFlag = tuple - short, long, description = "" - HwylCliStyles* = object - hdr = "b cyan" - shortFlag = "yellow" - longFlag = "magenta" - descFlag = "" - HwylCli* = object - cmd*: string - usage*: string - flags*: seq[HwylFlag] - styles*: HwylCliStyles - shortArgLen, longArgLen, descArgLen: int - - -func newHwylCli*( - cmd = "", - usage = "", - flags: openArray[HwylFlag] = @[], - styles = HwylCliStyles() -): HwylCli = - result.cmd = cmd - result.usage = usage - result.flags = @flags - result.styles = styles - for f in flags: - result.shortArgLen = max(result.shortArgLen, f.short.len) - result.longArgLen = max(result.longArgLen, f.long.len) - result.descArgLen = max(result.descArgLen, f.description.len) - - -func flagHelp(cli: HwylCli, f: HwylFlag): string = - result.add " " - if f.short != "": - result.add "[" & cli.styles.shortFlag & "]" - result.add "-" & f.short.alignLeft(cli.shortArgLen) - result.add "[/]" - else: - result.add " ".repeat(1 + cli.shortArgLen) - - result.add " " - if f.long != "": - result.add "[" & cli.styles.longFlag & "]" - result.add "--" & f.long.alignLeft(cli.longArgLen) - result.add "[/]" - else: - result.add " ".repeat(2 + cli.longArgLen) - - result.add " " - if f.description != "": - result.add "[" & cli.styles.descFlag & "]" - result.add f.description - result.add "[/]" - result.add "\n" - -proc bbImpl(cli: HwylCli): string = - if cli.cmd != "": - result.add cli.cmd - result.add "\n" - if cli.usage != "": - result.add "\n" - result.add "[" & cli.styles.hdr & "]" - result.add "usage[/]:\n" - result.add indent(cli.usage, 2 ) - result.add "\n" - if cli.flags.len > 0: - result.add "\n" - result.add "[" & cli.styles.hdr & "]" - result.add "flags[/]:\n" - for f in cli.flags: - result.add flagHelp(cli,f) - -proc bb*(cli: HwylCli): BbString = - result = bb(bbImpl(cli)) - -proc `$`*(cli: HwylCli): string = - result = $bb(cli) diff --git a/src/hwylterm/hwylcli.nim b/src/hwylterm/hwylcli.nim new file mode 100644 index 0000000..0366885 --- /dev/null +++ b/src/hwylterm/hwylcli.nim @@ -0,0 +1,768 @@ +##[ + # HwylCli +]## + +import std/[ + macros, os, sequtils, + sets, strutils, tables, + sugar +] +import ./[bbansi, parseopt3] +export parseopt3 + +type + HwylFlagHelp = tuple + short, long, description: string + HwylSubCmdHelp = tuple + name, desc: string + HwylCliStyles* = object + hdr = "bold cyan" + shortFlag = "yellow" + longFlag = "magenta" + descFlag = "" + cmd = "bold" + HwylCliHelp* = object + cmd*: string + usage*: string + desc*: string + subcmds: seq[HwylSubCmdHelp] + flags*: seq[HwylFlagHelp] + styles*: HwylCliStyles + subcmdLen, subcmdDescLen, shortArgLen, longArgLen, descArgLen: int + + +func newHwylCliHelp*( + cmd = "", + usage = "", + desc = "", + subcmds: openArray[HwylSubCmdHelp] = @[], + flags: openArray[HwylFlagHelp] = @[], + styles = HwylCliStyles() +): HwylCliHelp = + result.cmd = cmd + result.desc = dedent(desc).strip() + result.subcmds = subcmds.mapIt((it.name,it.desc.splitlines()[0])) + result.usage = dedent(usage).strip() + result.flags = @flags + result.styles = styles + # TODO: incorporate into "styles?" + result.subcmdLen = 8 + for f in flags: + result.shortArgLen = max(result.shortArgLen, f.short.len) + result.longArgLen = max(result.longArgLen, f.long.len) + result.descArgLen = max(result.descArgLen, f.description.len) + for s in subcmds: + result.subcmdLen = max(result.subcmdLen, s.name.len) + result.subcmdDescLen = max(result.subcmdDescLen, s.desc.len) + +func flagHelp(cli: HwylCliHelp, f: HwylFlagHelp): string = + result.add " " + if f.short != "": + result.add "[" & cli.styles.shortFlag & "]" + result.add "-" & f.short.alignLeft(cli.shortArgLen) + result.add "[/]" + else: + result.add " ".repeat(1 + cli.shortArgLen) + + result.add " " + if f.long != "": + result.add "[" & cli.styles.longFlag & "]" + result.add "--" & f.long.alignLeft(cli.longArgLen) + result.add "[/]" + else: + result.add " ".repeat(2 + cli.longArgLen) + + result.add " " + + if f.description != "": + result.add "[" & cli.styles.descFlag & "]" + result.add f.description + result.add "[/]" + result.add "\n" + +func subCmdLine(cli: HwylCliHelp, subcmd: HwylSubCmdHelp): string = + # NOTE: set some minimum for the subcmdlen? + result.add " " + result.add "[" & cli.styles.cmd & "]" + result.add subcmd.name.alignLeft(cli.subcmdLen) + result.add "[/]" + result.add " " + result.add subcmd.desc.alignLeft(cli.subcmdDescLen) + result.add "\n" + +proc bbImpl(cli: HwylCliHelp): string = + if cli.cmd != "": + result.add cli.cmd + result.add "\n" + if cli.usage != "": + result.add "\n" + result.add "[" & cli.styles.hdr & "]" + result.add "usage[/]:\n" + result.add indent(cli.usage, 2 ) + if cli.desc != "": + result.add "\n\n" + result.add cli.desc + result.add "\n" + if cli.subcmds.len > 0: + result.add "\n" + result.add "[" & cli.styles.hdr & "]" + result.add "subcommands[/]:\n" + for s in cli.subcmds: + result.add cli.subcmdLine(s) + if cli.flags.len > 0: + result.add "\n" + result.add "[" & cli.styles.hdr & "]" + result.add "flags[/]:\n" + for f in cli.flags: + # NOTE: added to accomate dumb macro below + # if f != ("","",""): + result.add flagHelp(cli,f) + +proc bb*(cli: HwylCliHelp): BbString = + result = bb(bbImpl(cli)) + +proc `$`*(cli: HwylCliHelp): string = + result = $bb(cli) + +# --------------------------- +# const supportedbaseTypes = ["string", "bool", "int"] +# const supportedSeqTypes = ["string", "int"] +# # +# proc getObjectFieldTypes(x: NimNode): Table[string, string] = +# let impl = getType(x).getTypeImpl() +# for f in impl[2]: +# let name = f[0].strVal +# template bail = error "unsupported field type for: " & name +# case f[1].kind +# of nnkSym: +# let typeSym = f[1].strVal +# if typeSym notin supportedbaseTypes: bail +# result[name] = typeSym +# of nnkBracketExpr: +# if f[1].len > 2: bail +# if f[1][0].strVal notin "seq": bail +# let typeSym = f[1][1].strVal +# if typeSym notin supportedSeqTypes: bail +# result[name] = f[1][0].strVal & "[" & f[1][1].strVal & "]" +# else: bail + +type + CliSetting = enum + NoHelpFlag, NoArgsShowHelp + CliFlag = object + name*: string + ident*: string + default*: NimNode + typeSym*: string + short*: char + long*: string + help*: NimNode + CliCfg = object + stopWords*: seq[string] + styles: NimNode + hidden*: seq[string] + subcommands: seq[CliCfg] + settings*: set[CliSetting] + run*: NimNode + desc*: NimNode + name*: string + subName*: string # used for help the generator + version*, usage*: NimNode + flags*: seq[CliFlag] + required*: seq[string] + globalFlags*: seq[CliFlag] + +func `?`(n: NimNode) = + ## for debugging macros + debugEcho treeRepr n + +# TODO: do i need this? +func newCliFlag(): CliFlag = + result.help = newLit("") + +template badNode = + error "unexpected node kind: " & $node.kind + +func typeSymFromNode(node: NimNode): string = + case node.kind + of nnkIdent, nnkStrLit: + result = node.strVal + of nnkBracketExpr: + result = node[0].strVal & "[" & node[1].strVal & "]" + else: badNode + +func getOptTypeSym(node: NimNode): string = + case node.kind: + of nnkCommand: + result = typeSymFromNode(node[1]) # [0] is T + of nnkCall: + result = typeSymFromNode(node[1][0]) # [1] is stmtlist [0] is the type + else: error "unexpected node kind: " & $node.kind + +func getOptOptNode(optOptValue: NimNode): NimNode = + case optOptValue.kind + of nnkStrLit: + result = optOptValue + of nnkStmtList: + result = optOptValue[0] + of nnkCommand: + result = optOptValue[1] + of nnkPrefix: # NOTE: should i double check prefix value? + result = optOptValue[1] + else: error "unexpected node kind: " & $optOptValue.kind + +# TODO: don't use the confusing name optOpts here and above +func parseOptOpts(opt: var CliFlag, optOpts: NimNode) = + expectKind optOpts, nnkStmtList + for optOpt in optOpts: + case optOpt.kind + of nnkCall, nnkCommand, nnkPrefix: + case optOpt[0].strVal + of "help","?": + opt.help = getOptOptNode(optOpt[1]) + of "short", "-": + let val = getOptOptNode(optOpt).strVal + if val.len > 1: + error "short flag must be a char" + opt.short = val[0].char + of "*", "default": + opt.default = getOptOptNode(optOpt) + of "i", "ident": + opt.ident = getOptOptNode(optOpt).strVal + of "T": + opt.typeSym = getOptTypeSym(optOpt) + else: + error "unexpected option setting: " & optOpt[0].strVal + else: + error "unexpected option node type: " & $optOpt.kind + +func startFlag(f: var CliFlag, n: NimNode) = + f.name = + case n[0].kind + of nnkIdent, nnkStrLit: n[0].strVal + of nnkAccQuoted: collect(for c in n[0]: c.strVal).join("") + else: error "unexpected node kind for option" + + # assume a single character is a short flag + if f.name.len == 1: + f.short = f.name[0].char + else: + f.long = f.name + +func parseCliFlag(n: NimNode): CliFlag = + if n.kind notin [nnkCommand, nnkCall]: + error "unexpected node kind: " & $n.kind + + # deduplicate these... + result = newCliFlag() + startFlag(result, n) + # option "some help desc" + if n.kind == nnkCommand: + result.help = n[1] + # option: + # help "some help description" + else: + parseOptOpts(result, n[1]) + + if result.ident == "": + result.ident = result.name + if result.typeSym == "": + result.typeSym = "string" + + +func parseCliFlags(flags: NimNode): seq[CliFlag] = + expectKind flags, nnkStmtList + for f in flags: + result.add parseCliFlag(f) + +func parseCliSetting(s: string): CliSetting = + try: parseEnum[CliSetting](s) + except: error "unknown cli setting: " & s + +func parseCliSettings(cfg: var CliCfg, node: NimNode) = + case node.kind + of nnkCommand: + for n in node[1..^1]: + cfg.settings.incl parseCliSetting(n.strVal) + of nnkCall: + expectKind node[1], nnkStmtList + for n in node[1]: + cfg.settings.incl parseCliSetting(n.strVal) + else: assert false + +func parseIdentLikeList(node: NimNode): seq[string] = + template check = + if n.kind notin [nnkStrLit,nnkIdent]: + error "expected StrLit or Ident, got:" & $n.kind + case node.kind + of nnkCommand: + for n in node[1..^1]: + check + result.add n.strVal + of nnkCall: + expectKind node[1], nnkStmtList + for n in node[1]: + check + result.add n.strVal + else: assert false + +func parseCliBody(body: NimNode, name: string = ""): CliCfg + +func isSubMarker(node: NimNode): bool = + if node.kind == nnkPrefix: + if eqIdent(node[0], "---"): + result = true + +func sliceStmts(node: NimNode): seq[ + tuple[name: string, slice: Slice[int]] +] = + + if not isSubMarker(node[0]): + error "expected a subcommand delimiting line" + + var + name: string = node[0][1].strVal + start = 1 + let nodeLen = node.len() + + for i in 1.. 0: + var handleSubCommands = nnkStmtList.newTree() + handleSubCommands.add quote do: + if `args`.len == 0: + quit "expected subcommand" + + var subCommandCase = nnkCaseStmt.newTree( + quote do: `args`[0] + ) + for sub in cfg.subcommands: + subCommandCase.add nnkOfBranch.newTree( + newLit(sub.subName), + hwylCliImpl(sub) + ) + + subcommandCase.add nnkElse.newTree( + quote do: + quit $bb"[red]error[/] unknown subcommand: " & `args`[0] + ) + + runBody.add handleSubCommands.add subCommandCase + + result.add quote do: + # block: + `printHelperProc` + `flagVars` + proc `parserProcName`(`cmdLine`: openArray[string] = commandLineParams()): seq[string] = + `parserBody` + + proc `runProcName`(`cmdLine`: openArray[string] = commandLineParams()) = + let `args` = `parserProcName`(`cmdLine`) + `runBody` + + if root: + result.add quote do: + `runProcName`() + else: + result.add quote do: + `runProcName`(`args`[1..^1]) + +macro hwylCli*(body: untyped) = + var cfg = parseCliBody(body) + hwylCliImpl(cfg, root = true) + +when isMainModule: + import std/strformat + hwylCli: + name "hwylterm" + ... "a description of hwylterm" + globalFlags: + config: + T seq[string] + ? "path to config file" + flags: + check: + T bool + ? "load config and exit" + - c + run: + echo "hello from the main command" + echo fmt"{config=}, {check=}" + subcommands: + --- a + description "the \"a\" subcommand" + flags: + `long-flag` "some help" + flagg "some other help" + run: + echo config + echo "hello from hwylterm sub command!" + echo `long-flag` + echo flagg + --- b + description "the \"b\" subcommand" + flags: + aflag: + T bool + ? "some help" + bflag: + ? "some other flag?" + * "wow" + run: + echo "hello from hwylterm sub `b` command" + echo aflag, bflag + + diff --git a/todo.md b/todo.md index ae56a83..bcd1bf1 100644 --- a/todo.md +++ b/todo.md @@ -12,6 +12,7 @@ - [ ] revamp spinner api (new threads?) - [x] add Bbstring ~~indexing operations~~ strutils, that are span aware - [ ] add a `commands` option for `newHwylCli` in `hwylterm/cli` +- [ ] console object with customizable options to apply formatting ## features