internal/control/view.go
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}