From f11b80a0ad202ba5ab2306f64e024e20648ddd28 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Wed, 16 Sep 2026 10:16:22 +0200 Subject: [PATCH] Show a live progress bar for each file-parsing step --- main.go | 42 ++++++++++++++---------------- progress.go | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 23 deletions(-) create mode 100644 progress.go diff --git a/main.go b/main.go index b2935403..af42c260 100644 --- a/main.go +++ b/main.go @@ -58,12 +58,11 @@ func run(args []string) error { // gather project symbols (enums, constants) so argument values that // reference them resolve to a type just like literals do table := symbols.New() - for _, file := range files { - src, err := os.ReadFile(file) - if err != nil { - return err - } + if err := progressEach("scanning", files, func(_ string, src []byte) error { table.CollectSource(src) + return nil + }); err != nil { + return err } // build the inheritance table from the project and its vendor directory, so @@ -73,23 +72,22 @@ func run(args []string) error { if err != nil { return err } - for _, file := range append(append([]string{}, files...), vendorFiles...) { - src, err := os.ReadFile(file) - if err != nil { - return err - } + allFiles := append(append([]string{}, files...), vendorFiles...) + if err := progressEach("parsing", allFiles, func(_ string, src []byte) error { inheritance.CollectSource(src) + return nil + }); err != nil { + return err } // 1. collect literal argument types across the whole project fmt.Println("1. Collecting argument types...") var records []collect.Record - for _, file := range files { - src, err := os.ReadFile(file) - if err != nil { - return err - } + if err := progressEach("collecting", files, func(_ string, src []byte) error { records = append(records, collect.FromSource(src, table)...) + return nil + }); err != nil { + return err } fmt.Printf(" Found %d arg types\n\n", len(records)) @@ -102,15 +100,10 @@ func run(args []string) error { fmt.Println("2. Adding types to parameters...") } added := 0 - for _, file := range files { - src, err := os.ReadFile(file) - if err != nil { - return err - } - + if err := progressEach("applying", files, func(file string, src []byte) error { output, count, changed := apply.Source(src, types, table, inheritance) if !changed { - continue + return nil } if dry { @@ -118,13 +111,16 @@ func run(args []string) error { fmt.Print(patch) } added += count - continue + return nil } if err := os.WriteFile(file, []byte(output), 0o644); err != nil { return err } added += count + return nil + }); err != nil { + return err } if added == 0 { diff --git a/progress.go b/progress.go new file mode 100644 index 00000000..66e1412a --- /dev/null +++ b/progress.go @@ -0,0 +1,73 @@ +package main + +import ( + "fmt" + "os" + "strings" +) + +// progressBarWidth is the number of cells in the rendered bar, matching the +// default width Symfony's progress bar uses. +const progressBarWidth = 28 + +// labelColumnWidth pads the phase label so the successive phases' bars line up +// in one column. It is the length of the widest label, "collecting". +const labelColumnWidth = 10 + +// stderrIsTerminal reports whether stderr is an interactive terminal. The live +// bar redraws in place with a carriage return, which only reads correctly on a +// terminal; a redirected or piped run stays plain. +var stderrIsTerminal = detectTerminal() + +func detectTerminal() bool { + fileInfo, err := os.Stderr.Stat() + if err != nil { + return false + } + return fileInfo.Mode()&os.ModeCharDevice != 0 +} + +// renderProgressBar draws a rector-style bar keyed by phase label, e.g. +// +// collecting 1080/1659 [==============>-------------] 65% +// +// The leading carriage return rewrites the line in place on each tick. +func renderProgressBar(label string, done int, total int) string { + percent := 0 + filled := 0 + if total > 0 { + percent = done * 100 / total + filled = done * progressBarWidth / total + } + + var bar string + if filled >= progressBarWidth { + bar = strings.Repeat("=", progressBarWidth) + } else { + bar = strings.Repeat("=", filled) + ">" + strings.Repeat("-", progressBarWidth-filled-1) + } + return fmt.Sprintf("\r%-*s %d/%d [%s] %3d%%", labelColumnWidth, label, done, total, bar, percent) +} + +// progressEach reads each file and hands its source to fn, drawing a live +// progress bar labeled label on stderr while it goes. The bar is skipped on a +// non-terminal run so piped output stays clean. +func progressEach(label string, files []string, fn func(file string, src []byte) error) error { + total := len(files) + for index, file := range files { + src, err := os.ReadFile(file) + if err != nil { + return err + } + if err := fn(file, src); err != nil { + return err + } + if stderrIsTerminal { + fmt.Fprint(os.Stderr, renderProgressBar(label, index+1, total)) + } + } + if total > 0 && stderrIsTerminal { + fmt.Fprintln(os.Stderr) // end the progress bar line + } + return nil +}