detached e879142

cli: prevent duplicate Cobra error output for styled failures

commit
e879142703019b7459d6174aeecc091ace1503b9
author
bnema · · 2026-02-11 19:43 UTC
parent
2d9aebe
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +47 -15
M internal/cli/cmd/crashes.go +4 -4
@@ -101,13 +101,13 @@ func runCrashesShow(_ *cobra.Command, args []string) error {
101101 report, err := resolveCrashReport(getCrashReportsDir(app.Config), args[0])
102102 if err != nil {
103103 _, _ = fmt.Fprintf(os.Stderr, "%s\n%s\n", renderer.RenderError(err), renderer.RenderHintList())
104- return fmt.Errorf("resolve crash report: %w", err)
104+ return wrapPrintedError(fmt.Errorf("resolve crash report: %w", err))
105105 }
106106
107107 body, err := os.ReadFile(report.MarkdownPath)
108108 if err != nil {
109109 _, _ = fmt.Fprintf(os.Stderr, "%s\n", renderer.RenderError(fmt.Errorf("read crash report markdown: %w", err)))
110- return fmt.Errorf("read crash report markdown: %w", err)
110+ return wrapPrintedError(fmt.Errorf("read crash report markdown: %w", err))
111111 }
112112 fmt.Print(string(body))
113113 return nil
@@ -123,13 +123,13 @@ func runCrashesIssue(_ *cobra.Command, args []string) error {
123123 report, err := resolveCrashReport(getCrashReportsDir(app.Config), args[0])
124124 if err != nil {
125125 _, _ = fmt.Fprintf(os.Stderr, "%s\n%s\n", renderer.RenderError(err), renderer.RenderHintList())
126- return fmt.Errorf("resolve crash report: %w", err)
126+ return wrapPrintedError(fmt.Errorf("resolve crash report: %w", err))
127127 }
128128
129129 body, err := os.ReadFile(report.MarkdownPath)
130130 if err != nil {
131131 _, _ = fmt.Fprintf(os.Stderr, "%s\n", renderer.RenderError(fmt.Errorf("read crash report markdown: %w", err)))
132- return fmt.Errorf("read crash report markdown: %w", err)
132+ return wrapPrintedError(fmt.Errorf("read crash report markdown: %w", err))
133133 }
134134 all := string(body)
135135 start := strings.Index(all, "## GitHub Issue Template")
@@ -101,13 +101,13 @@ func runCrashesShow(_ *cobra.Command, args []string) error {
101 report, err := resolveCrashReport(getCrashReportsDir(app.Config), args[0])101 report, err := resolveCrashReport(getCrashReportsDir(app.Config), args[0])
102 if err != nil {102 if err != nil {
103 _, _ = fmt.Fprintf(os.Stderr, "%s\n%s\n", renderer.RenderError(err), renderer.RenderHintList())103 _, _ = fmt.Fprintf(os.Stderr, "%s\n%s\n", renderer.RenderError(err), renderer.RenderHintList())
104- return fmt.Errorf("resolve crash report: %w", err)104+ return wrapPrintedError(fmt.Errorf("resolve crash report: %w", err))
105 }105 }
106 106
107 body, err := os.ReadFile(report.MarkdownPath)107 body, err := os.ReadFile(report.MarkdownPath)
108 if err != nil {108 if err != nil {
109 _, _ = fmt.Fprintf(os.Stderr, "%s\n", renderer.RenderError(fmt.Errorf("read crash report markdown: %w", err)))109 _, _ = fmt.Fprintf(os.Stderr, "%s\n", renderer.RenderError(fmt.Errorf("read crash report markdown: %w", err)))
110- return fmt.Errorf("read crash report markdown: %w", err)110+ return wrapPrintedError(fmt.Errorf("read crash report markdown: %w", err))
111 }111 }
112 fmt.Print(string(body))112 fmt.Print(string(body))
113 return nil113 return nil
@@ -123,13 +123,13 @@ func runCrashesIssue(_ *cobra.Command, args []string) error {
123 report, err := resolveCrashReport(getCrashReportsDir(app.Config), args[0])123 report, err := resolveCrashReport(getCrashReportsDir(app.Config), args[0])
124 if err != nil {124 if err != nil {
125 _, _ = fmt.Fprintf(os.Stderr, "%s\n%s\n", renderer.RenderError(err), renderer.RenderHintList())125 _, _ = fmt.Fprintf(os.Stderr, "%s\n%s\n", renderer.RenderError(err), renderer.RenderHintList())
126- return fmt.Errorf("resolve crash report: %w", err)126+ return wrapPrintedError(fmt.Errorf("resolve crash report: %w", err))
127 }127 }
128 128
129 body, err := os.ReadFile(report.MarkdownPath)129 body, err := os.ReadFile(report.MarkdownPath)
130 if err != nil {130 if err != nil {
131 _, _ = fmt.Fprintf(os.Stderr, "%s\n", renderer.RenderError(fmt.Errorf("read crash report markdown: %w", err)))131 _, _ = fmt.Fprintf(os.Stderr, "%s\n", renderer.RenderError(fmt.Errorf("read crash report markdown: %w", err)))
132- return fmt.Errorf("read crash report markdown: %w", err)132+ return wrapPrintedError(fmt.Errorf("read crash report markdown: %w", err))
133 }133 }
134 all := string(body)134 all := string(body)
135 start := strings.Index(all, "## GitHub Issue Template")135 start := strings.Index(all, "## GitHub Issue Template")
M internal/cli/cmd/root.go +34 -2
@@ -2,6 +2,7 @@
22 package cmd
33
44 import (
5+ "errors"
56 "fmt"
67 "os"
78
@@ -15,8 +16,10 @@ var (
1516 app *cli.App
1617 buildInfo build.Info
1718 rootCmd = &cobra.Command{
18- Use: "dumber",
19- Short: "A fully unfeatured unbloated browser for tiling WMs",
19+ Use: "dumber",
20+ Short: "A fully unfeatured unbloated browser for tiling WMs",
21+ SilenceErrors: true,
22+ SilenceUsage: true,
2023 Long: `Dumber - a dumb browser that works like your favorite terminal multiplexer.
2124
2225 A fully unfeatured unbloated browser for tiling WMs, built with GTK4 and WebKitGTK.
@@ -61,11 +64,40 @@ management.`,
6164 // Execute runs the root command.
6265 func Execute() {
6366 if err := rootCmd.Execute(); err != nil {
67+ var printedErr *printedError
68+ if errors.As(err, &printedErr) {
69+ os.Exit(1)
70+ }
6471 fmt.Fprintln(os.Stderr, err)
6572 os.Exit(1)
6673 }
6774 }
6875
76+type printedError struct {
77+ err error
78+}
79+
80+func (e *printedError) Error() string {
81+ if e == nil || e.err == nil {
82+ return ""
83+ }
84+ return e.err.Error()
85+}
86+
87+func (e *printedError) Unwrap() error {
88+ if e == nil {
89+ return nil
90+ }
91+ return e.err
92+}
93+
94+func wrapPrintedError(err error) error {
95+ if err == nil {
96+ return nil
97+ }
98+ return &printedError{err: err}
99+}
100+
69101 // GetApp returns the initialized app (for use by subcommands).
70102 func GetApp() *cli.App {
71103 return app
@@ -2,6 +2,7 @@
2 package cmd2 package cmd
3 3
4 import (4 import (
5+ "errors"
5 "fmt"6 "fmt"
6 "os"7 "os"
7 8
@@ -15,8 +16,10 @@ var (
15 app *cli.App16 app *cli.App
16 buildInfo build.Info17 buildInfo build.Info
17 rootCmd = &cobra.Command{18 rootCmd = &cobra.Command{
18- Use: "dumber",19+ Use: "dumber",
19- Short: "A fully unfeatured unbloated browser for tiling WMs",20+ Short: "A fully unfeatured unbloated browser for tiling WMs",
21+ SilenceErrors: true,
22+ SilenceUsage: true,
20 Long: `Dumber - a dumb browser that works like your favorite terminal multiplexer.23 Long: `Dumber - a dumb browser that works like your favorite terminal multiplexer.
21 24
22 A fully unfeatured unbloated browser for tiling WMs, built with GTK4 and WebKitGTK.25 A fully unfeatured unbloated browser for tiling WMs, built with GTK4 and WebKitGTK.
@@ -61,11 +64,40 @@ management.`,
61 // Execute runs the root command.64 // Execute runs the root command.
62 func Execute() {65 func Execute() {
63 if err := rootCmd.Execute(); err != nil {66 if err := rootCmd.Execute(); err != nil {
67+ var printedErr *printedError
68+ if errors.As(err, &printedErr) {
69+ os.Exit(1)
70+ }
64 fmt.Fprintln(os.Stderr, err)71 fmt.Fprintln(os.Stderr, err)
65 os.Exit(1)72 os.Exit(1)
66 }73 }
67 }74 }
68 75
76+type printedError struct {
77+ err error
78+}
79+
80+func (e *printedError) Error() string {
81+ if e == nil || e.err == nil {
82+ return ""
83+ }
84+ return e.err.Error()
85+}
86+
87+func (e *printedError) Unwrap() error {
88+ if e == nil {
89+ return nil
90+ }
91+ return e.err
92+}
93+
94+func wrapPrintedError(err error) error {
95+ if err == nil {
96+ return nil
97+ }
98+ return &printedError{err: err}
99+}
100+
69 // GetApp returns the initialized app (for use by subcommands).101 // GetApp returns the initialized app (for use by subcommands).
70 func GetApp() *cli.App {102 func GetApp() *cli.App {
71 return app103 return app
M internal/cli/cmd/sessions.go +9 -9
@@ -98,7 +98,7 @@ func runSessionsList(_ *cobra.Command, _ []string) error {
9898 if app.ListSessionsUC == nil {
9999 err := fmt.Errorf("session management not available")
100100 fmt.Fprintln(os.Stderr, renderer.RenderError(err))
101- return err
101+ return wrapPrintedError(err)
102102 }
103103
104104 // Get current session ID (empty if not running as browser)
@@ -111,7 +111,7 @@ func runSessionsList(_ *cobra.Command, _ []string) error {
111111 if err != nil {
112112 wrappedErr := fmt.Errorf("list sessions: %w", err)
113113 fmt.Fprintln(os.Stderr, renderer.RenderError(wrappedErr))
114- return wrappedErr
114+ return wrapPrintedError(wrappedErr)
115115 }
116116
117117 if sessionsJSON {
@@ -160,14 +160,14 @@ func runSessionsRestore(_ *cobra.Command, args []string) error {
160160 if app.RestoreUC == nil || app.ListSessionsUC == nil {
161161 err := fmt.Errorf("session restoration not available")
162162 fmt.Fprintln(os.Stderr, renderer.RenderError(err))
163- return err
163+ return wrapPrintedError(err)
164164 }
165165
166166 // Find session by ID or suffix
167167 sessionInfo, err := findSessionByIDOrSuffix(args[0])
168168 if err != nil {
169169 fmt.Fprintln(os.Stderr, renderer.RenderError(err))
170- return err
170+ return wrapPrintedError(err)
171171 }
172172
173173 sessionID := sessionInfo.Session.ID
@@ -177,7 +177,7 @@ func runSessionsRestore(_ *cobra.Command, args []string) error {
177177 if err != nil {
178178 wrappedErr := fmt.Errorf("restore session: %w", err)
179179 fmt.Fprintln(os.Stderr, renderer.RenderError(wrappedErr))
180- return wrappedErr
180+ return wrapPrintedError(wrappedErr)
181181 }
182182
183183 // Spawn a new dumber instance with the session
@@ -185,7 +185,7 @@ func runSessionsRestore(_ *cobra.Command, args []string) error {
185185 if err := spawner.SpawnWithSession(sessionID); err != nil {
186186 wrappedErr := fmt.Errorf("spawn browser: %w", err)
187187 fmt.Fprintln(os.Stderr, renderer.RenderError(wrappedErr))
188- return wrappedErr
188+ return wrapPrintedError(wrappedErr)
189189 }
190190
191191 fmt.Println(renderer.RenderRestoreStarted(sessionID))
@@ -224,14 +224,14 @@ func runSessionsDelete(_ *cobra.Command, args []string) error {
224224 if app.DeleteSessionUC == nil || app.ListSessionsUC == nil {
225225 err := fmt.Errorf("session management not available")
226226 fmt.Fprintln(os.Stderr, renderer.RenderError(err))
227- return err
227+ return wrapPrintedError(err)
228228 }
229229
230230 // Find session by ID or suffix
231231 sessionInfo, err := findSessionByIDOrSuffix(args[0])
232232 if err != nil {
233233 fmt.Fprintln(os.Stderr, renderer.RenderError(err))
234- return err
234+ return wrapPrintedError(err)
235235 }
236236
237237 // Get current session ID
@@ -247,7 +247,7 @@ func runSessionsDelete(_ *cobra.Command, args []string) error {
247247 }); err != nil {
248248 wrappedErr := fmt.Errorf("delete session: %w", err)
249249 fmt.Fprintln(os.Stderr, renderer.RenderError(wrappedErr))
250- return wrappedErr
250+ return wrapPrintedError(wrappedErr)
251251 }
252252
253253 fmt.Println(renderer.RenderDeleted(sessionInfo.Session.ID))
@@ -98,7 +98,7 @@ func runSessionsList(_ *cobra.Command, _ []string) error {
98 if app.ListSessionsUC == nil {98 if app.ListSessionsUC == nil {
99 err := fmt.Errorf("session management not available")99 err := fmt.Errorf("session management not available")
100 fmt.Fprintln(os.Stderr, renderer.RenderError(err))100 fmt.Fprintln(os.Stderr, renderer.RenderError(err))
101- return err101+ return wrapPrintedError(err)
102 }102 }
103 103
104 // Get current session ID (empty if not running as browser)104 // Get current session ID (empty if not running as browser)
@@ -111,7 +111,7 @@ func runSessionsList(_ *cobra.Command, _ []string) error {
111 if err != nil {111 if err != nil {
112 wrappedErr := fmt.Errorf("list sessions: %w", err)112 wrappedErr := fmt.Errorf("list sessions: %w", err)
113 fmt.Fprintln(os.Stderr, renderer.RenderError(wrappedErr))113 fmt.Fprintln(os.Stderr, renderer.RenderError(wrappedErr))
114- return wrappedErr114+ return wrapPrintedError(wrappedErr)
115 }115 }
116 116
117 if sessionsJSON {117 if sessionsJSON {
@@ -160,14 +160,14 @@ func runSessionsRestore(_ *cobra.Command, args []string) error {
160 if app.RestoreUC == nil || app.ListSessionsUC == nil {160 if app.RestoreUC == nil || app.ListSessionsUC == nil {
161 err := fmt.Errorf("session restoration not available")161 err := fmt.Errorf("session restoration not available")
162 fmt.Fprintln(os.Stderr, renderer.RenderError(err))162 fmt.Fprintln(os.Stderr, renderer.RenderError(err))
163- return err163+ return wrapPrintedError(err)
164 }164 }
165 165
166 // Find session by ID or suffix166 // Find session by ID or suffix
167 sessionInfo, err := findSessionByIDOrSuffix(args[0])167 sessionInfo, err := findSessionByIDOrSuffix(args[0])
168 if err != nil {168 if err != nil {
169 fmt.Fprintln(os.Stderr, renderer.RenderError(err))169 fmt.Fprintln(os.Stderr, renderer.RenderError(err))
170- return err170+ return wrapPrintedError(err)
171 }171 }
172 172
173 sessionID := sessionInfo.Session.ID173 sessionID := sessionInfo.Session.ID
@@ -177,7 +177,7 @@ func runSessionsRestore(_ *cobra.Command, args []string) error {
177 if err != nil {177 if err != nil {
178 wrappedErr := fmt.Errorf("restore session: %w", err)178 wrappedErr := fmt.Errorf("restore session: %w", err)
179 fmt.Fprintln(os.Stderr, renderer.RenderError(wrappedErr))179 fmt.Fprintln(os.Stderr, renderer.RenderError(wrappedErr))
180- return wrappedErr180+ return wrapPrintedError(wrappedErr)
181 }181 }
182 182
183 // Spawn a new dumber instance with the session183 // Spawn a new dumber instance with the session
@@ -185,7 +185,7 @@ func runSessionsRestore(_ *cobra.Command, args []string) error {
185 if err := spawner.SpawnWithSession(sessionID); err != nil {185 if err := spawner.SpawnWithSession(sessionID); err != nil {
186 wrappedErr := fmt.Errorf("spawn browser: %w", err)186 wrappedErr := fmt.Errorf("spawn browser: %w", err)
187 fmt.Fprintln(os.Stderr, renderer.RenderError(wrappedErr))187 fmt.Fprintln(os.Stderr, renderer.RenderError(wrappedErr))
188- return wrappedErr188+ return wrapPrintedError(wrappedErr)
189 }189 }
190 190
191 fmt.Println(renderer.RenderRestoreStarted(sessionID))191 fmt.Println(renderer.RenderRestoreStarted(sessionID))
@@ -224,14 +224,14 @@ func runSessionsDelete(_ *cobra.Command, args []string) error {
224 if app.DeleteSessionUC == nil || app.ListSessionsUC == nil {224 if app.DeleteSessionUC == nil || app.ListSessionsUC == nil {
225 err := fmt.Errorf("session management not available")225 err := fmt.Errorf("session management not available")
226 fmt.Fprintln(os.Stderr, renderer.RenderError(err))226 fmt.Fprintln(os.Stderr, renderer.RenderError(err))
227- return err227+ return wrapPrintedError(err)
228 }228 }
229 229
230 // Find session by ID or suffix230 // Find session by ID or suffix
231 sessionInfo, err := findSessionByIDOrSuffix(args[0])231 sessionInfo, err := findSessionByIDOrSuffix(args[0])
232 if err != nil {232 if err != nil {
233 fmt.Fprintln(os.Stderr, renderer.RenderError(err))233 fmt.Fprintln(os.Stderr, renderer.RenderError(err))
234- return err234+ return wrapPrintedError(err)
235 }235 }
236 236
237 // Get current session ID237 // Get current session ID
@@ -247,7 +247,7 @@ func runSessionsDelete(_ *cobra.Command, args []string) error {
247 }); err != nil {247 }); err != nil {
248 wrappedErr := fmt.Errorf("delete session: %w", err)248 wrappedErr := fmt.Errorf("delete session: %w", err)
249 fmt.Fprintln(os.Stderr, renderer.RenderError(wrappedErr))249 fmt.Fprintln(os.Stderr, renderer.RenderError(wrappedErr))
250- return wrappedErr250+ return wrapPrintedError(wrappedErr)
251 }251 }
252 252
253 fmt.Println(renderer.RenderDeleted(sessionInfo.Session.ID))253 fmt.Println(renderer.RenderDeleted(sessionInfo.Session.ID))

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