From b317a55cbff1923f09db854c27d37f1078613195 Mon Sep 17 00:00:00 2001 From: Charles Blake Date: Sat, 2 Nov 2024 16:57:38 -0400 Subject: [PATCH] Replace `textUt.ttyWidth = width` with a more fine-grained control mechanism of `wrapDoc`, `wrapTable`. Special values of `0` mean the old auto-terminal-width wrapping, `-1` means never wrap at all, and other values mean to wrap at that column (including indent). Update both config file parsers to allow CLusers to tweak settings. Update `test/PassValuesMulti.nim` with some example code showing 4 different wrap columns in play. Update reference output in light of both `cligen.nim` edits and new `PassValuesMulti` help output. Also update RELEASE_NOTES.md with details for CLauthors and revert the description (& implementation) of `var ttyWidth` in `textUt.nim` since it is less general. --- RELEASE-NOTES.md | 16 +++++++++---- cligen.nim | 18 +++++++++------ cligen/clCfgInit.nim | 4 +++- cligen/clCfgToml.nim | 2 ++ cligen/textUt.nim | 10 ++++---- test/PassValuesMulti.nim | 9 ++++++-- test/ref | 49 ++++++++++++++++++++++++++++------------ 7 files changed, 75 insertions(+), 33 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 046e9b95..ed12f5c5 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -3,10 +3,6 @@ RELEASE NOTES Version: 1.7.8 -------------- - - Make `cligen/textUt.ttyWidth` a `var` to enable fully draconian CLauthors - to hard-code any terminal wrapWidth (set `clCfg.widthEnv=""` to block any - run-time CLuser override). - - Allow CLauthors to set `clCfg.minStrQuoting = true` (& CLusers to override with `[layout]minStrQuoting = true|false|etc.`) to only put string default values in double quotes if "necessary" (presently containing a hard-coded @@ -17,6 +13,18 @@ Version: 1.7.8 `true` default value & the very same for `falseDefault`. `test/Version.nim` has an example of both `minStrQuoting` & `falseDefault`. + - Add 2 new fields to `ClCfg` to control word wrapping in doc-like and table- + like contexts, also adjustable via the two provided config-file systems. + 0=auto terminal width as before, -1 = never wrap, else some specific wrap + column (in an absolute sense, including leading indents). These also apply + to the top-level help & subcommand table for multi-cmds. To truly block + CLusers from adjusting wrap, CLauthor must disable use of `clCfg.widthEnv`, + and either not include a config file system or do their own that disallows + [layout]wrap(Doc|Table) edit. So, while CLauthors have "ultimate" control, + ease is not a priority since CLusers really know the most about what help + output will be used for (eg. piping output to grep, $PAGER, etc.). Sample + code showing fine-grained wrap control is in `test/PassValuesMulti.nim`. + Version: 1.7.7 -------------- - Long overdue simplification of config-file open in `clCfgInit`, `clCfgToml` diff --git a/cligen.nim b/cligen.nim index 3646aafd..cbe0babf 100644 --- a/cligen.nim +++ b/cligen.nim @@ -49,6 +49,8 @@ type # Main defns CLI authors need be aware of (besides top-level API calls) minStrQuoting*: bool ## Only quote string defaults when necessary trueDefault*: string ## How to render a default value of "true" falseDefault*: string ## How to render a default value of "false" + wrapDoc*: int ## Terminal column to wrap at for doc-like &.. + wrapTable*: int ##..Table-like sections. 0 => auto -1 => never HelpOnly* = object of CatchableError ## Ok Ctl Flow Only For --help VersionOnly* = object of CatchableError ## Ok Ctl Flow Only For --version @@ -588,8 +590,9 @@ macro dispatchGen*(pro: typed{nkSym}, cmdName: string="", doc: string="", if isReq: result.add(quote do: `mandId`.add(`parNm`)) result.add(quote do: # build one large help string - let ww = wrapWidth(`cf`.widthEnv) - let indentDoc = addPrefix(`prefixId`, wrap(mayRend(`cmtDoc`), ww, + let wwd = wrapWidth(`cf`.widthEnv, `cf`.wrapDoc) + let wwt = wrapWidth(`cf`.widthEnv, `cf`.wrapTable) + let indentDoc = addPrefix(`prefixId`, wrap(mayRend(`cmtDoc`), wwd, prefixLen=`prefixId`.len)) proc hl(tag, val: string): string = # {.gcsafe.} clCfg access (`cf`.helpAttr.getOrDefault(tag, "") & val & @@ -612,7 +615,7 @@ macro dispatchGen*(pro: typed{nkSym}, cmdName: string="", doc: string="", alignTable(`tabId`, 2*len(`prefixId`) + 2, `cf`.hTabColGap, `cf`.hTabMinLast, `cf`.hTabRowSep, toInts(`cf`.hTabCols), - `cf`.onCols, `cf`.offCols, width=ww)) ] + `cf`.onCols, `cf`.offCols, width=wwt))] if `apId`.help.len > 0 and `apId`.help[^1] != '\n': #ensure newline @end `apId`.help &= "\n" if len(`prefixId`) > 0: # to indent help in a multicmd context @@ -970,12 +973,13 @@ proc topLevelHelp*(doc: auto, use: auto, cmd: auto, subCmds: auto, clCfg.helpAttr.getOrDefault("doc", "") ] let off= @[ clCfg.helpAttrOff.getOrDefault("cmd", ""), clCfg.helpAttrOff.getOrDefault("doc", "") ] - let ww = wrapWidth(clCfg.widthEnv) - let docUse = if clCfg.render != nil: wrap(clCfg.render(doc), ww) - else: wrap(doc, ww) + let wwd = wrapWidth(clCfg.widthEnv, clCfg.wrapDoc) + let wwt = wrapWidth(clCfg.widthEnv, clCfg.wrapTable) + let docUse = if clCfg.render != nil: wrap(clCfg.render(doc), wwd) + else: wrap(doc, wwd) use % [ "doc", docUse, "command", on[0] & cmd & off[0], "ifVersion", ifVsn, "subcmds", addPrefix(" ", alignTable(pairs, 2, attrOn=on, - attrOff=off, width=ww))] + attrOff=off, width=wwt))] proc docDefault(n: NimNode): NimNode = if n.len > 1: newStrLitNode(summaryOfModule(n[1][0])) diff --git a/cligen/clCfgInit.nim b/cligen/clCfgInit.nim index 93a6d60d..6913d79d 100644 --- a/cligen/clCfgInit.nim +++ b/cligen/clCfgInit.nim @@ -63,8 +63,10 @@ proc apply(c: var ClCfg, path: string, plain=false) = c.noHelpHelp = e.value.optionNormalize in yes of "minstrquoting": c.minStrQuoting = e.value.optionNormalize in yes - of "truedefault": c.trueDefault = e.value + of "truedefault" : c.trueDefault = e.value of "falsedefault": c.falseDefault = e.value + of "wrapDoc" : c.wrapDoc = e.value.parseInt + of "wrapTable" : c.wrapTable = e.value.parseInt else: stderr.write path & ":" & " unexpected setting " & e.key & "\n" & "Expecting: rowseparator columngap leastfinal required columns " & diff --git a/cligen/clCfgToml.nim b/cligen/clCfgToml.nim index 1925bbe9..683fc056 100644 --- a/cligen/clCfgToml.nim +++ b/cligen/clCfgToml.nim @@ -48,6 +48,8 @@ proc apply(c: var ClCfg, cfgFile: string, plain=false) = of "minstrquoting": c.minStrQuoting = v2.getBool() of "truedefault" : c.trueDefault = v2.getStr() of "falsedefault": c.falseDefault = v2.getStr() + of "wrapDoc" : c.wrapDoc = v2.getInt() + of "wrapTable" : c.wrapTable = v2.getInt() else: stderr.write(&"{cfgFile}: unknown keyword {k2} in the [{k1}] section\n") of "syntax": diff --git a/cligen/textUt.nim b/cligen/textUt.nim index dc1043b7..779e9349 100644 --- a/cligen/textUt.nim +++ b/cligen/textUt.nim @@ -151,7 +151,7 @@ iterator optimalWrap(w: openArray[int], words: openArray[string], m=80, yield i..r[i] i = r[i] + 1 -var ttyWidth* = terminalWidth() +let ttyWidth* = terminalWidth() var errno {.importc, header: "".}: cint errno = 0 #XXX stdlib.terminal should probably clear errno for all client code @@ -181,7 +181,9 @@ proc extraSpace(w0, sep, w1: string): bool {.inline.} = proc wrap*(s: string; maxWidth=ttyWidth, power=3, prefixLen=0): string = ## Multi-paragraph with indent==>pre-formatted optimal line wrapping using ## the badness metric *sum excessSpace^power*. - let maxWidth = maxWidth - 2 * prefixLen + if maxWidth == -1: return s # -1 => wrap disabled + let maxW = if maxWidth == 0: ttyWidth else: maxWidth + let maxWidth = maxW - 2 * prefixLen for tup in s.paragraphs: let (pre, para) = tup if pre: @@ -237,7 +239,7 @@ proc alignTable*(tab: TextTab, prefixLen=0, colGap=2, minLast=16, rowSep="", let last = cols[^1] for row in tab: for c in cols: wCol[c] = max(wCol[c], row[c].measure) - var wTerm = width - prefixLen + let wTerm = (if width==0:ttyWidth elif width == -1:1000 else:width)-prefixLen var leader = (cols.len - 1) * colGap for c in cols[0 .. ^2]: leader += wCol[c] template doCol(c: int): untyped = @@ -256,7 +258,7 @@ proc alignTable*(tab: TextTab, prefixLen=0, colGap=2, minLast=16, rowSep="", result &= '\n' continue let wLast = max(minLast, wTerm - leader) - var wrapped = if '\n' in row[last]: row[last].split("\n") + var wrapped = if width == -1 or '\n' in row[last]: row[last].split("\n") else: wrap(row[last], wLast).split("\n") result &= attrOn[last] & (if wrapped.len>0: wrapped[0] else: "") if wrapped.len == 1: diff --git a/test/PassValuesMulti.nim b/test/PassValuesMulti.nim index e8141371..752b6507 100644 --- a/test/PassValuesMulti.nim +++ b/test/PassValuesMulti.nim @@ -12,7 +12,7 @@ proc show(gamma=1, iota=2.0, verb=false, paths: seq[string]): int = return 42 proc punt(zeta=1, eta=2.0, verb=false, names: seq[string]): int = - ## Another entry point; here we echoResult + ## Another entry point; here we can echoResult echo "zeta:", zeta, " eta:", eta, " verb:", verb for i, n in names: echo "args[", i, "]: ", n return 12345 @@ -44,13 +44,18 @@ Run "$command help" to get *comprehensive* help.$ifVersion""" clCfg.version = "0.0.1" #or maybe nimbleFile.fromNimble("version") clCfg.reqSep = true + clCfg.widthEnv = "" # Disable CLIGEN_WIDTH CLuser override via impossible $"" var noVsn = clCfg + clCfg.wrapDoc = 32; clCfg.wrapTable = 36 {.pop.} noVsn.version = "" + noVsn.hTabMinLast = 1 # 11 < default=16 & code does a max(minLast, target). + noVsn.wrapDoc = 11; noVsn.wrapTable = -1 const demohelp = { "verb": "on=chatty, off=quiet" }.toTable dispatchMulti([ "multi", doc = docLine, usage = topLvlUse ], [ demo, usage=hlUse, help = demohelp ], [ show, cmdName="print", usage=hlUse, short = { "gamma": 'z' }], - [ punt, echoResult=true, usage=hlUse, cf=noVsn ], + [ punt, echoResult=true, usage=hlUse, cf=noVsn, help={"zeta": + "very long help string for zeta to show no-wrapping"} ], [ nel_Ly, cmdName="nel-ly", usage=hlUse, noAutoEcho=true ] ) diff --git a/test/ref b/test/ref index 05a18dbe..84712e5f 100644 --- a/test/ref +++ b/test/ref @@ -652,16 +652,24 @@ Options: -a=, --a2= strings {} append 1 val to a2 ==> test/PassValuesMulti.out <== -Infer & generate command-line interface/option/argument parser +Infer & generate command-line +interface/option/argument parser Usage: PassValuesMulti {SUBCMD} [sub-command options & parameters] SUBCMDs: - help print comprehensive or per-cmd help - demo demo entry point with varied, meaningless parameters. - print show entry point with varied, meaningless parameters. - punt Another entry point; here we echoResult - nel-ly Yet another entry point; here we block autoEcho + help print comprehensive or + per-cmd help + demo demo entry point with + varied, meaningless + parameters. + print show entry point with + varied, meaningless + parameters. + punt Another entry point; here + we can echoResult + nel-ly Yet another entry point; + here we block autoEcho PassValuesMulti {-h|--help} or with no args at all prints this message. PassValuesMulti --help-syntax gives general cligen syntax help. @@ -678,16 +686,21 @@ for top-level/all subcommands. Usage is like: where subcommand syntaxes are as follows: demo [optional-params] [files: string...] -  demo entry point with varied, meaningless parameters. +  demo entry point with + varied, meaningless + parameters. Options: --version bool false print version -a=, --alpha= int 1 set alpha -b=, --beta= float 2.0 set beta - -v, --verb bool false on=chatty, off=quiet + -v, --verb bool false on=chatty, + off=quiet -i=, --item= string "" set item print [optional-params] [paths: string...] -  show entry point with varied, meaningless parameters. +  show entry point with + varied, meaningless + parameters. Options: --version bool false print version -z=, --gamma= int 1 set gamma @@ -695,14 +708,20 @@ where subcommand syntaxes are as follows: -v, --verb bool false set verb punt [optional-params] [names: string...] -  Another entry point; here we echoResult +  Another + entry + point; + here + we can + echoResult Options: - -z=, --zeta= int 1 set zeta + -z=, --zeta= int 1 very long help string for zeta to show no-wrapping -e=, --eta= float 2.0 set eta -v, --verb bool false set verb nel-ly [optional-params] [names: string...] -  Yet another entry point; here we block autoEcho +  Yet another entry point; + here we block autoEcho Options: --version bool false print version --hooves= int 4 set hooves @@ -976,9 +995,9 @@ Options: ==> test/TwoNondefaultedSeq.out <== test/TwoNondefaultedSeq.nim(9, 11) template/generic instantiation of `dispatch` from here -cligen.nim(922, 14) template/generic instantiation of `dispatchCf` from here -cligen.nim(909, 14) template/generic instantiation of `dispatchGen` from here -cligen.nim(303, 16) Warning: cligen only supports one seq param for positional args; using `args`, not `stuff`. Use `positional` parameter to `dispatch` to override this. [User] +cligen.nim(925, 14) template/generic instantiation of `dispatchCf` from here +cligen.nim(912, 14) template/generic instantiation of `dispatchGen` from here +cligen.nim(305, 16) Warning: cligen only supports one seq param for positional args; using `args`, not `stuff`. Use `positional` parameter to `dispatch` to override this. [User] Usage: demo [REQUIRED,optional-params] [args: string...] demo entry point with varied, meaningless parameters.