mirror of
https://github.com/ivuorinen/gibidify.git
synced 2026-01-26 11:34:03 +00:00
* build: update Go 1.25, CI workflows, and build tooling - Upgrade to Go 1.25 - Add benchmark targets to Makefile - Implement parallel gosec execution - Lock tool versions for reproducibility - Add shellcheck directives to scripts - Update CI workflows with improved caching * refactor: migrate from golangci-lint to revive - Replace golangci-lint with revive for linting - Configure comprehensive revive rules - Fix all EditorConfig violations - Add yamllint and yamlfmt support - Remove deprecated .golangci.yml * refactor: rename utils to shared and deduplicate code - Rename utils package to shared - Add shared constants package - Deduplicate constants across packages - Address CodeRabbit review feedback * fix: resolve SonarQube issues and add safety guards - Fix all 73 SonarQube OPEN issues - Add nil guards for resourceMonitor, backpressure, metricsCollector - Implement io.Closer for headerFileReader - Propagate errors from processing helpers - Add metrics and templates packages - Improve error handling across codebase * test: improve test infrastructure and coverage - Add benchmarks for cli, fileproc, metrics - Improve test coverage for cli, fileproc, config - Refactor tests with helper functions - Add shared test constants - Fix test function naming conventions - Reduce cognitive complexity in benchmark tests * docs: update documentation and configuration examples - Update CLAUDE.md with current project state - Refresh README with new features - Add usage and configuration examples - Add SonarQube project configuration - Consolidate config.example.yaml * fix: resolve shellcheck warnings in scripts - Use ./*.go instead of *.go to prevent dash-prefixed filenames from being interpreted as options (SC2035) - Remove unreachable return statement after exit (SC2317) - Remove obsolete gibidiutils/ directory reference * chore(deps): upgrade go dependencies * chore(lint): megalinter fixes * fix: improve test coverage and fix file descriptor leaks - Add defer r.Close() to fix pipe file descriptor leaks in benchmark tests - Refactor TestProcessorConfigureFileTypes with helper functions and assertions - Refactor TestProcessorLogFinalStats with output capture and keyword verification - Use shared constants instead of literal strings (TestFilePNG, FormatMarkdown, etc.) - Reduce cognitive complexity by extracting helper functions * fix: align test comments with function names Remove underscores from test comments to match actual function names: - benchmark/benchmark_test.go (2 fixes) - fileproc/filetypes_config_test.go (4 fixes) - fileproc/filetypes_registry_test.go (6 fixes) - fileproc/processor_test.go (6 fixes) - fileproc/resource_monitor_types_test.go (4 fixes) - fileproc/writer_test.go (3 fixes) * fix: various test improvements and bug fixes - Remove duplicate maxCacheSize check in filetypes_registry_test.go - Shorten long comment in processor_test.go to stay under 120 chars - Remove flaky time.Sleep in collector_test.go, use >= 0 assertion - Close pipe reader in benchmark_test.go to fix file descriptor leak - Use ContinueOnError in flags_test.go to match ResetFlags behavior - Add nil check for p.ui in processor_workers.go before UpdateProgress - Fix resource_monitor_validation_test.go by setting hardMemoryLimitBytes directly * chore(yaml): add missing document start markers Add --- document start to YAML files to satisfy yamllint: - .github/workflows/codeql.yml - .github/workflows/build-test-publish.yml - .github/workflows/security.yml - .github/actions/setup/action.yml * fix: guard nil resourceMonitor and fix test deadlock - Guard resourceMonitor before CreateFileProcessingContext call - Add ui.UpdateProgress on emergency stop and path error returns - Fix potential deadlock in TestProcessFile using wg.Go with defer close
212 lines
5.0 KiB
Go
212 lines
5.0 KiB
Go
// Package cli provides command-line interface functionality for gibidify.
|
||
package cli
|
||
|
||
import (
|
||
"fmt"
|
||
"io"
|
||
"os"
|
||
"time"
|
||
|
||
"github.com/fatih/color"
|
||
"github.com/schollz/progressbar/v3"
|
||
|
||
"github.com/ivuorinen/gibidify/shared"
|
||
)
|
||
|
||
// UIManager handles CLI user interface elements.
|
||
type UIManager struct {
|
||
enableColors bool
|
||
enableProgress bool
|
||
silentMode bool
|
||
progressBar *progressbar.ProgressBar
|
||
output io.Writer
|
||
}
|
||
|
||
// NewUIManager creates a new UI manager.
|
||
func NewUIManager() *UIManager {
|
||
return &UIManager{
|
||
enableColors: isColorTerminal(),
|
||
enableProgress: isInteractiveTerminal(),
|
||
output: os.Stderr, // Progress and colors go to stderr
|
||
}
|
||
}
|
||
|
||
// SetColorOutput enables or disables colored output.
|
||
func (ui *UIManager) SetColorOutput(enabled bool) {
|
||
ui.enableColors = enabled
|
||
color.NoColor = !enabled
|
||
}
|
||
|
||
// SetProgressOutput enables or disables progress bars.
|
||
func (ui *UIManager) SetProgressOutput(enabled bool) {
|
||
ui.enableProgress = enabled
|
||
}
|
||
|
||
// SetSilentMode enables or disables all UI output.
|
||
func (ui *UIManager) SetSilentMode(silent bool) {
|
||
ui.silentMode = silent
|
||
if silent {
|
||
ui.output = io.Discard
|
||
} else {
|
||
ui.output = os.Stderr
|
||
}
|
||
}
|
||
|
||
// StartProgress initializes a progress bar for file processing.
|
||
func (ui *UIManager) StartProgress(total int, description string) {
|
||
if !ui.enableProgress || total <= 0 {
|
||
return
|
||
}
|
||
|
||
ui.progressBar = progressbar.NewOptions(
|
||
total,
|
||
progressbar.OptionSetWriter(ui.output),
|
||
progressbar.OptionSetDescription(description),
|
||
progressbar.OptionSetTheme(
|
||
progressbar.Theme{
|
||
Saucer: color.GreenString(shared.UIProgressBarChar),
|
||
SaucerHead: color.GreenString(shared.UIProgressBarChar),
|
||
SaucerPadding: " ",
|
||
BarStart: "[",
|
||
BarEnd: "]",
|
||
},
|
||
),
|
||
progressbar.OptionShowCount(),
|
||
progressbar.OptionShowIts(),
|
||
progressbar.OptionSetWidth(40),
|
||
progressbar.OptionThrottle(100*time.Millisecond),
|
||
progressbar.OptionOnCompletion(
|
||
func() {
|
||
//nolint:errcheck // UI output, errors don't affect processing
|
||
_, _ = fmt.Fprint(ui.output, "\n")
|
||
},
|
||
),
|
||
progressbar.OptionSetRenderBlankState(true),
|
||
)
|
||
}
|
||
|
||
// UpdateProgress increments the progress bar.
|
||
func (ui *UIManager) UpdateProgress(increment int) {
|
||
if ui.progressBar != nil {
|
||
_ = ui.progressBar.Add(increment)
|
||
}
|
||
}
|
||
|
||
// FinishProgress completes the progress bar.
|
||
func (ui *UIManager) FinishProgress() {
|
||
if ui.progressBar != nil {
|
||
_ = ui.progressBar.Finish()
|
||
ui.progressBar = nil
|
||
}
|
||
}
|
||
|
||
// PrintSuccess prints a success message in green.
|
||
func (ui *UIManager) PrintSuccess(format string, args ...any) {
|
||
if ui.silentMode {
|
||
return
|
||
}
|
||
if ui.enableColors {
|
||
color.Green("✓ "+format, args...)
|
||
} else {
|
||
ui.printf("✓ "+format+"\n", args...)
|
||
}
|
||
}
|
||
|
||
// PrintError prints an error message in red.
|
||
func (ui *UIManager) PrintError(format string, args ...any) {
|
||
if ui.silentMode {
|
||
return
|
||
}
|
||
if ui.enableColors {
|
||
color.Red("✗ "+format, args...)
|
||
} else {
|
||
ui.printf("✗ "+format+"\n", args...)
|
||
}
|
||
}
|
||
|
||
// PrintWarning prints a warning message in yellow.
|
||
func (ui *UIManager) PrintWarning(format string, args ...any) {
|
||
if ui.silentMode {
|
||
return
|
||
}
|
||
if ui.enableColors {
|
||
color.Yellow("⚠ "+format, args...)
|
||
} else {
|
||
ui.printf("⚠ "+format+"\n", args...)
|
||
}
|
||
}
|
||
|
||
// PrintInfo prints an info message in blue.
|
||
func (ui *UIManager) PrintInfo(format string, args ...any) {
|
||
if ui.silentMode {
|
||
return
|
||
}
|
||
if ui.enableColors {
|
||
//nolint:errcheck // UI output, errors don't affect processing
|
||
color.Blue("ℹ "+format, args...)
|
||
} else {
|
||
ui.printf("ℹ "+format+"\n", args...)
|
||
}
|
||
}
|
||
|
||
// PrintHeader prints a header message in bold.
|
||
func (ui *UIManager) PrintHeader(format string, args ...any) {
|
||
if ui.silentMode {
|
||
return
|
||
}
|
||
if ui.enableColors {
|
||
//nolint:errcheck // UI output, errors don't affect processing
|
||
_, _ = color.New(color.Bold).Fprintf(ui.output, format+"\n", args...)
|
||
} else {
|
||
ui.printf(format+"\n", args...)
|
||
}
|
||
}
|
||
|
||
// isColorTerminal checks if the terminal supports colors.
|
||
func isColorTerminal() bool {
|
||
// Check common environment variables
|
||
term := os.Getenv("TERM")
|
||
if term == "" || term == "dumb" {
|
||
return false
|
||
}
|
||
|
||
// Check for CI environments that typically don't support colors
|
||
if os.Getenv("CI") != "" {
|
||
// GitHub Actions supports colors
|
||
if os.Getenv("GITHUB_ACTIONS") == shared.LiteralTrue {
|
||
return true
|
||
}
|
||
// Most other CI systems don't
|
||
return false
|
||
}
|
||
|
||
// Check if NO_COLOR is set (https://no-color.org/)
|
||
if os.Getenv("NO_COLOR") != "" {
|
||
return false
|
||
}
|
||
|
||
// Check if FORCE_COLOR is set
|
||
if os.Getenv("FORCE_COLOR") != "" {
|
||
return true
|
||
}
|
||
|
||
// Default to true for interactive terminals
|
||
return isInteractiveTerminal()
|
||
}
|
||
|
||
// isInteractiveTerminal checks if we're running in an interactive terminal.
|
||
func isInteractiveTerminal() bool {
|
||
// Check if stderr is a terminal (where we output progress/colors)
|
||
fileInfo, err := os.Stderr.Stat()
|
||
if err != nil {
|
||
return false
|
||
}
|
||
|
||
return (fileInfo.Mode() & os.ModeCharDevice) != 0
|
||
}
|
||
|
||
// printf is a helper that ignores printf errors (for UI output).
|
||
func (ui *UIManager) printf(format string, args ...any) {
|
||
_, _ = fmt.Fprintf(ui.output, format, args...)
|
||
}
|