From a8d93febba3f5dd0dd1274abcd92e18f0910d4a4 Mon Sep 17 00:00:00 2001 From: domi41 Date: Fri, 5 Nov 2021 23:03:09 +0100 Subject: [PATCH] feat: add `--no-color` and enable color in the text merit profile --- cmd/root.go | 12 +++--- formatter/formatter.go | 7 ++-- formatter/text.go | 85 ++++++++++++++++++++++++++++++++---------- go.mod | 5 +++ 4 files changed, 82 insertions(+), 27 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 9cdd8e3..4554a25 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -234,8 +234,8 @@ The --width parameter only applies to the default format (text). os.Exit(errorBalancing) } - deliberator := &judgment.MajorityJudgment{} - result, deliberationErr := deliberator.Deliberate(poll) + mj := &judgment.MajorityJudgment{} + result, deliberationErr := mj.Deliberate(poll) if deliberationErr != nil { fmt.Println("Deliberation Error:", deliberationErr) os.Exit(errorDeliberating) @@ -246,9 +246,10 @@ The --width parameter only applies to the default format (text). desiredWidth = 79 } options := &formatter.Options{ - Sorted: cmd.Flags().Lookup("sort").Changed, - Width: desiredWidth, - Scale: precisionScale, + Colorized: !cmd.Flags().Lookup("no-color").Changed, + Scale: precisionScale, + Sorted: cmd.Flags().Lookup("sort").Changed, + Width: desiredWidth, } out, formatterErr := outputFormatter.Format( @@ -290,6 +291,7 @@ func init() { //rootCmd.PersistentFlags().Bool("viper", true, "use Viper for configuration") rootCmd.Flags().BoolP("sort", "s", false, "sort proposals by their rank") rootCmd.Flags().BoolP("normalize", "n", false, "normalize input to balance proposal participation") + rootCmd.Flags().Bool("no-color", false, "do not use colors in the text outputs") rootCmd.SetVersionTemplate("{{.Version}}\n" + version.BuildDate + "\n") } diff --git a/formatter/formatter.go b/formatter/formatter.go index 3ed0aea..0d7933a 100644 --- a/formatter/formatter.go +++ b/formatter/formatter.go @@ -5,9 +5,10 @@ import "github.com/mieuxvoter/majority-judgment-library-go/judgment" // Options are shared between all formatters. // Some formatters may ignore some options. type Options struct { - Sorted bool - Width int - Scale float64 // so we can use integers internally, and display floats + Colorized bool + Scale float64 // so we can use integers internally, and display floats + Sorted bool + Width int } const defaultWidth = 79 diff --git a/formatter/text.go b/formatter/text.go index 7017a74..fb52e07 100644 --- a/formatter/text.go +++ b/formatter/text.go @@ -2,8 +2,10 @@ package formatter import ( "fmt" + "github.com/acarl005/stripansi" "github.com/mieuxvoter/majority-judgment-library-go/judgment" - "math" + "github.com/muesli/termenv" + "strconv" "strings" ) @@ -31,6 +33,10 @@ func (t *TextFormatter) Format( expectedWidth = defaultWidth } + colorized := options.Colorized + palette := judgment.CreateDefaultPalette(len(grades)) + colorProfile := termenv.ColorProfile() + proposalsResults := result.Proposals if options.Sorted { proposalsResults = result.ProposalsSorted @@ -80,6 +86,7 @@ func (t *TextFormatter) Format( line += makeAsciiMeritProfile( pollTally.Proposals[proposalResult.Index], chartWidth, + colorized, ) out += line + "\n" @@ -91,11 +98,19 @@ func (t *TextFormatter) Format( if maximumDefinitionLength < minimumDefinitionLength { maximumDefinitionLength = minimumDefinitionLength } + gradeChar := getCharForIndex(gradeIndex) + if colorized { + color := colorProfile.FromColor(palette[gradeIndex]) + s := termenv.String(gradeChar) + s = s.Background(color) + s = s.Foreground(color) + gradeChar = s.String() + } legendDefinitions = append( legendDefinitions, fmt.Sprintf( "%s=%s", - getCharForIndex(gradeIndex), + gradeChar, truncateString(gradeName, maximumDefinitionLength, '…'), ), ) @@ -127,7 +142,12 @@ func countDigits(i int) (count int) { // makeTextLegend makes a legend for an ASCII chart // `title` should be shorter than `indentation` characters -func makeTextLegend(title string, definitions []string, indentation int, maxWidth int) (legend string) { +func makeTextLegend( + title string, + definitions []string, + indentation int, + maxWidth int, +) (legend string) { line := "" leftOnLine := maxWidth for i, def := range definitions { @@ -135,7 +155,7 @@ func makeTextLegend(title string, definitions []string, indentation int, maxWidt line += fmt.Sprintf("%*s", indentation-1, title) leftOnLine -= indentation - 1 } - needed := measureStringLength(def) + 1 + needed := measureStringLength(stripansi.Strip(def)) + 1 if needed > leftOnLine && i > 0 { legend += line + "\n" line = "" @@ -154,31 +174,58 @@ func makeTextLegend(title string, definitions []string, indentation int, maxWidt func makeAsciiMeritProfile( tally *judgment.ProposalTally, width int, + colorized bool, ) (ascii string) { if width < 3 { width = 3 } - amountOfJudges := float64(tally.CountJudgments()) - for gradeIndex, gradeTallyInt := range tally.Tally { - gradeTally := float64(gradeTallyInt) + palette := judgment.CreateDefaultPalette(int(tally.CountAvailableGrades())) + colorProfile := termenv.ColorProfile() + + for cursor := 0; cursor < width; cursor++ { + ratio := float64(cursor) / float64(width) + gradeIndex, _ := getGradeAtRatio(tally, ratio) gradeChar := getCharForIndex(gradeIndex) - ascii += strings.Repeat( - gradeChar, - int(math.Round(float64(width)*gradeTally/amountOfJudges)), - ) - } + isMedian := (width)/2 == cursor + if isMedian { + gradeChar = "|" + } - for len(ascii) < width { - ascii += ascii[len(ascii)-1:] + if colorized { + color := colorProfile.FromColor(palette[gradeIndex]) + s := termenv.String(gradeChar) + s = s.Background(color) + if !isMedian { + s = s.Foreground(color) + } + gradeChar = s.String() + } + ascii += gradeChar } - for len(ascii) > width { - ascii = ascii[0 : len(ascii)-1] - } + return +} + +func getGradeAtRatio( + tally *judgment.ProposalTally, + ratio float64, +) (int, error) { + targetIndex := int(ratio * float64(tally.CountJudgments())) + cursorStart := 0 + cursor := 0 + for gradeIndex, gradeTallyInt := range tally.Tally { + if 0 == gradeTallyInt { + continue + } + cursorStart = cursor + cursor = cursor + int(gradeTallyInt) - ascii = replaceAtIndex(ascii, '|', width/2) + if cursorStart <= targetIndex && targetIndex < cursor { + return gradeIndex, nil + } + } - return + return 0, fmt.Errorf("") } func getCharForIndex(gradeIndex int) string { diff --git a/go.mod b/go.mod index 5b948b2..12e0a0c 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,10 @@ module github.com/MieuxVoter/majority-judgment-cli go 1.17 require ( + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d github.com/csimplestring/go-csv v0.0.0-20180328183906-5b8b3cd94f2c github.com/mieuxvoter/majority-judgment-library-go v0.3.1 + github.com/muesli/termenv v0.9.0 github.com/spf13/cobra v1.2.1 github.com/spf13/viper v1.9.0 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b @@ -16,8 +18,11 @@ require ( github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/magiconair/properties v1.8.5 // indirect + github.com/mattn/go-isatty v0.0.13 // indirect + github.com/mattn/go-runewidth v0.0.13 // indirect github.com/mitchellh/mapstructure v1.4.2 // indirect github.com/pelletier/go-toml v1.9.4 // indirect + github.com/rivo/uniseg v0.2.0 // indirect github.com/spf13/afero v1.6.0 // indirect github.com/spf13/cast v1.4.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect