main
Raw Download raw file
  1package tea
  2
  3import (
  4	"bytes"
  5	"fmt"
  6	"io"
  7	"strings"
  8	"sync"
  9	"time"
 10
 11	"github.com/charmbracelet/x/ansi"
 12	"github.com/muesli/ansi/compressor"
 13)
 14
 15const (
 16	// defaultFramerate specifies the maximum interval at which we should
 17	// update the view.
 18	defaultFPS = 60
 19	maxFPS     = 120
 20)
 21
 22// standardRenderer is a framerate-based terminal renderer, updating the view
 23// at a given framerate to avoid overloading the terminal emulator.
 24//
 25// In cases where very high performance is needed the renderer can be told
 26// to exclude ranges of lines, allowing them to be written to directly.
 27type standardRenderer struct {
 28	mtx *sync.Mutex
 29	out io.Writer
 30
 31	buf                bytes.Buffer
 32	queuedMessageLines []string
 33	framerate          time.Duration
 34	ticker             *time.Ticker
 35	done               chan struct{}
 36	lastRender         string
 37	lastRenderedLines  []string
 38	linesRendered      int
 39	altLinesRendered   int
 40	useANSICompressor  bool
 41	once               sync.Once
 42
 43	// cursor visibility state
 44	cursorHidden bool
 45
 46	// essentially whether or not we're using the full size of the terminal
 47	altScreenActive bool
 48
 49	// whether or not we're currently using bracketed paste
 50	bpActive bool
 51
 52	// reportingFocus whether reporting focus events is enabled
 53	reportingFocus bool
 54
 55	// renderer dimensions; usually the size of the window
 56	width  int
 57	height int
 58
 59	// lines explicitly set not to render
 60	ignoreLines map[int]struct{}
 61}
 62
 63// newRenderer creates a new renderer. Normally you'll want to initialize it
 64// with os.Stdout as the first argument.
 65func newRenderer(out io.Writer, useANSICompressor bool, fps int) renderer {
 66	if fps < 1 {
 67		fps = defaultFPS
 68	} else if fps > maxFPS {
 69		fps = maxFPS
 70	}
 71	r := &standardRenderer{
 72		out:                out,
 73		mtx:                &sync.Mutex{},
 74		done:               make(chan struct{}),
 75		framerate:          time.Second / time.Duration(fps),
 76		useANSICompressor:  useANSICompressor,
 77		queuedMessageLines: []string{},
 78	}
 79	if r.useANSICompressor {
 80		r.out = &compressor.Writer{Forward: out}
 81	}
 82	return r
 83}
 84
 85// start starts the renderer.
 86func (r *standardRenderer) start() {
 87	if r.ticker == nil {
 88		r.ticker = time.NewTicker(r.framerate)
 89	} else {
 90		// If the ticker already exists, it has been stopped and we need to
 91		// reset it.
 92		r.ticker.Reset(r.framerate)
 93	}
 94
 95	// Since the renderer can be restarted after a stop, we need to reset
 96	// the done channel and its corresponding sync.Once.
 97	r.once = sync.Once{}
 98
 99	go r.listen()
100}
101
102// stop permanently halts the renderer, rendering the final frame.
103func (r *standardRenderer) stop() {
104	// Stop the renderer before acquiring the mutex to avoid a deadlock.
105	r.once.Do(func() {
106		r.done <- struct{}{}
107	})
108
109	// flush locks the mutex
110	r.flush()
111
112	r.mtx.Lock()
113	defer r.mtx.Unlock()
114
115	r.execute(ansi.EraseEntireLine)
116	// Move the cursor back to the beginning of the line
117	r.execute("\r")
118
119	if r.useANSICompressor {
120		if w, ok := r.out.(io.WriteCloser); ok {
121			_ = w.Close()
122		}
123	}
124}
125
126// execute writes a sequence to the terminal.
127func (r *standardRenderer) execute(seq string) {
128	_, _ = io.WriteString(r.out, seq)
129}
130
131// kill halts the renderer. The final frame will not be rendered.
132func (r *standardRenderer) kill() {
133	// Stop the renderer before acquiring the mutex to avoid a deadlock.
134	r.once.Do(func() {
135		r.done <- struct{}{}
136	})
137
138	r.mtx.Lock()
139	defer r.mtx.Unlock()
140
141	r.execute(ansi.EraseEntireLine)
142	// Move the cursor back to the beginning of the line
143	r.execute("\r")
144}
145
146// listen waits for ticks on the ticker, or a signal to stop the renderer.
147func (r *standardRenderer) listen() {
148	for {
149		select {
150		case <-r.done:
151			r.ticker.Stop()
152			return
153
154		case <-r.ticker.C:
155			r.flush()
156		}
157	}
158}
159
160// flush renders the buffer.
161func (r *standardRenderer) flush() {
162	r.mtx.Lock()
163	defer r.mtx.Unlock()
164
165	if r.buf.Len() == 0 || r.buf.String() == r.lastRender {
166		// Nothing to do.
167		return
168	}
169
170	// Output buffer.
171	buf := &bytes.Buffer{}
172
173	// Moving to the beginning of the section, that we rendered.
174	if r.altScreenActive {
175		buf.WriteString(ansi.CursorHomePosition)
176	} else if r.linesRendered > 1 {
177		buf.WriteString(ansi.CursorUp(r.linesRendered - 1))
178	}
179
180	newLines := strings.Split(r.buf.String(), "\n")
181
182	// If we know the output's height, we can use it to determine how many
183	// lines we can render. We drop lines from the top of the render buffer if
184	// necessary, as we can't navigate the cursor into the terminal's scrollback
185	// buffer.
186	if r.height > 0 && len(newLines) > r.height {
187		newLines = newLines[len(newLines)-r.height:]
188	}
189
190	flushQueuedMessages := len(r.queuedMessageLines) > 0 && !r.altScreenActive
191
192	if flushQueuedMessages {
193		// Dump the lines we've queued up for printing.
194		for _, line := range r.queuedMessageLines {
195			if ansi.StringWidth(line) < r.width {
196				// We only erase the rest of the line when the line is shorter than
197				// the width of the terminal. When the cursor reaches the end of
198				// the line, any escape sequences that follow will only affect the
199				// last cell of the line.
200
201				// Removing previously rendered content at the end of line.
202				line = line + ansi.EraseLineRight
203			}
204
205			_, _ = buf.WriteString(line)
206			_, _ = buf.WriteString("\r\n")
207		}
208		// Clear the queued message lines.
209		r.queuedMessageLines = []string{}
210	}
211
212	// Paint new lines.
213	for i := 0; i < len(newLines); i++ {
214		canSkip := !flushQueuedMessages && // Queuing messages triggers repaint -> we don't have access to previous frame content.
215			len(r.lastRenderedLines) > i && r.lastRenderedLines[i] == newLines[i] // Previously rendered line is the same.
216
217		if _, ignore := r.ignoreLines[i]; ignore || canSkip {
218			// Unless this is the last line, move the cursor down.
219			if i < len(newLines)-1 {
220				buf.WriteByte('\n')
221			}
222			continue
223		}
224
225		if i == 0 && r.lastRender == "" {
226			// On first render, reset the cursor to the start of the line
227			// before writing anything.
228			buf.WriteByte('\r')
229		}
230
231		line := newLines[i]
232
233		// Truncate lines wider than the width of the window to avoid
234		// wrapping, which will mess up rendering. If we don't have the
235		// width of the window this will be ignored.
236		//
237		// Note that on Windows we only get the width of the window on
238		// program initialization, so after a resize this won't perform
239		// correctly (signal SIGWINCH is not supported on Windows).
240		if r.width > 0 {
241			line = ansi.Truncate(line, r.width, "")
242		}
243
244		if ansi.StringWidth(line) < r.width {
245			// We only erase the rest of the line when the line is shorter than
246			// the width of the terminal. When the cursor reaches the end of
247			// the line, any escape sequences that follow will only affect the
248			// last cell of the line.
249
250			// Removing previously rendered content at the end of line.
251			line = line + ansi.EraseLineRight
252		}
253
254		_, _ = buf.WriteString(line)
255
256		if i < len(newLines)-1 {
257			_, _ = buf.WriteString("\r\n")
258		}
259	}
260
261	// Clearing left over content from last render.
262	if r.lastLinesRendered() > len(newLines) {
263		buf.WriteString(ansi.EraseScreenBelow)
264	}
265
266	if r.altScreenActive {
267		r.altLinesRendered = len(newLines)
268	} else {
269		r.linesRendered = len(newLines)
270	}
271
272	// Make sure the cursor is at the start of the last line to keep rendering
273	// behavior consistent.
274	if r.altScreenActive {
275		// This case fixes a bug in macOS terminal. In other terminals the
276		// other case seems to do the job regardless of whether or not we're
277		// using the full terminal window.
278		buf.WriteString(ansi.CursorPosition(0, len(newLines)))
279	} else {
280		buf.WriteString(ansi.CursorBackward(r.width))
281	}
282
283	_, _ = r.out.Write(buf.Bytes())
284	r.lastRender = r.buf.String()
285
286	// Save previously rendered lines for comparison in the next render. If we
287	// don't do this, we can't skip rendering lines that haven't changed.
288	// See https://github.com/charmbracelet/bubbletea/pull/1233
289	r.lastRenderedLines = newLines
290	r.buf.Reset()
291}
292
293// lastLinesRendered returns the number of lines rendered lastly.
294func (r *standardRenderer) lastLinesRendered() int {
295	if r.altScreenActive {
296		return r.altLinesRendered
297	}
298	return r.linesRendered
299}
300
301// write writes to the internal buffer. The buffer will be outputted via the
302// ticker which calls flush().
303func (r *standardRenderer) write(s string) {
304	r.mtx.Lock()
305	defer r.mtx.Unlock()
306	r.buf.Reset()
307
308	// If an empty string was passed we should clear existing output and
309	// rendering nothing. Rather than introduce additional state to manage
310	// this, we render a single space as a simple (albeit less correct)
311	// solution.
312	if s == "" {
313		s = " "
314	}
315
316	_, _ = r.buf.WriteString(s)
317}
318
319func (r *standardRenderer) repaint() {
320	r.lastRender = ""
321	r.lastRenderedLines = nil
322}
323
324func (r *standardRenderer) clearScreen() {
325	r.mtx.Lock()
326	defer r.mtx.Unlock()
327
328	r.execute(ansi.EraseEntireScreen)
329	r.execute(ansi.CursorHomePosition)
330
331	r.repaint()
332}
333
334func (r *standardRenderer) altScreen() bool {
335	r.mtx.Lock()
336	defer r.mtx.Unlock()
337
338	return r.altScreenActive
339}
340
341func (r *standardRenderer) enterAltScreen() {
342	r.mtx.Lock()
343	defer r.mtx.Unlock()
344
345	if r.altScreenActive {
346		return
347	}
348
349	r.altScreenActive = true
350	r.execute(ansi.SetAltScreenSaveCursorMode)
351
352	// Ensure that the terminal is cleared, even when it doesn't support
353	// alt screen (or alt screen support is disabled, like GNU screen by
354	// default).
355	//
356	// Note: we can't use r.clearScreen() here because the mutex is already
357	// locked.
358	r.execute(ansi.EraseEntireScreen)
359	r.execute(ansi.CursorHomePosition)
360
361	// cmd.exe and other terminals keep separate cursor states for the AltScreen
362	// and the main buffer. We have to explicitly reset the cursor visibility
363	// whenever we enter AltScreen.
364	if r.cursorHidden {
365		r.execute(ansi.HideCursor)
366	} else {
367		r.execute(ansi.ShowCursor)
368	}
369
370	// Entering the alt screen resets the lines rendered count.
371	r.altLinesRendered = 0
372
373	r.repaint()
374}
375
376func (r *standardRenderer) exitAltScreen() {
377	r.mtx.Lock()
378	defer r.mtx.Unlock()
379
380	if !r.altScreenActive {
381		return
382	}
383
384	r.altScreenActive = false
385	r.execute(ansi.ResetAltScreenSaveCursorMode)
386
387	// cmd.exe and other terminals keep separate cursor states for the AltScreen
388	// and the main buffer. We have to explicitly reset the cursor visibility
389	// whenever we exit AltScreen.
390	if r.cursorHidden {
391		r.execute(ansi.HideCursor)
392	} else {
393		r.execute(ansi.ShowCursor)
394	}
395
396	r.repaint()
397}
398
399func (r *standardRenderer) showCursor() {
400	r.mtx.Lock()
401	defer r.mtx.Unlock()
402
403	r.cursorHidden = false
404	r.execute(ansi.ShowCursor)
405}
406
407func (r *standardRenderer) hideCursor() {
408	r.mtx.Lock()
409	defer r.mtx.Unlock()
410
411	r.cursorHidden = true
412	r.execute(ansi.HideCursor)
413}
414
415func (r *standardRenderer) enableMouseCellMotion() {
416	r.mtx.Lock()
417	defer r.mtx.Unlock()
418
419	r.execute(ansi.SetButtonEventMouseMode)
420}
421
422func (r *standardRenderer) disableMouseCellMotion() {
423	r.mtx.Lock()
424	defer r.mtx.Unlock()
425
426	r.execute(ansi.ResetButtonEventMouseMode)
427}
428
429func (r *standardRenderer) enableMouseAllMotion() {
430	r.mtx.Lock()
431	defer r.mtx.Unlock()
432
433	r.execute(ansi.SetAnyEventMouseMode)
434}
435
436func (r *standardRenderer) disableMouseAllMotion() {
437	r.mtx.Lock()
438	defer r.mtx.Unlock()
439
440	r.execute(ansi.ResetAnyEventMouseMode)
441}
442
443func (r *standardRenderer) enableMouseSGRMode() {
444	r.mtx.Lock()
445	defer r.mtx.Unlock()
446
447	r.execute(ansi.SetSgrExtMouseMode)
448}
449
450func (r *standardRenderer) disableMouseSGRMode() {
451	r.mtx.Lock()
452	defer r.mtx.Unlock()
453
454	r.execute(ansi.ResetSgrExtMouseMode)
455}
456
457func (r *standardRenderer) enableBracketedPaste() {
458	r.mtx.Lock()
459	defer r.mtx.Unlock()
460
461	r.execute(ansi.SetBracketedPasteMode)
462	r.bpActive = true
463}
464
465func (r *standardRenderer) disableBracketedPaste() {
466	r.mtx.Lock()
467	defer r.mtx.Unlock()
468
469	r.execute(ansi.ResetBracketedPasteMode)
470	r.bpActive = false
471}
472
473func (r *standardRenderer) bracketedPasteActive() bool {
474	r.mtx.Lock()
475	defer r.mtx.Unlock()
476
477	return r.bpActive
478}
479
480func (r *standardRenderer) enableReportFocus() {
481	r.mtx.Lock()
482	defer r.mtx.Unlock()
483
484	r.execute(ansi.SetFocusEventMode)
485	r.reportingFocus = true
486}
487
488func (r *standardRenderer) disableReportFocus() {
489	r.mtx.Lock()
490	defer r.mtx.Unlock()
491
492	r.execute(ansi.ResetFocusEventMode)
493	r.reportingFocus = false
494}
495
496func (r *standardRenderer) reportFocus() bool {
497	r.mtx.Lock()
498	defer r.mtx.Unlock()
499
500	return r.reportingFocus
501}
502
503// setWindowTitle sets the terminal window title.
504func (r *standardRenderer) setWindowTitle(title string) {
505	r.execute(ansi.SetWindowTitle(title))
506}
507
508// setIgnoredLines specifies lines not to be touched by the standard Bubble Tea
509// renderer.
510func (r *standardRenderer) setIgnoredLines(from int, to int) {
511	// Lock if we're going to be clearing some lines since we don't want
512	// anything jacking our cursor.
513	if r.lastLinesRendered() > 0 {
514		r.mtx.Lock()
515		defer r.mtx.Unlock()
516	}
517
518	if r.ignoreLines == nil {
519		r.ignoreLines = make(map[int]struct{})
520	}
521	for i := from; i < to; i++ {
522		r.ignoreLines[i] = struct{}{}
523	}
524
525	// Erase ignored lines
526	lastLinesRendered := r.lastLinesRendered()
527	if lastLinesRendered > 0 {
528		buf := &bytes.Buffer{}
529
530		for i := lastLinesRendered - 1; i >= 0; i-- {
531			if _, exists := r.ignoreLines[i]; exists {
532				buf.WriteString(ansi.EraseEntireLine)
533			}
534			buf.WriteString(ansi.CUU1)
535		}
536		buf.WriteString(ansi.CursorPosition(0, lastLinesRendered)) // put cursor back
537		_, _ = r.out.Write(buf.Bytes())
538	}
539}
540
541// clearIgnoredLines returns control of any ignored lines to the standard
542// Bubble Tea renderer. That is, any lines previously set to be ignored can be
543// rendered to again.
544func (r *standardRenderer) clearIgnoredLines() {
545	r.ignoreLines = nil
546}
547
548// insertTop effectively scrolls up. It inserts lines at the top of a given
549// area designated to be a scrollable region, pushing everything else down.
550// This is roughly how ncurses does it.
551//
552// To call this function use command ScrollUp().
553//
554// For this to work renderer.ignoreLines must be set to ignore the scrollable
555// region since we are bypassing the normal Bubble Tea renderer here.
556//
557// Because this method relies on the terminal dimensions, it's only valid for
558// full-window applications (generally those that use the alternate screen
559// buffer).
560//
561// This method bypasses the normal rendering buffer and is philosophically
562// different than the normal way we approach rendering in Bubble Tea. It's for
563// use in high-performance rendering, such as a pager that could potentially
564// be rendering very complicated ansi. In cases where the content is simpler
565// standard Bubble Tea rendering should suffice.
566//
567// Deprecated: This option is deprecated and will be removed in a future
568// version of this package.
569func (r *standardRenderer) insertTop(lines []string, topBoundary, bottomBoundary int) {
570	r.mtx.Lock()
571	defer r.mtx.Unlock()
572
573	buf := &bytes.Buffer{}
574
575	buf.WriteString(ansi.SetTopBottomMargins(topBoundary, bottomBoundary))
576	buf.WriteString(ansi.CursorPosition(0, topBoundary))
577	buf.WriteString(ansi.InsertLine(len(lines)))
578	_, _ = buf.WriteString(strings.Join(lines, "\r\n"))
579	buf.WriteString(ansi.SetTopBottomMargins(0, r.height))
580
581	// Move cursor back to where the main rendering routine expects it to be
582	buf.WriteString(ansi.CursorPosition(0, r.lastLinesRendered()))
583
584	_, _ = r.out.Write(buf.Bytes())
585}
586
587// insertBottom effectively scrolls down. It inserts lines at the bottom of
588// a given area designated to be a scrollable region, pushing everything else
589// up. This is roughly how ncurses does it.
590//
591// To call this function use the command ScrollDown().
592//
593// See note in insertTop() for caveats, how this function only makes sense for
594// full-window applications, and how it differs from the normal way we do
595// rendering in Bubble Tea.
596//
597// Deprecated: This option is deprecated and will be removed in a future
598// version of this package.
599func (r *standardRenderer) insertBottom(lines []string, topBoundary, bottomBoundary int) {
600	r.mtx.Lock()
601	defer r.mtx.Unlock()
602
603	buf := &bytes.Buffer{}
604
605	buf.WriteString(ansi.SetTopBottomMargins(topBoundary, bottomBoundary))
606	buf.WriteString(ansi.CursorPosition(0, bottomBoundary))
607	_, _ = buf.WriteString("\r\n" + strings.Join(lines, "\r\n"))
608	buf.WriteString(ansi.SetTopBottomMargins(0, r.height))
609
610	// Move cursor back to where the main rendering routine expects it to be
611	buf.WriteString(ansi.CursorPosition(0, r.lastLinesRendered()))
612
613	_, _ = r.out.Write(buf.Bytes())
614}
615
616// handleMessages handles internal messages for the renderer.
617func (r *standardRenderer) handleMessages(msg Msg) {
618	switch msg := msg.(type) {
619	case repaintMsg:
620		// Force a repaint by clearing the render cache as we slide into a
621		// render.
622		r.mtx.Lock()
623		r.repaint()
624		r.mtx.Unlock()
625
626	case WindowSizeMsg:
627		r.mtx.Lock()
628		r.width = msg.Width
629		r.height = msg.Height
630		r.repaint()
631		r.mtx.Unlock()
632
633	case clearScrollAreaMsg:
634		r.clearIgnoredLines()
635
636		// Force a repaint on the area where the scrollable stuff was in this
637		// update cycle
638		r.mtx.Lock()
639		r.repaint()
640		r.mtx.Unlock()
641
642	case syncScrollAreaMsg:
643		// Re-render scrolling area
644		r.clearIgnoredLines()
645		r.setIgnoredLines(msg.topBoundary, msg.bottomBoundary)
646		r.insertTop(msg.lines, msg.topBoundary, msg.bottomBoundary)
647
648		// Force non-scrolling stuff to repaint in this update cycle
649		r.mtx.Lock()
650		r.repaint()
651		r.mtx.Unlock()
652
653	case scrollUpMsg:
654		r.insertTop(msg.lines, msg.topBoundary, msg.bottomBoundary)
655
656	case scrollDownMsg:
657		r.insertBottom(msg.lines, msg.topBoundary, msg.bottomBoundary)
658
659	case printLineMessage:
660		if !r.altScreenActive {
661			lines := strings.Split(msg.messageBody, "\n")
662			r.mtx.Lock()
663			r.queuedMessageLines = append(r.queuedMessageLines, lines...)
664			r.repaint()
665			r.mtx.Unlock()
666		}
667	}
668}
669
670// HIGH-PERFORMANCE RENDERING STUFF
671
672type syncScrollAreaMsg struct {
673	lines          []string
674	topBoundary    int
675	bottomBoundary int
676}
677
678// SyncScrollArea performs a paint of the entire region designated to be the
679// scrollable area. This is required to initialize the scrollable region and
680// should also be called on resize (WindowSizeMsg).
681//
682// For high-performance, scroll-based rendering only.
683//
684// Deprecated: This option will be removed in a future version of this package.
685func SyncScrollArea(lines []string, topBoundary int, bottomBoundary int) Cmd {
686	return func() Msg {
687		return syncScrollAreaMsg{
688			lines:          lines,
689			topBoundary:    topBoundary,
690			bottomBoundary: bottomBoundary,
691		}
692	}
693}
694
695type clearScrollAreaMsg struct{}
696
697// ClearScrollArea deallocates the scrollable region and returns the control of
698// those lines to the main rendering routine.
699//
700// For high-performance, scroll-based rendering only.
701//
702// Deprecated: This option will be removed in a future version of this package.
703func ClearScrollArea() Msg {
704	return clearScrollAreaMsg{}
705}
706
707type scrollUpMsg struct {
708	lines          []string
709	topBoundary    int
710	bottomBoundary int
711}
712
713// ScrollUp adds lines to the top of the scrollable region, pushing existing
714// lines below down. Lines that are pushed out the scrollable region disappear
715// from view.
716//
717// For high-performance, scroll-based rendering only.
718//
719// Deprecated: This option will be removed in a future version of this package.
720func ScrollUp(newLines []string, topBoundary, bottomBoundary int) Cmd {
721	return func() Msg {
722		return scrollUpMsg{
723			lines:          newLines,
724			topBoundary:    topBoundary,
725			bottomBoundary: bottomBoundary,
726		}
727	}
728}
729
730type scrollDownMsg struct {
731	lines          []string
732	topBoundary    int
733	bottomBoundary int
734}
735
736// ScrollDown adds lines to the bottom of the scrollable region, pushing
737// existing lines above up. Lines that are pushed out of the scrollable region
738// disappear from view.
739//
740// For high-performance, scroll-based rendering only.
741//
742// Deprecated: This option will be removed in a future version of this package.
743func ScrollDown(newLines []string, topBoundary, bottomBoundary int) Cmd {
744	return func() Msg {
745		return scrollDownMsg{
746			lines:          newLines,
747			topBoundary:    topBoundary,
748			bottomBoundary: bottomBoundary,
749		}
750	}
751}
752
753type printLineMessage struct {
754	messageBody string
755}
756
757// Println prints above the Program. This output is unmanaged by the program and
758// will persist across renders by the Program.
759//
760// Unlike fmt.Println (but similar to log.Println) the message will be print on
761// its own line.
762//
763// If the altscreen is active no output will be printed.
764func Println(args ...interface{}) Cmd {
765	return func() Msg {
766		return printLineMessage{
767			messageBody: fmt.Sprint(args...),
768		}
769	}
770}
771
772// Printf prints above the Program. It takes a format template followed by
773// values similar to fmt.Printf. This output is unmanaged by the program and
774// will persist across renders by the Program.
775//
776// Unlike fmt.Printf (but similar to log.Printf) the message will be print on
777// its own line.
778//
779// If the altscreen is active no output will be printed.
780func Printf(template string, args ...interface{}) Cmd {
781	return func() Msg {
782		return printLineMessage{
783			messageBody: fmt.Sprintf(template, args...),
784		}
785	}
786}