detached 4ddee70

feat(cli): add gen-docs command for auto-generated man pages

commit
4ddee70026fb862e2d81f55ed1a908b8562cb69b
author
bnema · · 2025-12-27 19:31 UTC
parent
02bf2da
signature
GPG Unverified2B82207D370127A0 The key is unknown
7 files · +190 -3
M Makefile +6 -1
@@ -1,6 +1,6 @@
11 # Makefile for dumber (Clean Architecture - puregotk)
22
3-.PHONY: build build-frontend test lint clean install-tools dev generate help init check-docs
3+.PHONY: build build-frontend test lint clean install-tools dev generate help init check-docs man
44
55 # Load local overrides from .env.local if present (Makefile syntax)
66 ifneq (,$(wildcard .env.local))
@@ -137,6 +137,11 @@ check: ## Check that all tools and dependencies are working
137137 @$(MAKE) test
138138 @echo "\nAll checks passed!"
139139
140+# Documentation
141+man: build-quick ## Install man pages to ~/.local/share/man/man1/
142+ @echo "Installing man pages..."
143+ $(DIST_DIR)/$(BINARY_NAME) gen-docs
144+
140145 # Native release targets
141146 .PHONY: release-snapshot release
142147
@@ -1,6 +1,6 @@
1 # Makefile for dumber (Clean Architecture - puregotk)1 # Makefile for dumber (Clean Architecture - puregotk)
2 2
3-.PHONY: build build-frontend test lint clean install-tools dev generate help init check-docs3+.PHONY: build build-frontend test lint clean install-tools dev generate help init check-docs man
4 4
5 # Load local overrides from .env.local if present (Makefile syntax)5 # Load local overrides from .env.local if present (Makefile syntax)
6 ifneq (,$(wildcard .env.local))6 ifneq (,$(wildcard .env.local))
@@ -137,6 +137,11 @@ check: ## Check that all tools and dependencies are working
137 @$(MAKE) test137 @$(MAKE) test
138 @echo "\nAll checks passed!"138 @echo "\nAll checks passed!"
139 139
140+# Documentation
141+man: build-quick ## Install man pages to ~/.local/share/man/man1/
142+ @echo "Installing man pages..."
143+ $(DIST_DIR)/$(BINARY_NAME) gen-docs
144+
140 # Native release targets145 # Native release targets
141 .PHONY: release-snapshot release146 .PHONY: release-snapshot release
142 147
M go.mod +2 -0
@@ -30,6 +30,7 @@ require (
3030 github.com/charmbracelet/x/ansi v0.10.1 // indirect
3131 github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
3232 github.com/charmbracelet/x/term v0.2.1 // indirect
33+ github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
3334 github.com/davecgh/go-spew v1.1.1 // indirect
3435 github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
3536 github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
@@ -47,6 +48,7 @@ require (
4748 github.com/ncruces/julianday v1.0.0 // indirect
4849 github.com/pmezard/go-difflib v1.0.0 // indirect
4950 github.com/rivo/uniseg v0.4.7 // indirect
51+ github.com/russross/blackfriday/v2 v2.1.0 // indirect
5052 github.com/sagikazarmark/locafero v0.11.0 // indirect
5153 github.com/sahilm/fuzzy v0.1.1 // indirect
5254 github.com/sethvargo/go-retry v0.3.0 // indirect
@@ -30,6 +30,7 @@ require (
30 github.com/charmbracelet/x/ansi v0.10.1 // indirect30 github.com/charmbracelet/x/ansi v0.10.1 // indirect
31 github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect31 github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
32 github.com/charmbracelet/x/term v0.2.1 // indirect32 github.com/charmbracelet/x/term v0.2.1 // indirect
33+ github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
33 github.com/davecgh/go-spew v1.1.1 // indirect34 github.com/davecgh/go-spew v1.1.1 // indirect
34 github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect35 github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
35 github.com/go-viper/mapstructure/v2 v2.4.0 // indirect36 github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
@@ -47,6 +48,7 @@ require (
47 github.com/ncruces/julianday v1.0.0 // indirect48 github.com/ncruces/julianday v1.0.0 // indirect
48 github.com/pmezard/go-difflib v1.0.0 // indirect49 github.com/pmezard/go-difflib v1.0.0 // indirect
49 github.com/rivo/uniseg v0.4.7 // indirect50 github.com/rivo/uniseg v0.4.7 // indirect
51+ github.com/russross/blackfriday/v2 v2.1.0 // indirect
50 github.com/sagikazarmark/locafero v0.11.0 // indirect52 github.com/sagikazarmark/locafero v0.11.0 // indirect
51 github.com/sahilm/fuzzy v0.1.1 // indirect53 github.com/sahilm/fuzzy v0.1.1 // indirect
52 github.com/sethvargo/go-retry v0.3.0 // indirect54 github.com/sethvargo/go-retry v0.3.0 // indirect
M go.sum +2 -0 generated file: collapsed
M internal/application/port/xdg.go +4 -0
@@ -10,4 +10,8 @@ type XDGPaths interface {
1010 FilterJSONDir() (string, error)
1111 FilterStoreDir() (string, error)
1212 FilterCacheDir() (string, error)
13+
14+ // ManDir returns the user man page directory (man1 section).
15+ // Typically $XDG_DATA_HOME/man/man1 or ~/.local/share/man/man1.
16+ ManDir() (string, error)
1317 }
@@ -10,4 +10,8 @@ type XDGPaths interface {
10 FilterJSONDir() (string, error)10 FilterJSONDir() (string, error)
11 FilterStoreDir() (string, error)11 FilterStoreDir() (string, error)
12 FilterCacheDir() (string, error)12 FilterCacheDir() (string, error)
13+
14+ // ManDir returns the user man page directory (man1 section).
15+ // Typically $XDG_DATA_HOME/man/man1 or ~/.local/share/man/man1.
16+ ManDir() (string, error)
13 }17 }
A internal/cli/cmd/gen_docs.go +141 -0
@@ -0,0 +1,141 @@
1+package cmd
2+
3+import (
4+ "fmt"
5+ "os"
6+ "path/filepath"
7+ "time"
8+
9+ "github.com/spf13/cobra"
10+ "github.com/spf13/cobra/doc"
11+
12+ xdgadapter "github.com/bnema/dumber/internal/infrastructure/xdg"
13+)
14+
15+const dirPerm = 0o755
16+
17+var (
18+ genDocsOutputDir string
19+ genDocsFormat string
20+)
21+
22+var genDocsCmd = &cobra.Command{
23+ Use: "gen-docs",
24+ Short: "Generate documentation from CLI commands",
25+ Long: `Generate documentation (man pages or markdown) from CLI command definitions.
26+
27+The documentation is auto-generated from the command structure, including:
28+- Command names and aliases
29+- Short and long descriptions
30+- Flags and their descriptions
31+- Usage examples
32+
33+Supported formats:
34+ man Unix manual pages (groff format)
35+ markdown Markdown files (for websites/wikis)
36+
37+By default, man pages are installed to ~/.local/share/man/man1/ so they
38+are immediately available via 'man dumber'. You may need to run 'mandb'
39+to update the man page index.
40+
41+Examples:
42+ dumber gen-docs # Install man pages to ~/.local/share/man/man1/
43+ dumber gen-docs --format markdown # Generate markdown docs
44+ dumber gen-docs --output ./man # Generate to local directory`,
45+ RunE: runGenDocs,
46+}
47+
48+func init() {
49+ rootCmd.AddCommand(genDocsCmd)
50+ genDocsCmd.Flags().StringVarP(&genDocsOutputDir, "output", "o", "", "Output directory for generated docs")
51+ genDocsCmd.Flags().StringVarP(&genDocsFormat, "format", "f", "man", "Output format: man, markdown")
52+}
53+
54+func runGenDocs(_ *cobra.Command, _ []string) error {
55+ // Resolve output directory
56+ outputDir := genDocsOutputDir
57+ if outputDir == "" {
58+ switch genDocsFormat {
59+ case "man":
60+ xdg := xdgadapter.New()
61+ manDir, err := xdg.ManDir()
62+ if err != nil {
63+ return fmt.Errorf("resolve man directory: %w", err)
64+ }
65+ outputDir = manDir
66+ case "markdown":
67+ outputDir = "./docs"
68+ }
69+ }
70+
71+ // Create output directory if it doesn't exist
72+ if err := os.MkdirAll(outputDir, dirPerm); err != nil {
73+ return fmt.Errorf("create output directory: %w", err)
74+ }
75+
76+ switch genDocsFormat {
77+ case "man":
78+ return generateManPages(outputDir)
79+ case "markdown":
80+ return generateMarkdown(outputDir)
81+ default:
82+ return fmt.Errorf("unsupported format %q (use: man, markdown)", genDocsFormat)
83+ }
84+}
85+
86+func generateManPages(outputDir string) error {
87+ header := &doc.GenManHeader{
88+ Title: "DUMBER",
89+ Section: "1",
90+ Source: "dumber " + buildInfo.Version,
91+ Manual: "Dumber Manual",
92+ Date: func() *time.Time { t := time.Now(); return &t }(),
93+ }
94+
95+ // Disable auto-generation timestamp in the footer for reproducible builds
96+ rootCmd.DisableAutoGenTag = true
97+
98+ if err := doc.GenManTree(rootCmd, header, outputDir); err != nil {
99+ return fmt.Errorf("generate man pages: %w", err)
100+ }
101+
102+ fmt.Printf("Installed man pages to %s\n", outputDir)
103+ fmt.Println("Run 'mandb' if 'man dumber' doesn't work immediately.")
104+
105+ // List generated files
106+ entries, err := os.ReadDir(outputDir)
107+ if err != nil {
108+ return nil // Non-fatal
109+ }
110+ for _, e := range entries {
111+ if filepath.Ext(e.Name()) == ".1" {
112+ fmt.Printf(" - %s\n", e.Name())
113+ }
114+ }
115+
116+ return nil
117+}
118+
119+func generateMarkdown(outputDir string) error {
120+ // Disable auto-generation timestamp for reproducible builds
121+ rootCmd.DisableAutoGenTag = true
122+
123+ if err := doc.GenMarkdownTree(rootCmd, outputDir); err != nil {
124+ return fmt.Errorf("generate markdown docs: %w", err)
125+ }
126+
127+ fmt.Printf("Generated markdown docs in %s\n", outputDir)
128+
129+ // List generated files
130+ entries, err := os.ReadDir(outputDir)
131+ if err != nil {
132+ return nil // Non-fatal
133+ }
134+ for _, e := range entries {
135+ if filepath.Ext(e.Name()) == ".md" {
136+ fmt.Printf(" - %s\n", e.Name())
137+ }
138+ }
139+
140+ return nil
141+}
@@ -0,0 +1,141 @@
1+package cmd
2+
3+import (
4+ "fmt"
5+ "os"
6+ "path/filepath"
7+ "time"
8+
9+ "github.com/spf13/cobra"
10+ "github.com/spf13/cobra/doc"
11+
12+ xdgadapter "github.com/bnema/dumber/internal/infrastructure/xdg"
13+)
14+
15+const dirPerm = 0o755
16+
17+var (
18+ genDocsOutputDir string
19+ genDocsFormat string
20+)
21+
22+var genDocsCmd = &cobra.Command{
23+ Use: "gen-docs",
24+ Short: "Generate documentation from CLI commands",
25+ Long: `Generate documentation (man pages or markdown) from CLI command definitions.
26+
27+The documentation is auto-generated from the command structure, including:
28+- Command names and aliases
29+- Short and long descriptions
30+- Flags and their descriptions
31+- Usage examples
32+
33+Supported formats:
34+ man Unix manual pages (groff format)
35+ markdown Markdown files (for websites/wikis)
36+
37+By default, man pages are installed to ~/.local/share/man/man1/ so they
38+are immediately available via 'man dumber'. You may need to run 'mandb'
39+to update the man page index.
40+
41+Examples:
42+ dumber gen-docs # Install man pages to ~/.local/share/man/man1/
43+ dumber gen-docs --format markdown # Generate markdown docs
44+ dumber gen-docs --output ./man # Generate to local directory`,
45+ RunE: runGenDocs,
46+}
47+
48+func init() {
49+ rootCmd.AddCommand(genDocsCmd)
50+ genDocsCmd.Flags().StringVarP(&genDocsOutputDir, "output", "o", "", "Output directory for generated docs")
51+ genDocsCmd.Flags().StringVarP(&genDocsFormat, "format", "f", "man", "Output format: man, markdown")
52+}
53+
54+func runGenDocs(_ *cobra.Command, _ []string) error {
55+ // Resolve output directory
56+ outputDir := genDocsOutputDir
57+ if outputDir == "" {
58+ switch genDocsFormat {
59+ case "man":
60+ xdg := xdgadapter.New()
61+ manDir, err := xdg.ManDir()
62+ if err != nil {
63+ return fmt.Errorf("resolve man directory: %w", err)
64+ }
65+ outputDir = manDir
66+ case "markdown":
67+ outputDir = "./docs"
68+ }
69+ }
70+
71+ // Create output directory if it doesn't exist
72+ if err := os.MkdirAll(outputDir, dirPerm); err != nil {
73+ return fmt.Errorf("create output directory: %w", err)
74+ }
75+
76+ switch genDocsFormat {
77+ case "man":
78+ return generateManPages(outputDir)
79+ case "markdown":
80+ return generateMarkdown(outputDir)
81+ default:
82+ return fmt.Errorf("unsupported format %q (use: man, markdown)", genDocsFormat)
83+ }
84+}
85+
86+func generateManPages(outputDir string) error {
87+ header := &doc.GenManHeader{
88+ Title: "DUMBER",
89+ Section: "1",
90+ Source: "dumber " + buildInfo.Version,
91+ Manual: "Dumber Manual",
92+ Date: func() *time.Time { t := time.Now(); return &t }(),
93+ }
94+
95+ // Disable auto-generation timestamp in the footer for reproducible builds
96+ rootCmd.DisableAutoGenTag = true
97+
98+ if err := doc.GenManTree(rootCmd, header, outputDir); err != nil {
99+ return fmt.Errorf("generate man pages: %w", err)
100+ }
101+
102+ fmt.Printf("Installed man pages to %s\n", outputDir)
103+ fmt.Println("Run 'mandb' if 'man dumber' doesn't work immediately.")
104+
105+ // List generated files
106+ entries, err := os.ReadDir(outputDir)
107+ if err != nil {
108+ return nil // Non-fatal
109+ }
110+ for _, e := range entries {
111+ if filepath.Ext(e.Name()) == ".1" {
112+ fmt.Printf(" - %s\n", e.Name())
113+ }
114+ }
115+
116+ return nil
117+}
118+
119+func generateMarkdown(outputDir string) error {
120+ // Disable auto-generation timestamp for reproducible builds
121+ rootCmd.DisableAutoGenTag = true
122+
123+ if err := doc.GenMarkdownTree(rootCmd, outputDir); err != nil {
124+ return fmt.Errorf("generate markdown docs: %w", err)
125+ }
126+
127+ fmt.Printf("Generated markdown docs in %s\n", outputDir)
128+
129+ // List generated files
130+ entries, err := os.ReadDir(outputDir)
131+ if err != nil {
132+ return nil // Non-fatal
133+ }
134+ for _, e := range entries {
135+ if filepath.Ext(e.Name()) == ".md" {
136+ fmt.Printf(" - %s\n", e.Name())
137+ }
138+ }
139+
140+ return nil
141+}
M internal/cli/cmd/root.go +18 -2
@@ -16,8 +16,24 @@ var (
1616 buildInfo build.Info
1717 rootCmd = &cobra.Command{
1818 Use: "dumber",
19- Short: "A minimal web browser",
20- Long: `Dumber is a minimal, keyboard-driven web browser built with GTK4 and WebKitGTK.`,
19+ Short: "A fully unfeatured unbloated browser for tiling WMs",
20+ Long: `Dumber - a dumb browser that works like your favorite terminal multiplexer.
21+
22+A fully unfeatured unbloated browser for tiling WMs, built with GTK4 and WebKitGTK.
23+
24+Features:
25+ - Wayland native (Sway, Hyprland, River, Niri, etc.)
26+ - Tabs and workspaces with split or stacked panes
27+ - Keyboard-driven workflow inspired by Zellij
28+ - GPU rendering with automatic VA-API/VDPAU detection
29+ - Built-in ad blocking (UBlock-based network + cosmetic filtering)
30+ - Launcher integration (rofi/fuzzel) with favicons
31+ - Search shortcuts via bangs (!g, !gi, etc.)
32+ - Session management with auto-save and restore
33+
34+Use 'dumber browse' to launch the graphical browser, or explore the
35+subcommands for CLI-based operations like history search and session
36+management.`,
2137 PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
2238 // Skip initialization for help commands
2339 if cmd.Name() == "help" || cmd.Name() == "completion" {
@@ -16,8 +16,24 @@ var (
16 buildInfo build.Info16 buildInfo build.Info
17 rootCmd = &cobra.Command{17 rootCmd = &cobra.Command{
18 Use: "dumber",18 Use: "dumber",
19- Short: "A minimal web browser",19+ Short: "A fully unfeatured unbloated browser for tiling WMs",
20- Long: `Dumber is a minimal, keyboard-driven web browser built with GTK4 and WebKitGTK.`,20+ Long: `Dumber - a dumb browser that works like your favorite terminal multiplexer.
21+
22+A fully unfeatured unbloated browser for tiling WMs, built with GTK4 and WebKitGTK.
23+
24+Features:
25+ - Wayland native (Sway, Hyprland, River, Niri, etc.)
26+ - Tabs and workspaces with split or stacked panes
27+ - Keyboard-driven workflow inspired by Zellij
28+ - GPU rendering with automatic VA-API/VDPAU detection
29+ - Built-in ad blocking (UBlock-based network + cosmetic filtering)
30+ - Launcher integration (rofi/fuzzel) with favicons
31+ - Search shortcuts via bangs (!g, !gi, etc.)
32+ - Session management with auto-save and restore
33+
34+Use 'dumber browse' to launch the graphical browser, or explore the
35+subcommands for CLI-based operations like history search and session
36+management.`,
21 PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {37 PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
22 // Skip initialization for help commands38 // Skip initialization for help commands
23 if cmd.Name() == "help" || cmd.Name() == "completion" {39 if cmd.Name() == "help" || cmd.Name() == "completion" {
M internal/infrastructure/xdg/adapter.go +17 -0
@@ -1,6 +1,7 @@
11 package xdg
22
33 import (
4+ "os"
45 "path/filepath"
56
67 "github.com/bnema/dumber/internal/application/port"
@@ -55,4 +56,20 @@ func (a *Adapter) FilterCacheDir() (string, error) {
5556 return config.GetFilterCacheDir()
5657 }
5758
59+func (a *Adapter) ManDir() (string, error) {
60+ // Man pages go to the user's XDG_DATA_HOME/man/man1, not the dumber-specific dir.
61+ // This allows 'man dumber' to work without custom MANPATH configuration.
62+ home, err := os.UserHomeDir()
63+ if err != nil {
64+ return "", err
65+ }
66+
67+ dataHome := os.Getenv("XDG_DATA_HOME")
68+ if dataHome == "" {
69+ dataHome = filepath.Join(home, ".local", "share")
70+ }
71+
72+ return filepath.Join(dataHome, "man", "man1"), nil
73+}
74+
5875 var _ port.XDGPaths = (*Adapter)(nil)
@@ -1,6 +1,7 @@
1 package xdg1 package xdg
2 2
3 import (3 import (
4+ "os"
4 "path/filepath"5 "path/filepath"
5 6
6 "github.com/bnema/dumber/internal/application/port"7 "github.com/bnema/dumber/internal/application/port"
@@ -55,4 +56,20 @@ func (a *Adapter) FilterCacheDir() (string, error) {
55 return config.GetFilterCacheDir()56 return config.GetFilterCacheDir()
56 }57 }
57 58
59+func (a *Adapter) ManDir() (string, error) {
60+ // Man pages go to the user's XDG_DATA_HOME/man/man1, not the dumber-specific dir.
61+ // This allows 'man dumber' to work without custom MANPATH configuration.
62+ home, err := os.UserHomeDir()
63+ if err != nil {
64+ return "", err
65+ }
66+
67+ dataHome := os.Getenv("XDG_DATA_HOME")
68+ if dataHome == "" {
69+ dataHome = filepath.Join(home, ".local", "share")
70+ }
71+
72+ return filepath.Join(dataHome, "man", "man1"), nil
73+}
74+
58 var _ port.XDGPaths = (*Adapter)(nil)75 var _ port.XDGPaths = (*Adapter)(nil)

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close