From 72819e8a99f1add1fe793143a72be160a4ede8ea Mon Sep 17 00:00:00 2001 From: abhisek Date: Thu, 15 May 2025 15:26:48 +0530 Subject: [PATCH] fix: UI text wrapping --- guard/guard.go | 25 ++++++++----- internal/ui/ui.go | 61 ++++++++++++++++++++++---------- internal/ui/ui_test.go | 79 ++++++++++++++++++++++++++++++++++++++++++ main.go | 5 ++- 4 files changed, 139 insertions(+), 31 deletions(-) create mode 100644 internal/ui/ui_test.go diff --git a/guard/guard.go b/guard/guard.go index 76f1afa..771f89b 100644 --- a/guard/guard.go +++ b/guard/guard.go @@ -15,10 +15,19 @@ import ( ) type PackageManagerGuardInteraction struct { - SetStatus func(status string) - ClearStatus func() + // SetStatus is called to set the status of the guard in the UI + SetStatus func(status string) + + // ClearStatus is called to clear the status of the guard in the UI + ClearStatus func() + + // GetConfirmationOnMalware is called to get the confirmation of the user on the malware packages GetConfirmationOnMalware func(malwarePackages []*analyzer.PackageVersionAnalysisResult) (bool, error) - Block func() error + + // Block is called to block the installation of the malware packages. One or more malicious + // packages are passed as arguments. These are the packages that were detected as malicious. + // Client code must perform the necessary error handling and termination of the process. + Block func(...*analyzer.PackageVersionAnalysisResult) error } type PackageManagerGuardConfig struct { @@ -121,8 +130,7 @@ func (g *packageManagerGuard) Run(ctx context.Context, args []string) error { confirmableMalwarePackages := []*analyzer.PackageVersionAnalysisResult{} for _, result := range analysisResults { if result.Action == analyzer.ActionBlock { - _ = g.blockInstallation() - return fmt.Errorf("malicious packages detected, installation aborted") + return g.blockInstallation(result) } if result.Action == analyzer.ActionConfirm { @@ -137,8 +145,7 @@ func (g *packageManagerGuard) Run(ctx context.Context, args []string) error { } if !confirmed { - _ = g.blockInstallation() - return fmt.Errorf("malicious packages detected, installation aborted") + return g.blockInstallation(confirmableMalwarePackages...) } } @@ -234,12 +241,12 @@ func (g *packageManagerGuard) setStatus(status string) { g.interaction.SetStatus(status) } -func (g *packageManagerGuard) blockInstallation() error { +func (g *packageManagerGuard) blockInstallation(malwarePackages ...*analyzer.PackageVersionAnalysisResult) error { if g.interaction.Block == nil { return nil } - return g.interaction.Block() + return g.interaction.Block(malwarePackages...) } func (g *packageManagerGuard) clearStatus() { diff --git a/internal/ui/ui.go b/internal/ui/ui.go index 2958e4f..d908d00 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -37,12 +37,15 @@ func ClearStatus() { fmt.Print("\r") } -func Block() error { +func Block(malwarePackages ...*analyzer.PackageVersionAnalysisResult) error { StopSpinner() fmt.Println() fmt.Println(Colors.Red("❌ Malicious package blocked!")) + printMaliciousPackagesList(malwarePackages) + + fmt.Println() os.Exit(1) return nil @@ -63,22 +66,7 @@ func GetConfirmationOnMalware(malwarePackages []*analyzer.PackageVersionAnalysis fmt.Println() fmt.Println(Colors.Red(fmt.Sprintf("🚨 Suspicious package(s) detected: %d", len(malwarePackages)))) - for _, mp := range malwarePackages { - fmt.Println() - fmt.Println("⚠️ ", Colors.Red(fmt.Sprintf("%s@%s", mp.PackageVersion.GetPackage().GetName(), - mp.PackageVersion.GetVersion()))) - - if verbosityLevel == VerbosityLevelVerbose { - fmt.Println(Colors.Yellow(termWidthFormatText(mp.Summary, 60))) - - if mp.ReferenceURL != "" { - fmt.Println() - fmt.Println(Colors.Yellow(fmt.Sprintf("Reference: %s", mp.ReferenceURL))) - } - - fmt.Println() - } - } + printMaliciousPackagesList(malwarePackages) fmt.Println() fmt.Print(Colors.Yellow("Do you want to continue with the installation? (y/N) ")) @@ -108,14 +96,44 @@ func Fatalf(msg string, args ...interface{}) { os.Exit(1) } +func printMaliciousPackagesList(malwarePackages []*analyzer.PackageVersionAnalysisResult) { + for _, mp := range malwarePackages { + fmt.Println() + fmt.Println("⚠️ ", Colors.Red(fmt.Sprintf("%s@%s", mp.PackageVersion.GetPackage().GetName(), + mp.PackageVersion.GetVersion()))) + + if verbosityLevel == VerbosityLevelVerbose { + fmt.Println(Colors.Yellow(termWidthFormatText(mp.Summary, 80))) + + if mp.ReferenceURL != "" { + fmt.Println() + fmt.Println(Colors.Yellow(fmt.Sprintf("Reference: %s", mp.ReferenceURL))) + } + } + } +} + // Format the string to be maximum maxWidth. Use newlines to wrap the text. func termWidthFormatText(text string, maxWidth int) string { + // Replace all newlines with spaces so that we can split the text into words + // This is to ensure that we don't split the text at the newlines + text = strings.ReplaceAll(text, "\n", " ") + words := strings.Split(text, " ") lines := []string{} currentLine := "" - for _, word := range words { - if len(currentLine)+len(word) > maxWidth { + for i, word := range words { + // Skip empty words that might result from multiple spaces + if word == "" { + continue + } + + if i == 0 { + // First word doesn't need a leading space + currentLine = word + } else if len(currentLine)+len(word)+1 > maxWidth { + // +1 for the space we would add lines = append(lines, currentLine) currentLine = word } else { @@ -123,5 +141,10 @@ func termWidthFormatText(text string, maxWidth int) string { } } + // Don't forget to add the last line + if currentLine != "" { + lines = append(lines, currentLine) + } + return strings.Join(lines, "\n") } diff --git a/internal/ui/ui_test.go b/internal/ui/ui_test.go new file mode 100644 index 0000000..00b0bb0 --- /dev/null +++ b/internal/ui/ui_test.go @@ -0,0 +1,79 @@ +package ui + +import ( + "testing" +) + +// TestTermWidthFormatText is exported for testing +func TestTermWidthFormatTextFunc(t *testing.T) { + tests := []struct { + name string + text string + maxWidth int + expected string + }{ + { + name: "empty string", + text: "", + maxWidth: 10, + expected: "", + }, + { + name: "single word less than max width", + text: "hello", + maxWidth: 10, + expected: "hello", + }, + { + name: "single word longer than max width", + text: "supercalifragilisticexpialidocious", + maxWidth: 10, + expected: "supercalifragilisticexpialidocious", + }, + { + name: "multiple words on single line", + text: "hello world", + maxWidth: 20, + expected: "hello world", + }, + { + name: "multiple words wrapped to multiple lines", + text: "The quick brown fox jumps over the lazy dog", + maxWidth: 20, + expected: "The quick brown fox\njumps over the lazy\ndog", + }, + { + name: "text with existing newlines", + text: "hello\nworld", + maxWidth: 20, + expected: "hello world", + }, + { + name: "text with multiple spaces", + text: "hello world test", + maxWidth: 20, + expected: "hello world test", + }, + { + name: "very small max width", + text: "hello world", + maxWidth: 3, + expected: "hello\nworld", + }, + { + name: "large max width", + text: "The quick brown fox jumps over the lazy dog", + maxWidth: 100, + expected: "The quick brown fox jumps over the lazy dog", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := termWidthFormatText(tt.text, tt.maxWidth) + if result != tt.expected { + t.Errorf("termWidthFormatText() = %q, want %q", result, tt.expected) + } + }) + } +} diff --git a/main.go b/main.go index 7c2db73..778ba9c 100644 --- a/main.go +++ b/main.go @@ -36,9 +36,8 @@ func main() { os.Setenv("APP_LOG_LEVEL", "debug") } - // Skip stdout logging when debugging and verbose is not enabled - // This is default behavior for the CLI unless explicitly set - if !debug && !verbose { + // Skip stdout logging when debugging is not enabled + if !debug { os.Setenv("APP_LOG_SKIP_STDOUT_LOGGER", "true") }