From b627d2e4abebef5345d7c06b2e95e9231d971357 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 12 Oct 2025 13:51:16 +0530 Subject: [PATCH] Move error handling code into its own library --- go.mod | 1 + go.sum | 2 + kittens/choose_files/image_preview.go | 7 ++- kittens/choose_files/preview.go | 5 +- kittens/choose_files/save-file.go | 4 +- kittens/choose_files/scan.go | 9 ++-- tools/fzf/algo.go | 5 +- tools/highlight/impl.go | 4 +- tools/tui/loop/api.go | 11 ++-- tools/tui/loop/read.go | 7 +-- tools/tui/loop/write.go | 4 +- tools/tui/subseq/score.go | 4 +- tools/utils/images/utils.go | 5 +- tools/utils/misc.go | 74 --------------------------- 14 files changed, 35 insertions(+), 107 deletions(-) diff --git a/go.mod b/go.mod index 009321e06..784ec1471 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/google/uuid v1.6.0 github.com/kovidgoyal/dbus v0.0.0-20250519011319-e811c41c0bc1 github.com/kovidgoyal/exiffix v0.0.0-20250919160812-dbef770c2032 + github.com/kovidgoyal/go-parallel v1.0.1 github.com/kovidgoyal/imaging v1.7.1 github.com/seancfoley/ipaddress-go v1.7.1 github.com/shirou/gopsutil/v3 v3.24.5 diff --git a/go.sum b/go.sum index 14b5ad794..10ded8d6f 100644 --- a/go.sum +++ b/go.sum @@ -28,6 +28,8 @@ github.com/kovidgoyal/dbus v0.0.0-20250519011319-e811c41c0bc1 h1:rMY/hWfcVzBm6BL github.com/kovidgoyal/dbus v0.0.0-20250519011319-e811c41c0bc1/go.mod h1:RbNG3Q1g6GUy1/WzWVx+S24m7VKyvl57vV+cr2hpt50= github.com/kovidgoyal/exiffix v0.0.0-20250919160812-dbef770c2032 h1:TEV9lpo2a6fP1byiDsoJe2fXpvrj2itae41xMM+bEAg= github.com/kovidgoyal/exiffix v0.0.0-20250919160812-dbef770c2032/go.mod h1:VU38Nlbvb0lbyS5YkopCZMS5HuJ5QLVJBxRWyzq79q4= +github.com/kovidgoyal/go-parallel v1.0.1 h1:nYUjN+EdpbmQjTg3N5eTUInuXTB3/1oD2vHdaMfuHoI= +github.com/kovidgoyal/go-parallel v1.0.1/go.mod h1:BJNIbe6+hxyFWv7n6oEDPj3PA5qSw5OCtf0hcVxWJiw= github.com/kovidgoyal/imaging v1.7.1 h1:wQJf0LdE06kIlSFIFFBKJp9N4D0CJmNMfWuog+YQka4= github.com/kovidgoyal/imaging v1.7.1/go.mod h1:i0dBmu7j5/g4QhvS79Cx9IoiK7P02z1SwBJFDazrGLE= github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a h1:N9zuLhTvBSRt0gWSiJswwQ2HqDmtX/ZCDJURnKUt1Ik= diff --git a/kittens/choose_files/image_preview.go b/kittens/choose_files/image_preview.go index 101a3f7d4..d95943eb4 100644 --- a/kittens/choose_files/image_preview.go +++ b/kittens/choose_files/image_preview.go @@ -10,6 +10,7 @@ import ( "sync" "sync/atomic" + "github.com/kovidgoyal/go-parallel" "github.com/kovidgoyal/kitty/tools/disk_cache" "github.com/kovidgoyal/kitty/tools/icons" "github.com/kovidgoyal/kitty/tools/utils" @@ -114,8 +115,7 @@ func (p *ImagePreview) ensure_source_image() (err error) { func (p *ImagePreview) render_image(h *Handler, x, y, width, height int) { defer func() { if r := recover(); r != nil { - text, _ := utils.Format_stacktrace_on_panic(r) - h.err_chan <- fmt.Errorf("%s", text) + h.err_chan <- parallel.Format_stacktrace_on_panic(r, 1) p.WakeupMainThread() } }() @@ -160,8 +160,7 @@ func (p *ImagePreview) Render(h *Handler, x, y, width, height int) { func (p *ImagePreview) start_rendering() { defer func() { if r := recover(); r != nil { - text, _ := utils.Format_stacktrace_on_panic(r) - p.render_channel <- render_data{err: fmt.Errorf("%s", text)} + p.render_channel <- render_data{err: parallel.Format_stacktrace_on_panic(r, 1)} } close(p.render_channel) p.WakeupMainThread() diff --git a/kittens/choose_files/preview.go b/kittens/choose_files/preview.go index 76ffbe578..69393c633 100644 --- a/kittens/choose_files/preview.go +++ b/kittens/choose_files/preview.go @@ -11,6 +11,7 @@ import ( "sync" "unicode/utf8" + "github.com/kovidgoyal/go-parallel" "github.com/kovidgoyal/kitty/tools/highlight" "github.com/kovidgoyal/kitty/tools/icons" "github.com/kovidgoyal/kitty/tools/tui/loop" @@ -261,8 +262,8 @@ func (pm *PreviewManager) highlight_file_async(path string, output chan highligh go func() { defer func() { if r := recover(); r != nil { - text, _ := utils.Format_stacktrace_on_panic(r) - debugprintln(fmt.Sprintf("Failed to highlight: %s with panic: %s", path, text)) + err := parallel.Format_stacktrace_on_panic(r, 1) + debugprintln(fmt.Sprintf("Failed to highlight: %s with panic: %s", path, err)) } close(output) pm.WakeupMainThread() diff --git a/kittens/choose_files/save-file.go b/kittens/choose_files/save-file.go index 67816f366..42c6480c7 100644 --- a/kittens/choose_files/save-file.go +++ b/kittens/choose_files/save-file.go @@ -6,6 +6,7 @@ import ( "path/filepath" "strings" + "github.com/kovidgoyal/go-parallel" "github.com/kovidgoyal/kitty/tools/cli" "github.com/kovidgoyal/kitty/tools/tui/loop" "github.com/kovidgoyal/kitty/tools/utils" @@ -26,8 +27,7 @@ func FilePromptCompleter(getcwd func() string) func(string, string) *cli.Complet return func(before_cursor, after_cursor string) (ans *cli.Completions) { defer func() { if r := recover(); r != nil { - text, err := utils.Format_stacktrace_on_panic(r) - debugprintln(text) + err := parallel.Format_stacktrace_on_panic(r, 1) debugprintln(err) } }() diff --git a/kittens/choose_files/scan.go b/kittens/choose_files/scan.go index 5cc8582c9..7e86fa89a 100644 --- a/kittens/choose_files/scan.go +++ b/kittens/choose_files/scan.go @@ -18,6 +18,7 @@ import ( "unicode" "unicode/utf8" + "github.com/kovidgoyal/go-parallel" "github.com/kovidgoyal/kitty/tools/fzf" "github.com/kovidgoyal/kitty/tools/ignorefiles" "github.com/kovidgoyal/kitty/tools/utils" @@ -219,8 +220,8 @@ func (fss *FileSystemScanner) worker() { defer fss.unlock() fss.in_progress.Store(false) if r := recover(); r != nil { - st, qerr := utils.Format_stacktrace_on_panic(r) - fss.err = fmt.Errorf("%w\n%s", qerr, st) + qerr := parallel.Format_stacktrace_on_panic(r, 1) + fss.err = qerr } for _, l := range fss.listeners { close(l) @@ -532,8 +533,8 @@ func (fss *FileSystemScorer) worker(on_results chan bool, worker_wait *sync.Wait defer worker_wait.Done() if r := recover(); r != nil { if fss.keep_going.Load() { - st, qerr := utils.Format_stacktrace_on_panic(r) - fss.on_results(fmt.Errorf("%w\n%s", qerr, st), true) + qerr := parallel.Format_stacktrace_on_panic(r, 1) + fss.on_results(qerr, true) } } else { if fss.keep_going.Load() { diff --git a/tools/fzf/algo.go b/tools/fzf/algo.go index ca3c29917..4dad1073f 100644 --- a/tools/fzf/algo.go +++ b/tools/fzf/algo.go @@ -8,7 +8,7 @@ import ( "unicode" "unicode/utf8" - "github.com/kovidgoyal/kitty/tools/utils" + "github.com/kovidgoyal/go-parallel" "golang.org/x/text/unicode/norm" ) @@ -362,12 +362,11 @@ func (m *FuzzyMatcher) score(items []string, pattern string, scoring_func func(s pat := []rune(pattern) pattern_is_ascii := !slices.ContainsFunc(pat, func(r rune) bool { return r >= utf8.RuneSelf }) ans = make([]Result, len(items)) - err = utils.Run_in_parallel_over_range(0, func(start, end int) error { + err = parallel.Run_in_parallel_over_range(0, func(start, end int) { s := slab{} for i := start; i < end; i++ { ans[i] = scoring_func(items[i], pat, pattern_is_ascii, &s, as_chars) } - return nil }, 0, len(items)) return diff --git a/tools/highlight/impl.go b/tools/highlight/impl.go index 7e6af8ab4..3a2da413b 100644 --- a/tools/highlight/impl.go +++ b/tools/highlight/impl.go @@ -10,6 +10,7 @@ import ( "github.com/alecthomas/chroma/v2" "github.com/alecthomas/chroma/v2/lexers" "github.com/alecthomas/chroma/v2/styles" + "github.com/kovidgoyal/go-parallel" "github.com/kovidgoyal/kitty/tools/utils" ) @@ -185,8 +186,7 @@ func (h *highlighter) Sanitize(x string) string { return h.sanitize(x) } func (h *highlighter) HighlightFile(path string, srd StyleResolveData) (highlighted_string string, err error) { defer func() { if r := recover(); r != nil { - text, _ := utils.Format_stacktrace_on_panic(r) - err = fmt.Errorf("%s", text) + err = parallel.Format_stacktrace_on_panic(r, 1) } }() filename_for_detection := filepath.Base(path) diff --git a/tools/tui/loop/api.go b/tools/tui/loop/api.go index 4e0b234a7..3743f2d80 100644 --- a/tools/tui/loop/api.go +++ b/tools/tui/loop/api.go @@ -12,6 +12,7 @@ import ( "golang.org/x/sys/unix" + "github.com/kovidgoyal/go-parallel" "github.com/kovidgoyal/kitty/tools/tty" "github.com/kovidgoyal/kitty/tools/utils" "github.com/kovidgoyal/kitty/tools/utils/style" @@ -324,18 +325,17 @@ func (self *Loop) DebugPrintln(args ...any) { func (self *Loop) Run() (err error) { defer func() { if r := recover(); r != nil { - var text string - text, err = utils.Format_stacktrace_on_panic(r) + err = parallel.Format_stacktrace_on_panic(r, 1) is_terminal := tty.IsTerminal(os.Stderr.Fd()) if is_terminal { os.Stderr.WriteString("\x1b]\x1b\\\x1bc\x1b[H\x1b[2J") // reset terminal } - os.Stderr.WriteString(text) + os.Stderr.WriteString(err.Error()) os.Stderr.WriteString("\n") if is_terminal { if term, err := tty.OpenControllingTerm(tty.SetRaw); err == nil { defer term.RestoreAndClose() - term.DebugPrintln(text) + term.DebugPrintln(err.Error()) fmt.Println("Press any key to exit.\r") buf := make([]byte, 16) _, _ = term.Read(buf) @@ -592,8 +592,7 @@ type SizedText struct { func (self *Loop) RecoverFromPanicInGoRoutine() { if r := recover(); r != nil { - text, err := utils.Format_stacktrace_on_panic(r) - err = fmt.Errorf("Panicked in non-main go routine\n%s\n%w", text, err) + err := parallel.Format_stacktrace_on_panic(r, 1) // print to kitty stdout as multiple go routines might panic but only // one panic is reported by the main loop panic_channel if f := tty.KittyStdout(); f != nil { diff --git a/tools/tui/loop/read.go b/tools/tui/loop/read.go index 80ab71f5f..0a2c12e1a 100644 --- a/tools/tui/loop/read.go +++ b/tools/tui/loop/read.go @@ -12,6 +12,7 @@ import ( "golang.org/x/sys/unix" + "github.com/kovidgoyal/go-parallel" "github.com/kovidgoyal/kitty/tools/tty" "github.com/kovidgoyal/kitty/tools/utils" ) @@ -46,8 +47,8 @@ func read_ignoring_temporary_errors(f *tty.Term, buf []byte) (int, error) { func read_from_tty(pipe_r *os.File, term *tty.Term, results_channel chan<- []byte, err_channel chan<- error, quit_channel <-chan byte, leftover_channel chan<- []byte) { defer func() { if r := recover(); r != nil { - text, _ := utils.Format_stacktrace_on_panic(r) - err_channel <- fmt.Errorf("%s", text) + err := parallel.Format_stacktrace_on_panic(r, 1) + err_channel <- err } }() keep_going := true @@ -123,7 +124,7 @@ func read_until_primary_device_attributes_response(term *tty.Term, initial_bytes go func() { defer func() { if r := recover(); r != nil { - text, _ := utils.Format_stacktrace_on_panic(r) + text := parallel.Format_stacktrace_on_panic(r, 1).Error() received <- fmt.Errorf("%s", text) } }() diff --git a/tools/tui/loop/write.go b/tools/tui/loop/write.go index efd567fad..a279e2e16 100644 --- a/tools/tui/loop/write.go +++ b/tools/tui/loop/write.go @@ -10,6 +10,7 @@ import ( "golang.org/x/sys/unix" + "github.com/kovidgoyal/go-parallel" "github.com/kovidgoyal/kitty/tools/tty" "github.com/kovidgoyal/kitty/tools/utils" ) @@ -170,8 +171,7 @@ func write_to_tty( ) { defer func() { if r := recover(); r != nil { - text, _ := utils.Format_stacktrace_on_panic(r) - err_channel <- fmt.Errorf("%s", text) + err_channel <- parallel.Format_stacktrace_on_panic(r, 1) } }() keep_going := true diff --git a/tools/tui/subseq/score.go b/tools/tui/subseq/score.go index ba8e0fa94..83b7a59b9 100644 --- a/tools/tui/subseq/score.go +++ b/tools/tui/subseq/score.go @@ -7,6 +7,7 @@ import ( "slices" "strings" + "github.com/kovidgoyal/go-parallel" "github.com/kovidgoyal/kitty/tools/utils" ) @@ -215,12 +216,11 @@ func ScoreItems(query string, items []string, opts Options) []*Match { ropts := resolved_options_type{ level1: []rune(opts.Level1), level2: []rune(opts.Level2), level3: []rune(opts.Level3), } - utils.Run_in_parallel_over_range(opts.NumberOfThreads, func(start, limit int) error { + parallel.Run_in_parallel_over_range(opts.NumberOfThreads, func(start, limit int) { w := workspace_type{} for i := start; i < limit; i++ { ans[i] = score_item(items[i], i, nr, &ropts, &w) } - return nil }, 0, len(items)) return ans } diff --git a/tools/utils/images/utils.go b/tools/utils/images/utils.go index 9b49c7674..9df960435 100644 --- a/tools/utils/images/utils.go +++ b/tools/utils/images/utils.go @@ -8,7 +8,7 @@ import ( "sync" "sync/atomic" - "github.com/kovidgoyal/kitty/tools/utils" + "github.com/kovidgoyal/go-parallel" ) var _ = fmt.Print @@ -55,8 +55,7 @@ func (self *Context) SafeParallel(start, stop int, fn func(<-chan int)) (err err go func() { defer func() { if r := recover(); r != nil { - text, _ := utils.Format_stacktrace_on_panic(r) - err = fmt.Errorf("%s", text) + err = parallel.Format_stacktrace_on_panic(r, 1) } wg.Done() }() diff --git a/tools/utils/misc.go b/tools/utils/misc.go index 959cda5cb..76dd50949 100644 --- a/tools/utils/misc.go +++ b/tools/utils/misc.go @@ -68,80 +68,6 @@ func Filter[T any](s []T, f func(x T) bool) []T { return ans } -func Format_stacktrace_on_panic(r any) (text string, err error) { - pcs := make([]uintptr, 512) - n := runtime.Callers(3, pcs) - lines := []string{} - frames := runtime.CallersFrames(pcs[:n]) - rt := fmt.Sprint(r) - if strings.HasPrefix(rt, "Panicked with error:") { - err = fmt.Errorf("%s", rt) - lines = append(lines, "Panic caused by previous panic (probably in a gouroutine). Previous panic:\r\n") - lines = append(lines, rt) - lines = append(lines, "\r\n\r\nStacktrace of current panic (most recent call first):\r\n") - } else { - err = fmt.Errorf("Panicked: %s", r) - lines = append(lines, fmt.Sprintf("\r\nPanicked with error: %s\r\nStacktrace (most recent call first):\r\n", r)) - } - found_first_frame := false - for frame, more := frames.Next(); more; frame, more = frames.Next() { - if !found_first_frame { - if strings.HasPrefix(frame.Function, "runtime.") { - continue - } - found_first_frame = true - } - lines = append(lines, fmt.Sprintf("%s\r\n\t%s:%d\r\n", frame.Function, frame.File, frame.Line)) - } - text = strings.Join(lines, "") - return strings.TrimSpace(text), err -} - -// Run the specified function in parallel over chunks from the specified range. -// If the function panics, it is turned into a regular error. -func Run_in_parallel_over_range(num_procs int, f func(int, int) error, start, limit int) (err error) { - num_items := limit - start - if num_procs <= 0 { - num_procs = runtime.NumCPU() - } - num_procs = max(1, min(num_procs, num_items)) - if num_procs < 2 { - defer func() { - if r := recover(); r != nil { - stacktrace, e := Format_stacktrace_on_panic(r) - err = fmt.Errorf("%s\n\n%w", stacktrace, e) - } - }() - return f(start, limit) - } - chunk_sz := max(1, num_items/num_procs) - var wg sync.WaitGroup - echan := make(chan error, num_procs) - for start < limit { - end := min(start+chunk_sz, limit) - wg.Add(1) - go func(start, end int) { - defer func() { - if r := recover(); r != nil { - stacktrace, e := Format_stacktrace_on_panic(r) - echan <- fmt.Errorf("%s\n\n%w", stacktrace, e) - } - wg.Done() - }() - if err := f(start, end); err != nil { - echan <- err - } - }(start, end) - start = end - } - wg.Wait() - close(echan) - for qerr := range echan { - return qerr - } - return - -} func Map[T any, O any](f func(x T) O, s []T) []O { ans := make([]O, 0, len(s)) for _, x := range s {