From 70bc4f103366ec6cf856616db5e72452b88d727a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 11 Nov 2023 17:09:23 +0530 Subject: [PATCH] Generate man pages for kitten and all its sub-commands recursively Fixes #6808 --- docs/conf.py | 104 ++++++++++++++++-- docs/kittens/broadcast.rst | 5 + docs/kittens/clipboard.rst | 5 + docs/kittens/hints.rst | 6 ++ docs/kittens/hyperlinked_grep.rst | 6 ++ docs/kittens/icat.rst | 6 ++ docs/kittens/panel.rst | 5 + docs/kittens/query_terminal.rst | 6 ++ docs/kittens/remote_file.rst | 6 ++ docs/kittens/ssh.rst | 5 + docs/kittens/themes.rst | 6 ++ docs/kittens/transfer.rst | 6 ++ docs/kittens/unicode_input.rst | 6 ++ kittens/broadcast/main.py | 1 + kittens/panel/main.py | 1 + kittens/query_terminal/main.py | 1 + tools/cli/completion-parse-args.go | 7 -- tools/cli/help.go | 110 +++++++++++++++++++ tools/cli/markup/prettify.go | 34 +++--- tools/cli/option-from-string.go | 163 +++++++++++++++++++++++++++++ tools/cmd/main.go | 3 +- tools/cmd/tool/main.go | 25 +++++ 22 files changed, 485 insertions(+), 32 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index ed5a556be..a2fac5b09 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -7,6 +7,7 @@ # full list see the documentation: # https://www.sphinx-doc.org/en/master/config +import glob import os import re import subprocess @@ -27,7 +28,7 @@ if kitty_src not in sys.path: sys.path.insert(0, kitty_src) from kitty.conf.types import Definition, expand_opt_references # noqa -from kitty.constants import str_version, website_url # noqa +from kitty.constants import str_version, website_url # noqa # config {{{ # -- Project information ----------------------------------------------------- @@ -176,8 +177,8 @@ manpages_url = 'https://man7.org/linux/man-pages/man{section}/{page}.{section}.h # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('invocation', 'kitty', 'kitty Documentation', [author], 1), - ('conf', 'kitty.conf', 'kitty terminal emulator configuration file', [author], 5) + ('invocation', 'kitty', 'The fast, feature rich terminal emulator', [author], 1), + ('conf', 'kitty.conf', 'Configuration file for kitty', [author], 5) ] @@ -549,10 +550,13 @@ def add_html_context(app: Any, pagename: str, templatename: str, context: Any, d @lru_cache def monkeypatch_man_writer() -> None: ''' - Monkeypatch the docutils man translator to output better tables + Monkeypatch the docutils man translator to be nicer ''' from docutils.writers.manpage import Table, Translator + from docutils.nodes import Element + from sphinx.writers.manpage import ManualPageTranslator + # Generate nicer tables https://sourceforge.net/p/docutils/bugs/475/ class PatchedTable(Table): # type: ignore _options: list[str] def __init__(self) -> None: @@ -572,15 +576,100 @@ def monkeypatch_man_writer() -> None: del ans[3] # top border del ans[-2] # bottom border return ans - def visit_table(self: Translator, node: object) -> None: + def visit_table(self: ManualPageTranslator, node: object) -> None: setattr(self, '_active_table', PatchedTable()) - setattr(Translator, 'visit_table', visit_table) + setattr(ManualPageTranslator, 'visit_table', visit_table) + + # Improve header generation + def header(self: ManualPageTranslator) -> str: + di = getattr(self, '_docinfo') + di['ktitle'] = di['title'].replace('_', '-') + th = (".TH \"%(ktitle)s\" %(manual_section)s" + " \"%(date)s\" \"%(version)s\"") % di + if di["manual_group"]: + th += " \"%(manual_group)s\"" % di + th += "\n" + sh_tmpl: str = (".SH Name\n" + "%(ktitle)s \\- %(subtitle)s\n") + return th + sh_tmpl % di # type: ignore + + setattr(ManualPageTranslator, 'header', header) + + def visit_image(self: ManualPageTranslator, node: Element) -> None: + pass + + def depart_image(self: ManualPageTranslator, node: Element) -> None: + pass + + def depart_figure(self: ManualPageTranslator, node: Element) -> None: + self.body.append(' (images not supported)\n') + Translator.depart_figure(self, node) + + setattr(ManualPageTranslator, 'visit_image', visit_image) + setattr(ManualPageTranslator, 'depart_image', depart_image) + setattr(ManualPageTranslator, 'depart_figure', depart_figure) + + orig_astext = Translator.astext + def astext(self: Translator) -> str: + b = [] + for line in self.body: + if line.startswith('.SH'): + x, y = line.split(' ', 1) + parts = y.splitlines(keepends=True) + parts[0] = parts[0].capitalize() + line = x + ' ' + '\n'.join(parts) + b.append(line) + self.body = b + return orig_astext(self) + setattr(Translator, 'astext', astext) + + +def setup_man_pages() -> None: + from kittens.runner import get_kitten_cli_docs + base = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + for x in glob.glob(os.path.join(base, 'docs/kittens/*.rst')): + kn = os.path.basename(x).rpartition('.')[0] + if kn == 'custom': + continue + cd = get_kitten_cli_docs(kn) or {} + khn = kn.replace('_', '-') + man_pages.append((f'kittens/{kn}', 'kitten-' + khn, cd.get('short_desc', 'kitten Documentation'), [author], 1)) + monkeypatch_man_writer() + + +def build_extra_man_pages() -> None: + base = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + kitten = os.path.join(base, 'kitty/launcher/kitten') + if not os.path.exists(kitten): + raise Exception('The kitten binary is not built cannot generate man pages') + raw = subprocess.check_output([kitten, '-h']).decode() + started = 0 + names = set() + for line in raw.splitlines(): + if line.strip() == '@': + started = len(line.rstrip()[:-1]) + q = line.strip() + if started and len(q.split()) == 1 and not q.startswith('-') and ':' not in q: + if len(line) - len(line.lstrip()) == started: + if not os.path.exists(os.path.join(base, f'docs/kittens/{q}.rst')): + names.add(q) + cwd = os.path.join(base, 'docs/_build/man') + subprocess.check_call([kitten, '__generate_man_pages__'], cwd=cwd) + subprocess.check_call([kitten, '__generate_man_pages__'] + list(names), cwd=cwd) + + +if building_man_pages: + setup_man_pages() + + +def build_finished(*a: Any, **kw: Any) -> None: + if building_man_pages: + build_extra_man_pages() def setup(app: Any) -> None: os.makedirs('generated/conf', exist_ok=True) from kittens.runner import all_kitten_names - monkeypatch_man_writer() kn = all_kitten_names() write_cli_docs(kn) write_remote_control_protocol_docs() @@ -589,6 +678,7 @@ def setup(app: Any) -> None: app.connect('source-read', replace_string) app.add_config_value('analytics_id', '', 'env') app.connect('html-page-context', add_html_context) + app.connect('build-finished', build_finished) app.add_lexer('session', SessionLexer() if version_info[0] < 3 else SessionLexer) app.add_role('link', link_role) app.add_role('commit', commit_role) diff --git a/docs/kittens/broadcast.rst b/docs/kittens/broadcast.rst index f5cc2a2f8..f7be55121 100644 --- a/docs/kittens/broadcast.rst +++ b/docs/kittens/broadcast.rst @@ -1,6 +1,11 @@ broadcast ================================================== +.. only:: man + + Overview + -------------- + *Type text in all kitty windows simultaneously* The ``broadcast`` kitten can be used to type text simultaneously in all diff --git a/docs/kittens/clipboard.rst b/docs/kittens/clipboard.rst index 5ac48c533..823381aea 100644 --- a/docs/kittens/clipboard.rst +++ b/docs/kittens/clipboard.rst @@ -1,6 +1,11 @@ clipboard ================================================== +.. only:: man + + Overview + -------------- + *Copy/paste to the system clipboard from shell scripts* .. highlight:: sh diff --git a/docs/kittens/hints.rst b/docs/kittens/hints.rst index eff21fc32..73d58e597 100644 --- a/docs/kittens/hints.rst +++ b/docs/kittens/hints.rst @@ -1,6 +1,12 @@ Hints ========== +.. only:: man + + Overview + -------------- + + |kitty| has a *hints mode* to select and act on arbitrary text snippets currently visible on the screen. For example, you can press :sc:`open_url` to choose any URL visible on the screen and then open it using your default web diff --git a/docs/kittens/hyperlinked_grep.rst b/docs/kittens/hyperlinked_grep.rst index 06aa8c3c5..838dc868a 100644 --- a/docs/kittens/hyperlinked_grep.rst +++ b/docs/kittens/hyperlinked_grep.rst @@ -1,6 +1,12 @@ Hyperlinked grep ================= +.. only:: man + + Overview + -------------- + + .. note:: As of ripgrep versions newer that 13.0 it supports hyperlinks diff --git a/docs/kittens/icat.rst b/docs/kittens/icat.rst index 6b474d230..94036d016 100644 --- a/docs/kittens/icat.rst +++ b/docs/kittens/icat.rst @@ -1,6 +1,12 @@ icat ======================================== +.. only:: man + + Overview + -------------- + + *Display images in the terminal* The ``icat`` kitten can be used to display arbitrary images in the |kitty| diff --git a/docs/kittens/panel.rst b/docs/kittens/panel.rst index b3e7ce1a7..bd1b4fcc9 100644 --- a/docs/kittens/panel.rst +++ b/docs/kittens/panel.rst @@ -3,6 +3,11 @@ Draw a GPU accelerated dock panel on your desktop .. highlight:: sh +.. only:: man + + Overview + -------------- + You can use this kitten to draw a GPU accelerated panel on the edge of your screen, that shows the output from an arbitrary terminal program. diff --git a/docs/kittens/query_terminal.rst b/docs/kittens/query_terminal.rst index 35b8b9033..91f41f4d5 100644 --- a/docs/kittens/query_terminal.rst +++ b/docs/kittens/query_terminal.rst @@ -1,6 +1,12 @@ Query terminal ================= +.. only:: man + + Overview + -------------- + + This kitten is used to query |kitty| from terminal programs about version, values of various runtime options controlling its features, etc. diff --git a/docs/kittens/remote_file.rst b/docs/kittens/remote_file.rst index 5fa368557..300c62f13 100644 --- a/docs/kittens/remote_file.rst +++ b/docs/kittens/remote_file.rst @@ -1,6 +1,12 @@ Remote files ============== +.. only:: man + + Overview + -------------- + + |kitty| has the ability to easily *Edit*, *Open* or *Download* files from a computer into which you are SSHed. In your SSH session run:: diff --git a/docs/kittens/ssh.rst b/docs/kittens/ssh.rst index 82d7408ae..0e5d7c33a 100644 --- a/docs/kittens/ssh.rst +++ b/docs/kittens/ssh.rst @@ -1,6 +1,11 @@ Truly convenient SSH ========================================= +.. only:: man + + Overview + ---------------- + * Automatic :ref:`shell_integration` on remote hosts * Easily :ref:`clone local shell/editor config ` on remote hosts diff --git a/docs/kittens/themes.rst b/docs/kittens/themes.rst index f4cb57a4b..b9f797cda 100644 --- a/docs/kittens/themes.rst +++ b/docs/kittens/themes.rst @@ -1,6 +1,12 @@ Changing kitty colors ======================== +.. only:: man + + Overview + -------------- + + The themes kitten allows you to easily change color themes, from a collection of over three hundred pre-built themes available at `kitty-themes `_. To use it, simply run:: diff --git a/docs/kittens/transfer.rst b/docs/kittens/transfer.rst index ddb2f5c1b..1fb4b43e8 100644 --- a/docs/kittens/transfer.rst +++ b/docs/kittens/transfer.rst @@ -1,6 +1,12 @@ Transfer files ================ +.. only:: man + + Overview + -------------- + + .. versionadded:: 0.30.0 .. _rsync: https://en.wikipedia.org/wiki/Rsync diff --git a/docs/kittens/unicode_input.rst b/docs/kittens/unicode_input.rst index 8fd32e1bc..83c85a10f 100644 --- a/docs/kittens/unicode_input.rst +++ b/docs/kittens/unicode_input.rst @@ -1,6 +1,12 @@ Unicode input ================ +.. only:: man + + Overview + -------------- + + You can input Unicode characters by name, hex code, recently used and even an editable favorites list. Press :sc:`input_unicode_character` to start the unicode input kitten, shown below. diff --git a/kittens/broadcast/main.py b/kittens/broadcast/main.py index c98d304cc..80baf0262 100644 --- a/kittens/broadcast/main.py +++ b/kittens/broadcast/main.py @@ -160,3 +160,4 @@ elif __name__ == '__doc__': cd['usage'] = usage cd['options'] = OPTIONS cd['help_text'] = help_text + cd['short_desc'] = 'Broadcast typed text to kitty windows' diff --git a/kittens/panel/main.py b/kittens/panel/main.py index 7ed05d31d..032d691dd 100644 --- a/kittens/panel/main.py +++ b/kittens/panel/main.py @@ -149,3 +149,4 @@ elif __name__ == '__doc__': cd['usage'] = usage cd['options'] = OPTIONS cd['help_text'] = help_text + cd['short_desc'] = help_text diff --git a/kittens/query_terminal/main.py b/kittens/query_terminal/main.py index 7f5ea7581..31598d6fd 100644 --- a/kittens/query_terminal/main.py +++ b/kittens/query_terminal/main.py @@ -258,3 +258,4 @@ elif __name__ == '__doc__': cd['usage'] = usage cd['options'] = options_spec cd['help_text'] = help_text + cd['short_desc'] = 'Query the terminal for various capabilities' diff --git a/tools/cli/completion-parse-args.go b/tools/cli/completion-parse-args.go index 0e885076e..3ac6d9787 100644 --- a/tools/cli/completion-parse-args.go +++ b/tools/cli/completion-parse-args.go @@ -11,12 +11,6 @@ import ( var _ = fmt.Print var _ = os.Getenv -func (self *Completions) add_group(group *MatchGroup) { - if len(group.Matches) > 0 { - self.Groups = append(self.Groups, group) - } -} - func (self *Completions) add_options_group(options []*Option, word string) { group := self.AddMatchGroup("Options") if strings.HasPrefix(word, "--") { @@ -122,7 +116,6 @@ func complete_word(word string, completions *Completions, only_args_allowed bool if cmd.ArgCompleter != nil { cmd.ArgCompleter(completions, word, arg_num) } - return } func completion_parse_args(cmd *Command, words []string, completions *Completions) { diff --git a/tools/cli/help.go b/tools/cli/help.go index 19dd06bbf..26571ce64 100644 --- a/tools/cli/help.go +++ b/tools/cli/help.go @@ -8,12 +8,15 @@ import ( "os" "os/exec" "strings" + "time" + "golang.org/x/exp/slices" "golang.org/x/sys/unix" "kitty" "kitty/tools/cli/markup" "kitty/tools/tty" + "kitty/tools/utils" "kitty/tools/utils/style" ) @@ -62,6 +65,37 @@ func (self *Command) FormatSubCommands(output io.Writer, formatter *markup.Conte } +func (self *Option) FormatOptionForMan(output io.Writer) { + fmt.Fprintln(output, ".TP") + fmt.Fprint(output, ".BI \"") + for i, a := range self.Aliases { + fmt.Fprint(output, a.String()) + if i != len(self.Aliases)-1 { + fmt.Fprint(output, ", ") + } + } + fmt.Fprint(output, "\" ") + defval := self.Default + switch self.OptionType { + case StringOption: + if self.IsList { + defval = "" + } + case BoolOption, CountOption: + defval = "" + } + + if defval != "" { + fmt.Fprintf(output, "\" [=%s]\"", escape_text_for_man(defval)) + } + fmt.Fprintln(output) + fmt.Fprintln(output, escape_help_for_man(self.Help)) + if self.Choices != nil { + fmt.Fprintln(output) + fmt.Fprintln(output, "Choices: "+strings.Join(self.Choices, ", ")) + } +} + func (self *Option) FormatOption(output io.Writer, formatter *markup.Context, screen_width int) { fmt.Fprint(output, " ") for i, a := range self.Aliases { @@ -101,6 +135,82 @@ func ShowHelpInPager(text string) { _ = pager.Run() } +func (self *Command) GenerateManPages(level int, recurse bool) (err error) { + var names []string + p := self + for p != nil { + names = append(names, p.Name) + p = p.Parent + } + slices.Reverse(names) + name := strings.Join(names, "-") + outf, err := os.Create(fmt.Sprintf("%s.%d", name, level)) + if err != nil { + return err + } + defer outf.Close() + fmt.Fprintf(outf, `.TH "%s" "1" "%s" "%s" "%s"`, name, time.Now().Format("Jan 02, 2006"), kitty.VersionString, "kitten Manual") + fmt.Fprintln(outf) + fmt.Fprintln(outf, ".SH Name") + fmt.Fprintln(outf, name, "\\-", escape_text_for_man(self.ShortDescription)) + fmt.Fprintln(outf, ".SH Usage") + fmt.Fprintln(outf, ".SY", `"`+self.CommandStringForUsage()+` `+self.Usage+`"`) + fmt.Fprintln(outf, ".YS") + if self.HelpText != "" { + fmt.Fprintln(outf, ".SH Description") + fmt.Fprintln(outf, escape_help_for_man(self.HelpText)) + } + + if self.HasVisibleSubCommands() { + for _, g := range self.SubCommandGroups { + if !g.HasVisibleSubCommands() { + continue + } + title := g.Title + if title == "" { + title = "Commands" + } + fmt.Fprintln(outf, ".SH", title) + + for _, c := range utils.Sort(g.SubCommands, func(a, b *Command) int { return strings.Compare(a.Name, b.Name) }) { + if c.Hidden { + continue + } + if recurse { + if err = c.GenerateManPages(level, recurse); err != nil { + return err + } + } + fmt.Fprintln(outf, ".TP", "2") + fmt.Fprintln(outf, c.Name) + fmt.Fprintln(outf, escape_text_for_man(c.ShortDescription)+".", "See: ") + fmt.Fprintf(outf, ".MR %s %d\n", name+"-"+c.Name, level) + } + } + fmt.Fprintln(outf, ".PP") + fmt.Fprintln(outf, "Get help for an individual command by running:") + fmt.Fprintln(outf, ".SY", self.CommandStringForUsage()) + fmt.Fprintln(outf, "command", "-h") + fmt.Fprintln(outf, ".YS") + } + + group_titles, gmap := self.GetVisibleOptions() + if len(group_titles) > 0 { + for _, title := range group_titles { + ptitle := title + if title == "" { + ptitle = "Options" + } + fmt.Fprintln(outf, ".SH", ptitle) + for _, opt := range gmap[title] { + opt.FormatOptionForMan(outf) + } + } + } + + return +} + func (self *Command) ShowHelpWithCommandString(cs string) { formatter := markup.New(tty.IsTerminal(os.Stdout.Fd())) screen_width := 80 diff --git a/tools/cli/markup/prettify.go b/tools/cli/markup/prettify.go index eac2d9cbf..29aad2a87 100644 --- a/tools/cli/markup/prettify.go +++ b/tools/cli/markup/prettify.go @@ -53,7 +53,7 @@ func New(allow_escape_codes bool) *Context { return &ans } -func remove_backslash_escapes(text string) string { +func Remove_backslash_escapes(text string) string { // see https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#escaping-mechanism out := strings.Builder{} prev_was_slash := false @@ -75,11 +75,11 @@ func remove_backslash_escapes(text string) string { return out.String() } -func replace_all_rst_roles(str string, repl func(rst_format_match) string) string { - var m rst_format_match +func ReplaceAllRSTRoles(str string, repl func(Rst_format_match) string) string { + var m Rst_format_match rf := func(full_match string, groupdict map[string]utils.SubMatch) string { - m.payload = groupdict["payload"].Text - m.role = groupdict["role"].Text + m.Payload = groupdict["payload"].Text + m.Role = groupdict["role"].Text return repl(m) } return utils.ReplaceAll(utils.MustCompile(":(?P[a-z]+):(?:(?:`(?P[^`]+)`)|(?:'(?P[^']+)'))"), str, rf) @@ -103,35 +103,35 @@ func (self *Context) hyperlink_for_path(path string, text string) string { return self.hyperlink_for_url(url, text) } -func text_and_target(x string) (text string, target string) { +func Text_and_target(x string) (text string, target string) { parts := strings.SplitN(x, "<", 2) text = strings.TrimSpace(parts[0]) target = strings.TrimRight(parts[len(parts)-1], ">") return } -type rst_format_match struct { - role, payload string +type Rst_format_match struct { + Role, Payload string } func (self *Context) link(x string) string { - text, url := text_and_target(x) + text, url := Text_and_target(x) return self.hyperlink_for_url(url, text) } func (self *Context) ref_hyperlink(x string, prefix string) string { - text, target := text_and_target(x) + text, target := Text_and_target(x) url := "kitty+doc://" + utils.Hostname() + "/#ref=" + prefix + target - text = replace_all_rst_roles(text, func(group rst_format_match) string { - return group.payload + text = ReplaceAllRSTRoles(text, func(group Rst_format_match) string { + return group.Payload }) return self.hyperlink_for_url(url, text) } func (self *Context) Prettify(text string) string { - return replace_all_rst_roles(text, func(group rst_format_match) string { - val := group.payload - switch group.role { + return ReplaceAllRSTRoles(text, func(group Rst_format_match) string { + val := group.Payload + switch group.Role { case "file": if val == "kitty.conf" && self.fmt_ctx.AllowEscapeCodes { path := filepath.Join(utils.ConfigDir(), val) @@ -141,7 +141,7 @@ func (self *Context) Prettify(text string) string { case "env", "envvar": return self.ref_hyperlink(val, "envvar-") case "doc": - text, target := text_and_target(val) + text, target := Text_and_target(val) no_title := text == target target = strings.Trim(target, "/") if title, ok := kitty.DocTitleMap[target]; ok && no_title { @@ -163,7 +163,7 @@ func (self *Context) Prettify(text string) string { case "term": return self.ref_hyperlink(val, "term-") case "code": - return self.Code(remove_backslash_escapes(val)) + return self.Code(Remove_backslash_escapes(val)) case "link": return self.link(val) case "option": diff --git a/tools/cli/option-from-string.go b/tools/cli/option-from-string.go index a1d1dffe8..4685f36ff 100644 --- a/tools/cli/option-from-string.go +++ b/tools/cli/option-from-string.go @@ -9,6 +9,7 @@ import ( "strconv" "strings" + "kitty/tools/cli/markup" "kitty/tools/utils" ) @@ -193,6 +194,168 @@ func indent_of_line(x string) int { return len(x) - len(strings.TrimLeft(x, " \n\t\v\f")) } +func escape_text_for_man(raw string) string { + italic := func(x string) string { + return "\n.I " + x + } + bold := func(x string) string { + return "\n.B " + x + } + text_without_target := func(val string) string { + text, target := markup.Text_and_target(val) + no_title := text == target + if no_title { + return val + } + return text + } + ref_hyperlink := func(val, prefix string) string { + return text_without_target(val) + } + + raw = markup.ReplaceAllRSTRoles(raw, func(group markup.Rst_format_match) string { + val := group.Payload + switch group.Role { + case "file": + return italic(val) + case "env", "envvar": + return bold(val) + case "doc": + return text_without_target(val) + case "iss": + return "Issue #" + val + case "pull": + return "PR #" + val + case "disc": + return "Discussion #" + val + case "ref": + return ref_hyperlink(val, "") + case "ac": + return ref_hyperlink(val, "action-") + case "term": + return ref_hyperlink(val, "term-") + case "code": + return markup.Remove_backslash_escapes(val) + case "link": + return text_without_target(val) + case "option": + idx := strings.LastIndex(val, "--") + if idx < 0 { + idx = strings.Index(val, "-") + } + if idx > -1 { + val = strings.TrimSuffix(val[idx:], ">") + } + return bold(val) + case "opt": + return bold(val) + case "yellow": + return val + case "blue": + return val + case "green": + return val + case "cyan": + return val + case "magenta": + return val + case "emph": + return val + default: + return val + } + }) + sb := strings.Builder{} + sb.Grow(2 * len(raw)) + replacements := map[rune]string{ + '"': `\[dq]`, '\'': `\[aq]`, '-': `\-`, '\\': `\e`, '^': `\(ha`, '`': `\(ga`, '~': `\(ti`, + } + for _, ch := range raw { + if rep, found := replacements[ch]; found { + sb.WriteString(rep) + } else { + sb.WriteRune(ch) + } + } + return sb.String() +} + +func escape_help_for_man(raw string) string { + help := strings.Builder{} + help.Grow(len(raw) + 256) + prev_indent := 0 + in_code_block := false + lines := utils.Splitlines(raw) + + handle_non_empty_line := func(i int, line string) int { + if strings.TrimSpace(line) == "#placeholder_for_formatting#" { + return i + 1 + } + if strings.HasPrefix(line, ".. code::") { + in_code_block = true + return i + 1 + } + current_indent := indent_of_line(line) + if current_indent > 1 { + if prev_indent == 0 { + help.WriteString("\n") + } else { + line = strings.TrimSpace(line) + } + } + prev_indent = current_indent + if help.Len() > 0 && !strings.HasSuffix(help.String(), "\n") { + help.WriteString(" ") + } + help.WriteString(line) + return i + } + + handle_empty_line := func(i int, line string) int { + prev_indent = 0 + help.WriteString("\n") + if !strings.HasSuffix(help.String(), "::") { + help.WriteString("\n") + } + return i + } + + handle_code_block_line := func(i int, line string) int { + if line == "" { + help.WriteString("\n") + return i + } + current_indent := indent_of_line(line) + if current_indent == 0 { + in_code_block = false + return handle_non_empty_line(i, line) + } + line = line[4:] + is_prompt := strings.HasPrefix(line, "$ ") + if is_prompt { + help.WriteString(":yellow:`$ `") + line = line[2:] + } + help.WriteString(line) + help.WriteString("\n") + return i + } + + for i := 0; i < len(lines); i++ { + line := lines[i] + if in_code_block { + i = handle_code_block_line(i, line) + continue + } + if line != "" { + i = handle_non_empty_line(i, line) + } else { + i = handle_empty_line(i, line) + } + } + return escape_text_for_man(help.String()) +} + func prepare_help_text_for_display(raw string) string { help := strings.Builder{} help.Grow(len(raw) + 256) diff --git a/tools/cmd/main.go b/tools/cmd/main.go index eed0f32e9..be907c1b2 100644 --- a/tools/cmd/main.go +++ b/tools/cmd/main.go @@ -20,7 +20,8 @@ func main() { return } root := cli.NewRootCommand() - root.ShortDescription = "Fast, statically compiled implementations for various kittens (command line tools for use with kitty)" + root.ShortDescription = "Fast, statically compiled implementations of various kittens (command line tools for use with kitty)" + root.HelpText = "kitten serves as a launcher for running individual kittens. Each kitten can be run as :code:`kitten command`. The list of available kittens is given below." root.Usage = "command [command options] [command args]" root.Run = func(cmd *cli.Command, args []string) (int, error) { cmd.ShowHelp() diff --git a/tools/cmd/tool/main.go b/tools/cmd/tool/main.go index 832c43607..c8e328be7 100644 --- a/tools/cmd/tool/main.go +++ b/tools/cmd/tool/main.go @@ -96,4 +96,29 @@ func KittyToolEntryPoints(root *cli.Command) { return confirm_and_run_shebang(args) }, }) + // __generate_man_pages__ + root.AddSubCommand(&cli.Command{ + Name: "__generate_man_pages__", + Hidden: true, + OnlyArgsAllowed: true, + Run: func(cmd *cli.Command, args []string) (rc int, err error) { + q := root + if len(args) > 0 { + for _, scname := range args { + sc := q.FindSubCommand(scname) + if sc == nil { + return 1, fmt.Errorf("No sub command named: %s found", scname) + } + if err = sc.GenerateManPages(1, true); err != nil { + return 1, err + } + } + } else { + if err = q.GenerateManPages(1, false); err != nil { + rc = 1 + } + } + return + }, + }) }