1// Package cmd provides Cobra CLI commands for dumber.
2package cmd
3
4import (
5 "errors"
6 "fmt"
7 "os"
8 "strings"
9
10 "github.com/spf13/cobra"
11
12 "github.com/bnema/dumber/internal/cli"
13 "github.com/bnema/dumber/internal/domain/build"
14)
15
16var (
17 app *cli.App
18 buildInfo build.Info
19 rootCmd = &cobra.Command{
20 Use: "dumber",
21 Short: "A fully unfeatured unbloated browser for tiling WMs",
22 SilenceErrors: true,
23 SilenceUsage: true,
24 Long: `Dumber - a dumb browser that works like your favorite terminal multiplexer.
25
26A fully unfeatured unbloated browser for tiling WMs, built with GTK4. CEF is the default backend; WebKitGTK is available as a fallback.
27
28Features:
29 - Wayland native (Sway, Hyprland, River, Niri, etc.)
30 - Tabs and workspaces with split or stacked panes
31 - Keyboard-driven workflow inspired by Zellij
32 - GPU rendering with automatic VA-API/VDPAU detection
33 - Built-in ad blocking (UBlock-based network + cosmetic filtering)
34 - Launcher integration (rofi/fuzzel) with favicons
35 - Search shortcuts via bangs (!g, !gi, etc.)
36 - Session management with auto-save and restore
37
38Use 'dumber browse' to launch the graphical browser, 'dumber omnibox' to open
39the standalone launcher overlay, or explore the subcommands for CLI-based
40operations like history search and session management.`,
41 PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
42 // Skip initialization for commands that don't need app context
43 switch cmd.Name() {
44 case "help", "completion", "gen-docs":
45 return nil
46 }
47
48 var err error
49 app, err = cli.NewApp()
50 if err != nil {
51 return fmt.Errorf("initialize app: %w", err)
52 }
53 // Set build info from main.go
54 app.BuildInfo = buildInfo
55 return nil
56 },
57 PersistentPostRun: func(_ *cobra.Command, _ []string) {
58 if app != nil {
59 _ = app.Close()
60 }
61 },
62 }
63)
64
65// Execute runs the root command.
66func Execute() {
67 if err := rootCmd.Execute(); err != nil {
68 var printedErr *printedError
69 if errors.As(err, &printedErr) {
70 os.Exit(1)
71 }
72 fmt.Fprintln(os.Stderr, err)
73 os.Exit(1)
74 }
75}
76
77type printedError struct {
78 err error
79}
80
81func (e *printedError) Error() string {
82 if e == nil || e.err == nil {
83 return ""
84 }
85 return e.err.Error()
86}
87
88func (e *printedError) Unwrap() error {
89 if e == nil {
90 return nil
91 }
92 return e.err
93}
94
95func wrapPrintedError(err error) error {
96 if err == nil {
97 return nil
98 }
99 return &printedError{err: err}
100}
101
102// GetApp returns the initialized app (for use by subcommands).
103func GetApp() *cli.App {
104 return app
105}
106
107// browseCmd is a placeholder for help - actual execution is in main.go
108var browseCmd = &cobra.Command{
109 Use: "browse [url]",
110 Short: "Launch the graphical browser",
111 Args: cobra.MaximumNArgs(1),
112 Long: `Launch the GTK4 graphical browser.
113
114If a URL is provided, navigate to it. Otherwise, open the homepage.
115
116With --instance <name> (CEF only), Dumber opens or focuses one dedicated
117window inside the current CEF profile host. Named instances share cookies,
118tokens, local storage, configuration, favorites and history by default.
119
120Use --profile <name> for a separate persistent CEF profile and host process.
121Use --ephemeral for a fresh temporary CEF profile removed when its host exits.
122--profile and --ephemeral are mutually exclusive.
123
124Examples:
125 dumber browse # Open browser normally
126 dumber browse example.com # Open browser to URL
127 dumber browse --instance scratch example.com # Dedicated shared-profile window
128 dumber browse --instance work --profile work # Isolated persistent profile
129 dumber browse --instance private --ephemeral # Temporary isolated profile`,
130 PreRunE: func(command *cobra.Command, _ []string) error {
131 instance, _ := command.Flags().GetString("instance")
132 profile, _ := command.Flags().GetString("profile")
133 ephemeral, _ := command.Flags().GetBool("ephemeral")
134 if command.Flags().Changed("instance") && (instance == "" || strings.HasPrefix(instance, "-")) {
135 return fmt.Errorf("--instance must not be empty or start with '-'")
136 }
137 if command.Flags().Changed("profile") && (profile == "" || strings.HasPrefix(profile, "-")) {
138 return fmt.Errorf("--profile must not be empty or start with '-'")
139 }
140 if profile != "" && ephemeral {
141 return fmt.Errorf("--profile and --ephemeral are mutually exclusive")
142 }
143 return nil
144 },
145 Run: func(_ *cobra.Command, _ []string) {
146 // Valid GUI launches are handled by main.go before Cobra runs.
147 },
148}
149
150func init() {
151 browseCmd.Flags().AddFlagSet(browseFlags())
152 rootCmd.AddCommand(browseCmd)
153}
154
155// SetBuildInfo sets the build information (called from main.go before Execute).
156func SetBuildInfo(info build.Info) {
157 buildInfo = info
158}