1// Package termlog provides facilities for logging to a terminal geared towards
2// interactive use.
3package termlog
4
5import (
6	"fmt"
7	"io"
8	"os"
9	"sync"
10	"time"
11
12	"github.com/fatih/color"
13	"golang.org/x/crypto/ssh/terminal"
14)
15
16const (
17	say = iota
18	notice
19	warn
20	shout
21	header
22)
23
24const defaultTimeFmt = "15:04:05: "
25const indent = "  "
26
27// A single global output Mutex, because fatih/color has a single global output
28// writer
29var outputMutex = sync.Mutex{}
30
31// Palette defines the colour of output
32type Palette struct {
33	Timestamp *color.Color
34	Say       *color.Color
35	Notice    *color.Color
36	Warn      *color.Color
37	Shout     *color.Color
38	Header    *color.Color
39}
40
41// DefaultPalette is a sensbile default palette, with the following foreground
42// colours:
43//
44// 	Say: Terminal default
45// 	Notice: Blue
46// 	Warn: Yellow
47// 	Shout: Red
48// 	Timestamp: Cyan
49var DefaultPalette = Palette{
50	Say:       color.New(),
51	Notice:    color.New(color.FgBlue),
52	Warn:      color.New(color.FgYellow),
53	Shout:     color.New(color.FgRed),
54	Timestamp: color.New(color.FgCyan),
55	Header:    color.New(color.FgBlue),
56}
57
58// Logger logs things
59type Logger interface {
60	Say(format string, args ...interface{})
61	Notice(format string, args ...interface{})
62	Warn(format string, args ...interface{})
63	Shout(format string, args ...interface{})
64
65	SayAs(name string, format string, args ...interface{})
66	NoticeAs(name string, format string, args ...interface{})
67	WarnAs(name string, format string, args ...interface{})
68	ShoutAs(name string, format string, args ...interface{})
69}
70
71// Group is a collected group of log entries. Logs are only displayed once the
72// Done method is called.
73type Group interface {
74	Logger
75	Done()
76	Quiet()
77}
78
79// Stream is a stream of log entries with a header
80type Stream interface {
81	Logger
82	Quiet()
83	Header()
84}
85
86// TermLog is the top-level termlog interface
87type TermLog interface {
88	Logger
89	Group() Group
90	Stream(header string) Stream
91	Quiet()
92}
93
94type linesource interface {
95	getID() string
96	getHeader() string
97}
98
99type line struct {
100	name   string
101	str    string
102	source linesource
103}
104
105// Log is the top-level log structure
106type Log struct {
107	Palette *Palette
108	TimeFmt string
109	enabled map[string]bool
110	quiet   bool
111	lastid  string
112}
113
114// NewLog creates a new Log instance and initialises it with a set of defaults.
115func NewLog() *Log {
116	l := &Log{
117		Palette: &DefaultPalette,
118		enabled: make(map[string]bool),
119		TimeFmt: defaultTimeFmt,
120	}
121	l.enabled[""] = true
122	if !terminal.IsTerminal(int(os.Stdout.Fd())) || os.Getenv("TERM") == "dumb" {
123		l.Color(false)
124	}
125	return l
126}
127
128func (l *Log) format(timestamp bool, level int, format string, args []interface{}) string {
129	ts := ""
130	if timestamp {
131		f := l.Palette.Timestamp.SprintfFunc()
132		ts = f(
133			"%s", time.Now().Format(l.TimeFmt),
134		)
135	}
136	var p *color.Color
137	switch level {
138	case say:
139		p = l.Palette.Say
140	case notice:
141		p = l.Palette.Notice
142	case warn:
143		p = l.Palette.Warn
144	case shout:
145		p = l.Palette.Shout
146	case header:
147		p = l.Palette.Header
148	default:
149		panic("unknown log level")
150	}
151	return ts + p.SprintfFunc()(format, args...)
152}
153
154// Color sets the state of colour output - true to turn on, false to disable.
155func (*Log) Color(state bool) {
156	color.NoColor = !state
157}
158
159// Enable logging for a specified name
160func (l *Log) Enable(name string) {
161	l.enabled[name] = true
162}
163
164// Quiet disables all output
165func (l *Log) Quiet() {
166	l.quiet = true
167}
168
169func (l *Log) header(source linesource) {
170	l.lastid = source.getID()
171	hdr := l.format(true, header, source.getHeader(), nil)
172	fmt.Fprintf(color.Output, hdr+"\n")
173}
174
175func (l *Log) output(quiet bool, lines ...*line) {
176	if quiet {
177		return
178	}
179	outputMutex.Lock()
180	defer outputMutex.Unlock()
181	for _, line := range lines {
182		if _, ok := l.enabled[line.name]; !ok {
183			continue
184		}
185		id := line.source.getID()
186		if id != "" && id != l.lastid {
187			l.header(line.source)
188		}
189		fmt.Fprintf(color.Output, "%s\n", line.str)
190	}
191}
192
193// Say logs a line
194func (l *Log) Say(format string, args ...interface{}) {
195	l.output(l.quiet, &line{"", l.format(true, say, format, args), l})
196}
197
198// Notice logs a line with the Notice color
199func (l *Log) Notice(format string, args ...interface{}) {
200	l.output(l.quiet, &line{"", l.format(true, notice, format, args), l})
201}
202
203// Warn logs a line with the Warn color
204func (l *Log) Warn(format string, args ...interface{}) {
205	l.output(l.quiet, &line{"", l.format(true, warn, format, args), l})
206}
207
208// Shout logs a line with the Shout color
209func (l *Log) Shout(format string, args ...interface{}) {
210	l.output(l.quiet, &line{"", l.format(true, shout, format, args), l})
211}
212
213// SayAs logs a line
214func (l *Log) SayAs(name string, format string, args ...interface{}) {
215	l.output(l.quiet, &line{name, l.format(true, say, format, args), l})
216}
217
218// NoticeAs logs a line with the Notice color
219func (l *Log) NoticeAs(name string, format string, args ...interface{}) {
220	l.output(l.quiet, &line{name, l.format(true, notice, format, args), l})
221}
222
223// WarnAs logs a line with the Warn color
224func (l *Log) WarnAs(name string, format string, args ...interface{}) {
225	l.output(l.quiet, &line{name, l.format(true, warn, format, args), l})
226}
227
228// ShoutAs logs a line with the Shout color
229func (l *Log) ShoutAs(name string, format string, args ...interface{}) {
230	l.output(l.quiet, &line{name, l.format(true, shout, format, args), l})
231}
232
233// Group creates a new log group
234func (l *Log) Group() Group {
235	return &group{
236		lines: make([]*line, 0),
237		log:   l,
238		quiet: l.quiet,
239	}
240}
241
242// Stream creates a new log group
243func (l *Log) Stream(header string) Stream {
244	return &stream{
245		header: header,
246		log:    l,
247		quiet:  l.quiet,
248	}
249}
250
251func (l *Log) getID() string {
252	return ""
253}
254
255func (l *Log) getHeader() string {
256	return ""
257}
258
259// SetOutput sets the output writer for termlog (stdout by default).
260func SetOutput(w io.Writer) {
261	color.Output = w
262}
263