From 9fc8dbf4e167ae3d3a5fd602df74645e07d79679 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sat, 14 Jan 2023 20:02:52 +0100 Subject: [PATCH 1/2] command: adapt the output of the bug list to the terminal size --- commands/bug/bug.go | 25 ++++++++++++++++++++++--- commands/execenv/env.go | 13 +++++++++++++ commands/execenv/env_testing.go | 12 ++++++++---- entity/id.go | 4 ++-- entity/id_interleaved.go | 2 +- 5 files changed, 46 insertions(+), 10 deletions(-) diff --git a/commands/bug/bug.go b/commands/bug/bug.go index a5ce11ed..46b00d3d 100644 --- a/commands/bug/bug.go +++ b/commands/bug/bug.go @@ -15,6 +15,7 @@ import ( "github.com/MichaelMure/git-bug/commands/execenv" "github.com/MichaelMure/git-bug/entities/bug" "github.com/MichaelMure/git-bug/entities/common" + "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/query" "github.com/MichaelMure/git-bug/util/colors" ) @@ -233,6 +234,24 @@ func bugsIDFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) error { } func bugsDefaultFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) error { + width := env.Out.Width() + widthId := entity.HumanIdLength + widthStatus := len("closed") + widthComment := 6 + + widthRemaining := width - + widthId - 1 - + widthStatus - 1 - + widthComment - 1 + + widthTitle := int(float32(widthRemaining-3) * 0.7) + if widthTitle < 0 { + widthTitle = 0 + } + + widthRemaining = widthRemaining - widthTitle - 3 - 2 + widthAuthor := widthRemaining + for _, b := range bugExcerpts { author, err := env.Backend.Identities().ResolveExcerpt(b.AuthorId) if err != nil { @@ -249,8 +268,8 @@ func bugsDefaultFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) err // truncate + pad if needed labelsFmt := text.TruncateMax(labelsTxt.String(), 10) - titleFmt := text.LeftPadMaxLine(strings.TrimSpace(b.Title), 50-text.Len(labelsFmt), 0) - authorFmt := text.LeftPadMaxLine(author.DisplayName(), 15, 0) + titleFmt := text.LeftPadMaxLine(strings.TrimSpace(b.Title), widthTitle-text.Len(labelsFmt), 0) + authorFmt := text.LeftPadMaxLine(author.DisplayName(), widthAuthor, 0) comments := fmt.Sprintf("%3d 💬", b.LenComments-1) if b.LenComments-1 <= 0 { @@ -260,7 +279,7 @@ func bugsDefaultFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) err comments = " ∞ 💬" } - env.Out.Printf("%s\t%s\t%s\t%s\t%s\n", + env.Out.Printf("%s\t%s\t%s %s %s\n", colors.Cyan(b.Id().Human()), colors.Yellow(b.Status), titleFmt+labelsFmt, diff --git a/commands/execenv/env.go b/commands/execenv/env.go index 46de8401..e693807e 100644 --- a/commands/execenv/env.go +++ b/commands/execenv/env.go @@ -7,6 +7,7 @@ import ( "os" "github.com/mattn/go-isatty" + "golang.org/x/term" "github.com/MichaelMure/git-bug/cache" "github.com/MichaelMure/git-bug/repository" @@ -57,6 +58,8 @@ type Out interface { // IsTerminal tells if the output is a user terminal (rather than a buffer, // a pipe ...), which tells if we can use colors and other interactive features. IsTerminal() bool + // Width return the width of the attached terminal, or a good enough value. + Width() int // Raw return the underlying io.Writer, or itself if not. // This is useful if something need to access the raw file descriptor. @@ -123,6 +126,16 @@ func (o out) IsTerminal() bool { return false } +func (o out) Width() int { + if f, ok := o.Raw().(*os.File); ok { + width, _, err := term.GetSize(int(f.Fd())) + if err == nil { + return width + } + } + return 80 +} + func (o out) Raw() io.Writer { return o.Writer } diff --git a/commands/execenv/env_testing.go b/commands/execenv/env_testing.go index 03fe0430..15d7b646 100644 --- a/commands/execenv/env_testing.go +++ b/commands/execenv/env_testing.go @@ -20,14 +20,14 @@ type TestIn struct { forceIsTerminal bool } -func (t *TestIn) ForceIsTerminal(value bool) { - t.forceIsTerminal = value -} - func (t *TestIn) IsTerminal() bool { return t.forceIsTerminal } +func (t *TestIn) ForceIsTerminal(value bool) { + t.forceIsTerminal = value +} + var _ Out = &TestOut{} type TestOut struct { @@ -60,6 +60,10 @@ func (te *TestOut) IsTerminal() bool { return te.forceIsTerminal } +func (te *TestOut) Width() int { + return 80 +} + func (te *TestOut) Raw() io.Writer { return te.Buffer } diff --git a/entity/id.go b/entity/id.go index 49398da8..0949bf92 100644 --- a/entity/id.go +++ b/entity/id.go @@ -11,7 +11,7 @@ import ( // sha-256 const idLength = 64 -const humanIdLength = 7 +const HumanIdLength = 7 const UnsetId = Id("unset") @@ -34,7 +34,7 @@ func (i Id) String() string { // Human return the identifier, shortened for human consumption func (i Id) Human() string { - format := fmt.Sprintf("%%.%ds", humanIdLength) + format := fmt.Sprintf("%%.%ds", HumanIdLength) return fmt.Sprintf(format, i) } diff --git a/entity/id_interleaved.go b/entity/id_interleaved.go index 28c59a42..7ae6d72e 100644 --- a/entity/id_interleaved.go +++ b/entity/id_interleaved.go @@ -22,7 +22,7 @@ func (ci CombinedId) String() string { // Human return the identifier, shortened for human consumption func (ci CombinedId) Human() string { - format := fmt.Sprintf("%%.%ds", humanIdLength) + format := fmt.Sprintf("%%.%ds", HumanIdLength) return fmt.Sprintf(format, ci) } From f23a7f07cc9d7e9b9407e0fcb99de6904f1b52a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Tue, 17 Jan 2023 20:25:34 +0100 Subject: [PATCH 2/2] commands: remove compact style for "bug", as the width adaptive default renderer cover that usage --- commands/bug/bug.go | 86 ++++++++++++-------------------- commands/bug/bug_comment_test.go | 7 ++- commands/bug/bug_test.go | 50 +++++++------------ doc/man/git-bug-bridge-new.1 | 6 --- doc/man/git-bug-bug.1 | 4 +- doc/md/git-bug_bridge_new.md | 2 +- doc/md/git-bug_bug.md | 2 +- 7 files changed, 58 insertions(+), 99 deletions(-) diff --git a/commands/bug/bug.go b/commands/bug/bug.go index 46b00d3d..a5243c1d 100644 --- a/commands/bug/bug.go +++ b/commands/bug/bug.go @@ -94,10 +94,10 @@ git bug status:open --by creation "foo bar" baz flags.StringVarP(&options.sortDirection, "direction", "d", "asc", "Select the sorting direction. Valid values are [asc,desc]") cmd.RegisterFlagCompletionFunc("direction", completion.From([]string{"asc", "desc"})) - flags.StringVarP(&options.outputFormat, "format", "f", "default", - "Select the output formatting style. Valid values are [default,plain,compact,id,json,org-mode]") + flags.StringVarP(&options.outputFormat, "format", "f", "", + "Select the output formatting style. Valid values are [default,plain,id,json,org-mode]") cmd.RegisterFlagCompletionFunc("format", - completion.From([]string{"default", "plain", "compact", "id", "json", "org-mode"})) + completion.From([]string{"default", "plain", "id", "json", "org-mode"})) const selectGroup = "select" cmd.AddGroup(&cobra.Group{ID: selectGroup, Title: "Implicit selection"}) @@ -147,28 +147,32 @@ func runBug(env *execenv.Env, opts bugOptions, args []string) error { return err } - bugExcerpt := make([]*cache.BugExcerpt, len(allIds)) + excerpts := make([]*cache.BugExcerpt, len(allIds)) for i, id := range allIds { b, err := env.Backend.Bugs().ResolveExcerpt(id) if err != nil { return err } - bugExcerpt[i] = b + excerpts[i] = b } switch opts.outputFormat { - case "org-mode": - return bugsOrgmodeFormatter(env, bugExcerpt) - case "plain": - return bugsPlainFormatter(env, bugExcerpt) - case "json": - return bugsJsonFormatter(env, bugExcerpt) - case "compact": - return bugsCompactFormatter(env, bugExcerpt) - case "id": - return bugsIDFormatter(env, bugExcerpt) + case "": + if env.Out.IsTerminal() { + return bugsDefaultFormatter(env, excerpts) + } else { + return bugsPlainFormatter(env, excerpts) + } case "default": - return bugsDefaultFormatter(env, bugExcerpt) + return bugsDefaultFormatter(env, excerpts) + case "id": + return bugsIDFormatter(env, excerpts) + case "plain": + return bugsPlainFormatter(env, excerpts) + case "json": + return bugsJsonFormatter(env, excerpts) + case "org-mode": + return bugsOrgmodeFormatter(env, excerpts) default: return fmt.Errorf("unknown format %s", opts.outputFormat) } @@ -187,9 +191,9 @@ func repairQuery(args []string) string { return strings.Join(args, " ") } -func bugsJsonFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) error { - jsonBugs := make([]cmdjson.BugExcerpt, len(bugExcerpts)) - for i, b := range bugExcerpts { +func bugsJsonFormatter(env *execenv.Env, excerpts []*cache.BugExcerpt) error { + jsonBugs := make([]cmdjson.BugExcerpt, len(excerpts)) + for i, b := range excerpts { jsonBug, err := cmdjson.NewBugExcerpt(env.Backend, b) if err != nil { return err @@ -199,41 +203,15 @@ func bugsJsonFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) error return env.Out.PrintJSON(jsonBugs) } -func bugsCompactFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) error { - for _, b := range bugExcerpts { - author, err := env.Backend.Identities().ResolveExcerpt(b.AuthorId) - if err != nil { - return err - } - - var labelsTxt strings.Builder - for _, l := range b.Labels { - lc256 := l.Color().Term256() - labelsTxt.WriteString(lc256.Escape()) - labelsTxt.WriteString("◼") - labelsTxt.WriteString(lc256.Unescape()) - } - - env.Out.Printf("%s %s %s %s %s\n", - colors.Cyan(b.Id().Human()), - colors.Yellow(b.Status), - text.LeftPadMaxLine(strings.TrimSpace(b.Title), 40, 0), - text.LeftPadMaxLine(labelsTxt.String(), 5, 0), - colors.Magenta(text.TruncateMax(author.DisplayName(), 15)), - ) - } - return nil -} - -func bugsIDFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) error { - for _, b := range bugExcerpts { +func bugsIDFormatter(env *execenv.Env, excerpts []*cache.BugExcerpt) error { + for _, b := range excerpts { env.Out.Println(b.Id().String()) } return nil } -func bugsDefaultFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) error { +func bugsDefaultFormatter(env *execenv.Env, excerpts []*cache.BugExcerpt) error { width := env.Out.Width() widthId := entity.HumanIdLength widthStatus := len("closed") @@ -252,7 +230,7 @@ func bugsDefaultFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) err widthRemaining = widthRemaining - widthTitle - 3 - 2 widthAuthor := widthRemaining - for _, b := range bugExcerpts { + for _, b := range excerpts { author, err := env.Backend.Identities().ResolveExcerpt(b.AuthorId) if err != nil { return err @@ -290,14 +268,14 @@ func bugsDefaultFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) err return nil } -func bugsPlainFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) error { - for _, b := range bugExcerpts { - env.Out.Printf("%s [%s] %s\n", b.Id().Human(), b.Status, strings.TrimSpace(b.Title)) +func bugsPlainFormatter(env *execenv.Env, excerpts []*cache.BugExcerpt) error { + for _, b := range excerpts { + env.Out.Printf("%s\t%s\t%s\n", b.Id().Human(), b.Status, strings.TrimSpace(b.Title)) } return nil } -func bugsOrgmodeFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) error { +func bugsOrgmodeFormatter(env *execenv.Env, excerpts []*cache.BugExcerpt) error { // see https://orgmode.org/manual/Tags.html orgTagRe := regexp.MustCompile("[^[:alpha:]_@]") formatTag := func(l bug.Label) string { @@ -310,7 +288,7 @@ func bugsOrgmodeFormatter(env *execenv.Env, bugExcerpts []*cache.BugExcerpt) err env.Out.Println("#+TODO: OPEN | CLOSED") - for _, b := range bugExcerpts { + for _, b := range excerpts { status := strings.ToUpper(b.Status.String()) var title string diff --git a/commands/bug/bug_comment_test.go b/commands/bug/bug_comment_test.go index c1dc9952..5625f1be 100644 --- a/commands/bug/bug_comment_test.go +++ b/commands/bug/bug_comment_test.go @@ -2,7 +2,7 @@ package bugcmd import ( "fmt" - "io/ioutil" + "os" "strings" "testing" "time" @@ -10,7 +10,6 @@ import ( "github.com/stretchr/testify/require" "github.com/MichaelMure/git-bug/commands/bug/testenv" - "github.com/MichaelMure/git-bug/commands/cmdtest" "github.com/MichaelMure/git-bug/commands/execenv" ) @@ -143,7 +142,7 @@ func requireCommentsEqual(t *testing.T, golden string, env *execenv.Env) { t.Log("Got here") for i, comment := range comments { fileName := fmt.Sprintf(goldenFilePattern, golden, i) - require.NoError(t, ioutil.WriteFile(fileName, []byte(comment.message), 0644)) + require.NoError(t, os.WriteFile(fileName, []byte(comment.message), 0644)) } } @@ -157,7 +156,7 @@ func requireCommentsEqual(t *testing.T, golden string, env *execenv.Env) { require.Equal(t, date.Add(time.Duration(i)*time.Minute), comment.date) fileName := fmt.Sprintf(goldenFilePattern, golden, i) - exp, err := ioutil.ReadFile(fileName) + exp, err := os.ReadFile(fileName) require.NoError(t, err) require.Equal(t, strings.ReplaceAll(string(exp), "\r", ""), strings.ReplaceAll(comment.message, "\r", "")) } diff --git a/commands/bug/bug_test.go b/commands/bug/bug_test.go index cb6a4373..e6fadd6f 100644 --- a/commands/bug/bug_test.go +++ b/commands/bug/bug_test.go @@ -2,7 +2,6 @@ package bugcmd import ( "encoding/json" - "fmt" "testing" "github.com/stretchr/testify/require" @@ -61,44 +60,33 @@ $` format string exp string }{ - {"default", "^[0-9a-f]{7}\topen\tthis is a bug title \tJohn Doe \t\n$"}, - {"plain", "^[0-9a-f]{7} \\[open\\] this is a bug title\n$"}, - {"compact", "^[0-9a-f]{7} open this is a bug title John Doe\n$"}, + {"default", "^[0-9a-f]{7}\topen\tthis is a bug title John Doe \n$"}, + {"plain", "^[0-9a-f]{7}\topen\tthis is a bug title\n$"}, {"id", "^[0-9a-f]{64}\n$"}, {"org-mode", expOrgMode}, + {"json", ".*"}, } for _, testcase := range cases { - opts := bugOptions{ - sortDirection: "asc", - sortBy: "creation", - outputFormat: testcase.format, - } - - name := fmt.Sprintf("with %s format", testcase.format) - - t.Run(name, func(t *testing.T) { + t.Run(testcase.format, func(t *testing.T) { env, _ := testenv.NewTestEnvAndBug(t) + opts := bugOptions{ + sortDirection: "asc", + sortBy: "creation", + outputFormat: testcase.format, + } + require.NoError(t, runBug(env, opts, []string{})) - require.Regexp(t, testcase.exp, env.Out.String()) + + switch testcase.format { + case "json": + var bugs []cmdjson.BugExcerpt + require.NoError(t, json.Unmarshal(env.Out.Bytes(), &bugs)) + require.Len(t, bugs, 1) + default: + require.Regexp(t, testcase.exp, env.Out.String()) + } }) } - - t.Run("with JSON format", func(t *testing.T) { - opts := bugOptions{ - sortDirection: "asc", - sortBy: "creation", - outputFormat: "json", - } - - env, _ := testenv.NewTestEnvAndBug(t) - - require.NoError(t, runBug(env, opts, []string{})) - - var bugs []cmdjson.BugExcerpt - require.NoError(t, json.Unmarshal(env.Out.Bytes(), &bugs)) - - require.Len(t, bugs, 1) - }) } diff --git a/doc/man/git-bug-bridge-new.1 b/doc/man/git-bug-bridge-new.1 index 1b0b2f38..ffc8c469 100644 --- a/doc/man/git-bug-bridge-new.1 +++ b/doc/man/git-bug-bridge-new.1 @@ -13,14 +13,8 @@ git-bug-bridge-new - Configure a new bridge .SH DESCRIPTION .PP -.RS - -.nf Configure a new bridge by passing flags or/and using interactive terminal prompts. You can avoid all the terminal prompts by passing all the necessary flags to configure your bridge. -.fi -.RE - .SH OPTIONS .PP diff --git a/doc/man/git-bug-bug.1 b/doc/man/git-bug-bug.1 index 6ee62303..c9d2bd65 100644 --- a/doc/man/git-bug-bug.1 +++ b/doc/man/git-bug-bug.1 @@ -61,8 +61,8 @@ You can pass an additional query to filter and order the list. This query can be Select the sorting direction. Valid values are [asc,desc] .PP -\fB-f\fP, \fB--format\fP="default" - Select the output formatting style. Valid values are [default,plain,compact,id,json,org-mode] +\fB-f\fP, \fB--format\fP="" + Select the output formatting style. Valid values are [default,plain,id,json,org-mode] .PP \fB-h\fP, \fB--help\fP[=false] diff --git a/doc/md/git-bug_bridge_new.md b/doc/md/git-bug_bridge_new.md index 81bffd49..5e5724f5 100644 --- a/doc/md/git-bug_bridge_new.md +++ b/doc/md/git-bug_bridge_new.md @@ -4,7 +4,7 @@ Configure a new bridge ### Synopsis - Configure a new bridge by passing flags or/and using interactive terminal prompts. You can avoid all the terminal prompts by passing all the necessary flags to configure your bridge. +Configure a new bridge by passing flags or/and using interactive terminal prompts. You can avoid all the terminal prompts by passing all the necessary flags to configure your bridge. ``` git-bug bridge new [flags] diff --git a/doc/md/git-bug_bug.md b/doc/md/git-bug_bug.md index c040cd16..5b79bb97 100644 --- a/doc/md/git-bug_bug.md +++ b/doc/md/git-bug_bug.md @@ -42,7 +42,7 @@ git bug status:open --by creation "foo bar" baz -n, --no strings Filter by absence of something. Valid values are [label] -b, --by string Sort the results by a characteristic. Valid values are [id,creation,edit] (default "creation") -d, --direction string Select the sorting direction. Valid values are [asc,desc] (default "asc") - -f, --format string Select the output formatting style. Valid values are [default,plain,compact,id,json,org-mode] (default "default") + -f, --format string Select the output formatting style. Valid values are [default,plain,id,json,org-mode] -h, --help help for bug ```