internal/control/view.go

e6cd75b5f28bacf51620bb531320c30fd4e66bfd
gitbay/internal/control/view.go history · blame · raw

163 lines · 4283 bytes

14 symbols in this file
  1package control
  2
  3import (
  4	"fmt"
  5	"io"
  6	"strings"
  7
  8	"gitbay.org/gitbay/internal/store"
  9	"gitbay.org/gitbay/internal/termtext"
 10)
 11
 12// when is a stored timestamp in a view: the web's format at a
 13// terminal, RFC3339 to the second when plain.
 14func (c *Ctx) when(s string) string {
 15	if c.Term.Cols == 0 {
 16		return stamp(s)
 17	}
 18	t, ok := parseStamp(s)
 19	if !ok {
 20		return s
 21	}
 22	return t.Format("2006-01-02 15:04 UTC")
 23}
 24
 25// siteURL is the instance's address with path segments appended.
 26func (c *Ctx) siteURL(parts ...string) string {
 27	return strings.TrimRight(c.Cfg.Server.SiteURL, "/") + "/" + strings.Join(parts, "/")
 28}
 29
 30// view lays out a show piped: a title line, aligned fields, a body,
 31// events, comments. A terminal draws a screen instead (screen.go); a
 32// caller that still reaches view at a terminal gets the same plain
 33// layout, with user text made safe.
 34type view struct {
 35	c     *Ctx
 36	w     io.Writer
 37	wrote bool // has this view written anything yet
 38}
 39
 40func (c *Ctx) view(w io.Writer) *view { return &view{c: c, w: w} }
 41
 42func (v *view) opts() termtext.Options {
 43	return termtext.Options{Base: v.c.Cfg.Server.SiteURL}
 44}
 45
 46// sep writes a blank line before the next block, unless this view has
 47// written nothing yet: fields, a body or a section as the first thing a
 48// command prints (admin runners, admin stats, notifications settings
 49// show, ...) does not open with an empty line.
 50func (v *view) sep() {
 51	if v.wrote {
 52		io.WriteString(v.w, "\n")
 53	}
 54	v.wrote = true
 55}
 56
 57// section prints a sub-table's label: a blank line, then "label:".
 58// Callers skip the call entirely when the table it introduces has no
 59// rows.
 60func (v *view) section(label string) {
 61	v.sep()
 62	io.WriteString(v.w, label+":\n")
 63}
 64
 65// title prints "ref  title  state" on one line. title or state may be
 66// "": either is skipped rather than leaving a trailing blank field.
 67func (v *view) title(ref, title, state string) {
 68	v.sep()
 69	t := v.c.Term
 70	ref, title, state = t.safe(ref), t.safe(title), t.safe(state)
 71	line := ref
 72	if title != "" {
 73		line += "  " + title
 74	}
 75	if state != "" {
 76		line += "  " + state
 77	}
 78	io.WriteString(v.w, line+"\n")
 79}
 80
 81// fields prints key/value pairs aligned on the widest key, skipping
 82// empty values.
 83func (v *view) fields(kv ...string) {
 84	wide := 0
 85	for i := 0; i+1 < len(kv); i += 2 {
 86		if kv[i+1] != "" {
 87			wide = max(wide, cells(kv[i]))
 88		}
 89	}
 90	v.sep()
 91	for i := 0; i+1 < len(kv); i += 2 {
 92		key, val := v.c.Term.safe(kv[i]), v.c.Term.safe(kv[i+1])
 93		if val == "" {
 94			continue
 95		}
 96		io.WriteString(v.w, "  "+pad(key, wide)+"  "+val+"\n")
 97	}
 98}
 99
100func (v *view) body(src, format string) {
101	if strings.TrimSpace(src) == "" {
102		return
103	}
104	v.sep()
105	src = v.c.Term.safe(src)
106	for _, line := range strings.Split(strings.TrimRight(termtext.Render(src, format, v.opts()), "\n"), "\n") {
107		if line == "" {
108			io.WriteString(v.w, "\n")
109			continue
110		}
111		io.WriteString(v.w, "  "+line+"\n")
112	}
113}
114
115// text writes src as it is, indented: a commit message, which has no
116// markup to render.
117func (v *view) text(src string) {
118	src = strings.TrimRight(v.c.Term.safe(src), "\n")
119	if strings.TrimSpace(src) == "" {
120		return
121	}
122	v.sep()
123	for _, line := range strings.Split(src, "\n") {
124		if line == "" {
125			io.WriteString(v.w, "\n")
126			continue
127		}
128		io.WriteString(v.w, "  "+line+"\n")
129	}
130}
131
132// event is one line for a system comment: its text without link
133// targets, then the time.
134func (v *view) event(text, format, ts string) {
135	line := "· " + termtext.Inline(v.c.Term.safe(text), format)
136	io.WriteString(v.w, "  "+line+"  "+v.c.Term.safe(stamp(ts))+"\n")
137	v.wrote = true
138}
139
140func (v *view) comment(id int64, author, ts, body, format string) {
141	when := v.c.Term.safe(stamp(ts)) + fmt.Sprintf(" (comment %d)", id)
142	head := "── " + v.c.Term.safe(author) + ", " + when + " "
143	v.sep()
144	io.WriteString(v.w, head+"\n")
145	v.body(body, format)
146}
147
148// reactions prints one line of counts under an item, the caller's own
149// marked "(you)". Nothing when nobody has reacted.
150func (v *view) reactions(rs []ReactionOut) {
151	if len(rs) == 0 {
152		return
153	}
154	var parts []string
155	for _, r := range rs {
156		p := fmt.Sprintf("%s %d", store.ReactionEmoji(r.Reaction), r.Count)
157		if r.Me {
158			p += " (you)"
159		}
160		parts = append(parts, p)
161	}
162	io.WriteString(v.w, "reactions: "+strings.Join(parts, ", ")+"\n")
163}