termui: display an explicit placeholder for empty messages

Michael Muré created

Change summary

bug/timeline.go            |   7 ++
graphql/graph/gen_graph.go | 114 ++++++++++++++++++++++++++++++++++-----
graphql/timeline.graphql   |   2 
termui/show_bug.go         |  39 ++++++++++---
util/colors/colors.go      |   1 
5 files changed, 137 insertions(+), 26 deletions(-)

Detailed changes

bug/timeline.go 🔗

@@ -1,6 +1,8 @@
 package bug
 
 import (
+	"strings"
+
 	"github.com/MichaelMure/git-bug/util/git"
 )
 
@@ -67,3 +69,8 @@ func (c *CommentTimelineItem) Append(comment Comment) {
 func (c *CommentTimelineItem) Edited() bool {
 	return len(c.History) > 1
 }
+
+// MessageIsEmpty return true is the message is empty or only made of spaces
+func (c *CommentTimelineItem) MessageIsEmpty() bool {
+	return len(strings.TrimSpace(c.Message)) == 0
+}

graphql/graph/gen_graph.go 🔗

@@ -68,14 +68,15 @@ type ComplexityRoot struct {
 	}
 
 	AddCommentTimelineItem struct {
-		Hash      func(childComplexity int) int
-		Author    func(childComplexity int) int
-		Message   func(childComplexity int) int
-		Files     func(childComplexity int) int
-		CreatedAt func(childComplexity int) int
-		LastEdit  func(childComplexity int) int
-		Edited    func(childComplexity int) int
-		History   func(childComplexity int) int
+		Hash           func(childComplexity int) int
+		Author         func(childComplexity int) int
+		Message        func(childComplexity int) int
+		MessageIsEmpty func(childComplexity int) int
+		Files          func(childComplexity int) int
+		CreatedAt      func(childComplexity int) int
+		LastEdit       func(childComplexity int) int
+		Edited         func(childComplexity int) int
+		History        func(childComplexity int) int
 	}
 
 	Bug struct {
@@ -137,14 +138,15 @@ type ComplexityRoot struct {
 	}
 
 	CreateTimelineItem struct {
-		Hash      func(childComplexity int) int
-		Author    func(childComplexity int) int
-		Message   func(childComplexity int) int
-		Files     func(childComplexity int) int
-		CreatedAt func(childComplexity int) int
-		LastEdit  func(childComplexity int) int
-		Edited    func(childComplexity int) int
-		History   func(childComplexity int) int
+		Hash           func(childComplexity int) int
+		Author         func(childComplexity int) int
+		Message        func(childComplexity int) int
+		MessageIsEmpty func(childComplexity int) int
+		Files          func(childComplexity int) int
+		CreatedAt      func(childComplexity int) int
+		LastEdit       func(childComplexity int) int
+		Edited         func(childComplexity int) int
+		History        func(childComplexity int) int
 	}
 
 	EditCommentOperation struct {
@@ -1051,6 +1053,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.AddCommentTimelineItem.Message(childComplexity), true
 
+	case "AddCommentTimelineItem.messageIsEmpty":
+		if e.complexity.AddCommentTimelineItem.MessageIsEmpty == nil {
+			break
+		}
+
+		return e.complexity.AddCommentTimelineItem.MessageIsEmpty(childComplexity), true
+
 	case "AddCommentTimelineItem.files":
 		if e.complexity.AddCommentTimelineItem.Files == nil {
 			break
@@ -1360,6 +1369,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.CreateTimelineItem.Message(childComplexity), true
 
+	case "CreateTimelineItem.messageIsEmpty":
+		if e.complexity.CreateTimelineItem.MessageIsEmpty == nil {
+			break
+		}
+
+		return e.complexity.CreateTimelineItem.MessageIsEmpty(childComplexity), true
+
 	case "CreateTimelineItem.files":
 		if e.complexity.CreateTimelineItem.Files == nil {
 			break
@@ -2183,6 +2199,11 @@ func (ec *executionContext) _AddCommentTimelineItem(ctx context.Context, sel ast
 			if out.Values[i] == graphql.Null {
 				invalid = true
 			}
+		case "messageIsEmpty":
+			out.Values[i] = ec._AddCommentTimelineItem_messageIsEmpty(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				invalid = true
+			}
 		case "files":
 			out.Values[i] = ec._AddCommentTimelineItem_files(ctx, field, obj)
 			if out.Values[i] == graphql.Null {
@@ -2309,6 +2330,33 @@ func (ec *executionContext) _AddCommentTimelineItem_message(ctx context.Context,
 	return graphql.MarshalString(res)
 }
 
+// nolint: vetshadow
+func (ec *executionContext) _AddCommentTimelineItem_messageIsEmpty(ctx context.Context, field graphql.CollectedField, obj *bug.AddCommentTimelineItem) graphql.Marshaler {
+	ctx = ec.Tracer.StartFieldExecution(ctx, field)
+	defer func() { ec.Tracer.EndFieldExecution(ctx) }()
+	rctx := &graphql.ResolverContext{
+		Object: "AddCommentTimelineItem",
+		Args:   nil,
+		Field:  field,
+	}
+	ctx = graphql.WithResolverContext(ctx, rctx)
+	ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
+	resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.MessageIsEmpty(), nil
+	})
+	if resTmp == nil {
+		if !ec.HasError(rctx) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(bool)
+	rctx.Result = res
+	ctx = ec.Tracer.StartFieldChildExecution(ctx)
+	return graphql.MarshalBoolean(res)
+}
+
 // nolint: vetshadow
 func (ec *executionContext) _AddCommentTimelineItem_files(ctx context.Context, field graphql.CollectedField, obj *bug.AddCommentTimelineItem) graphql.Marshaler {
 	ctx = ec.Tracer.StartFieldExecution(ctx, field)
@@ -4022,6 +4070,11 @@ func (ec *executionContext) _CreateTimelineItem(ctx context.Context, sel ast.Sel
 			if out.Values[i] == graphql.Null {
 				invalid = true
 			}
+		case "messageIsEmpty":
+			out.Values[i] = ec._CreateTimelineItem_messageIsEmpty(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				invalid = true
+			}
 		case "files":
 			out.Values[i] = ec._CreateTimelineItem_files(ctx, field, obj)
 			if out.Values[i] == graphql.Null {
@@ -4148,6 +4201,33 @@ func (ec *executionContext) _CreateTimelineItem_message(ctx context.Context, fie
 	return graphql.MarshalString(res)
 }
 
+// nolint: vetshadow
+func (ec *executionContext) _CreateTimelineItem_messageIsEmpty(ctx context.Context, field graphql.CollectedField, obj *bug.CreateTimelineItem) graphql.Marshaler {
+	ctx = ec.Tracer.StartFieldExecution(ctx, field)
+	defer func() { ec.Tracer.EndFieldExecution(ctx) }()
+	rctx := &graphql.ResolverContext{
+		Object: "CreateTimelineItem",
+		Args:   nil,
+		Field:  field,
+	}
+	ctx = graphql.WithResolverContext(ctx, rctx)
+	ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
+	resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.MessageIsEmpty(), nil
+	})
+	if resTmp == nil {
+		if !ec.HasError(rctx) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(bool)
+	rctx.Result = res
+	ctx = ec.Tracer.StartFieldChildExecution(ctx)
+	return graphql.MarshalBoolean(res)
+}
+
 // nolint: vetshadow
 func (ec *executionContext) _CreateTimelineItem_files(ctx context.Context, field graphql.CollectedField, obj *bug.CreateTimelineItem) graphql.Marshaler {
 	ctx = ec.Tracer.StartFieldExecution(ctx, field)
@@ -9079,6 +9159,7 @@ type CreateTimelineItem implements TimelineItem {
     hash: Hash!
     author: Person!
     message: String!
+    messageIsEmpty: Boolean!
     files: [Hash!]!
     createdAt: Time!
     lastEdit: Time!
@@ -9092,6 +9173,7 @@ type AddCommentTimelineItem implements TimelineItem {
     hash: Hash!
     author: Person!
     message: String!
+    messageIsEmpty: Boolean!
     files: [Hash!]!
     createdAt: Time!
     lastEdit: Time!

graphql/timeline.graphql 🔗

@@ -34,6 +34,7 @@ type CreateTimelineItem implements TimelineItem {
     hash: Hash!
     author: Person!
     message: String!
+    messageIsEmpty: Boolean!
     files: [Hash!]!
     createdAt: Time!
     lastEdit: Time!
@@ -47,6 +48,7 @@ type AddCommentTimelineItem implements TimelineItem {
     hash: Hash!
     author: Person!
     message: String!
+    messageIsEmpty: Boolean!
     files: [Hash!]!
     createdAt: Time!
     lastEdit: Time!

termui/show_bug.go 🔗

@@ -95,7 +95,7 @@ func (sb *showBug) layout(g *gocui.Gui) error {
 	}
 
 	v.Clear()
-	fmt.Fprintf(v, "[q] Save and return [←↓↑→,hjkl] Navigation [o] Toggle open/close [e] Edit [c] Comment [t] Change title")
+	_, _ = fmt.Fprintf(v, "[q] Save and return [←↓↑→,hjkl] Navigation [o] Toggle open/close [e] Edit [c] Comment [t] Change title")
 
 	_, err = g.SetViewOnTop(showBugInstructionView)
 	if err != nil {
@@ -228,7 +228,7 @@ func (sb *showBug) renderMain(g *gocui.Gui, mainView *gocui.View) error {
 		return err
 	}
 
-	fmt.Fprint(v, bugHeader)
+	_, _ = fmt.Fprint(v, bugHeader)
 	y0 += lines + 1
 
 	for _, op := range snap.Timeline {
@@ -241,13 +241,21 @@ func (sb *showBug) renderMain(g *gocui.Gui, mainView *gocui.View) error {
 
 		case *bug.CreateTimelineItem:
 			create := op.(*bug.CreateTimelineItem)
-			content, lines := text.WrapLeftPadded(create.Message, maxX-1, 4)
+
+			var content string
+			var lines int
+
+			if create.MessageIsEmpty() {
+				content, lines = text.WrapLeftPadded(emptyMessagePlaceholder(), maxX-1, 4)
+			} else {
+				content, lines = text.WrapLeftPadded(create.Message, maxX-1, 4)
+			}
 
 			v, err := sb.createOpView(g, viewName, x0, y0, maxX+1, lines, true)
 			if err != nil {
 				return err
 			}
-			fmt.Fprint(v, content)
+			_, _ = fmt.Fprint(v, content)
 			y0 += lines + 2
 
 		case *bug.AddCommentTimelineItem:
@@ -258,7 +266,13 @@ func (sb *showBug) renderMain(g *gocui.Gui, mainView *gocui.View) error {
 				edited = " (edited)"
 			}
 
-			message, _ := text.WrapLeftPadded(comment.Message, maxX-1, 4)
+			var message string
+			if comment.MessageIsEmpty() {
+				message, _ = text.WrapLeftPadded(emptyMessagePlaceholder(), maxX-1, 4)
+			} else {
+				message, _ = text.WrapLeftPadded(comment.Message, maxX-1, 4)
+			}
+
 			content := fmt.Sprintf("%s commented on %s%s\n\n%s",
 				colors.Magenta(comment.Author.DisplayName()),
 				comment.CreatedAt.Time().Format(timeLayout),
@@ -271,7 +285,7 @@ func (sb *showBug) renderMain(g *gocui.Gui, mainView *gocui.View) error {
 			if err != nil {
 				return err
 			}
-			fmt.Fprint(v, content)
+			_, _ = fmt.Fprint(v, content)
 			y0 += lines + 2
 
 		case *bug.SetTitleTimelineItem:
@@ -288,7 +302,7 @@ func (sb *showBug) renderMain(g *gocui.Gui, mainView *gocui.View) error {
 			if err != nil {
 				return err
 			}
-			fmt.Fprint(v, content)
+			_, _ = fmt.Fprint(v, content)
 			y0 += lines + 2
 
 		case *bug.SetStatusTimelineItem:
@@ -305,7 +319,7 @@ func (sb *showBug) renderMain(g *gocui.Gui, mainView *gocui.View) error {
 			if err != nil {
 				return err
 			}
-			fmt.Fprint(v, content)
+			_, _ = fmt.Fprint(v, content)
 			y0 += lines + 2
 
 		case *bug.LabelChangeTimelineItem:
@@ -354,7 +368,7 @@ func (sb *showBug) renderMain(g *gocui.Gui, mainView *gocui.View) error {
 			if err != nil {
 				return err
 			}
-			fmt.Fprint(v, content)
+			_, _ = fmt.Fprint(v, content)
 			y0 += lines + 2
 		}
 	}
@@ -362,6 +376,11 @@ func (sb *showBug) renderMain(g *gocui.Gui, mainView *gocui.View) error {
 	return nil
 }
 
+// emptyMessagePlaceholder return a formatted placeholder for an empty message
+func emptyMessagePlaceholder() string {
+	return colors.GreyBold("No description provided.")
+}
+
 func (sb *showBug) createOpView(g *gocui.Gui, name string, x0 int, y0 int, maxX int, height int, selectable bool) (*gocui.View, error) {
 	v, err := g.SetView(name, x0, y0, maxX, y0+height+1)
 
@@ -423,7 +442,7 @@ func (sb *showBug) renderSidebar(g *gocui.Gui, sideView *gocui.View) error {
 		return err
 	}
 
-	fmt.Fprint(v, content)
+	_, _ = fmt.Fprint(v, content)
 
 	return nil
 }

util/colors/colors.go 🔗

@@ -13,6 +13,7 @@ var (
 	YellowBg   = color.New(color.BgYellow, color.FgBlack).SprintFunc()
 	Green      = color.New(color.FgGreen).SprintFunc()
 	GreenBg    = color.New(color.BgGreen, color.FgBlack).SprintFunc()
+	GreyBold   = color.New(color.BgBlack, color.Bold).SprintfFunc()
 	Red        = color.New(color.FgRed).SprintFunc()
 	Cyan       = color.New(color.FgCyan).SprintFunc()
 	CyanBg     = color.New(color.BgCyan, color.FgBlack).SprintFunc()