package ansi import ( "bytes" "errors" "fmt" "io" "strings" "unicode/utf8" "charm.land/lipgloss/v2" "github.com/charmbracelet/x/ansi" ) // MarginWriter is a Writer that applies indentation and padding around // whatever you write to it. type MarginWriter struct { w io.Writer iw *IndentWriter } // NewMarginWriter returns a new MarginWriter. func NewMarginWriter(ctx RenderContext, w io.Writer, rules StyleBlock) *MarginWriter { bs := ctx.blockStack var indentation uint var margin uint if rules.Indent != nil { indentation = *rules.Indent } if rules.Margin != nil { margin = *rules.Margin } pw := NewPaddingWriter(w, int(bs.Width(ctx)), func(_ io.Writer) { _, _ = renderText(w, rules.StylePrimitive, " ") }) ic := " " if rules.IndentToken != nil { ic = *rules.IndentToken } iw := NewIndentWriter(pw, int(indentation+margin), func(_ io.Writer) { _, _ = renderText(w, bs.Parent().Style.StylePrimitive, ic) }) return &MarginWriter{ w: lipgloss.NewWrapWriter(w), iw: iw, } } // Write writes to the margin writer and implements [io.Writer]. func (w *MarginWriter) Write(b []byte) (int, error) { n, err := w.iw.Write(b) if err != nil { return 0, fmt.Errorf("glamour: error writing bytes: %w", err) } return n, nil } // Close closes the [MarginWriter]. func (w *MarginWriter) Close() error { var werr error if c, ok := w.w.(io.WriteCloser); ok { werr = c.Close() } return errors.Join(werr, w.iw.Close()) } // PaddingFunc is a function that applies padding around whatever you write to it. type PaddingFunc = func(w io.Writer) // PaddingWriter is a writer that applies padding around whatever you write to // it. type PaddingWriter struct { Padding int PadFunc PaddingFunc w *lipgloss.WrapWriter cache bytes.Buffer } // NewPaddingWriter returns a new PaddingWriter. func NewPaddingWriter(w io.Writer, padding int, padFunc PaddingFunc) *PaddingWriter { return &PaddingWriter{ Padding: padding, PadFunc: padFunc, w: lipgloss.NewWrapWriter(w), } } // Write writes to the padding writer. func (w *PaddingWriter) Write(p []byte) (int, error) { // Use UTF-8 aware iteration to properly handle multi-byte characters (e.g., CJK) for i := 0; i < len(p); { r, size := utf8.DecodeRune(p[i:]) if r == '\n' { //nolint:nestif line := w.cache.String() linew := ansi.StringWidth(line) if w.Padding > 0 && linew < w.Padding { if w.PadFunc != nil { for n := 0; n < w.Padding-linew; n++ { w.PadFunc(w.w) } } else { _, err := io.WriteString(w.w, strings.Repeat(" ", w.Padding-linew)) if err != nil { return 0, fmt.Errorf("glamour: error writing padding: %w", err) } } } w.cache.Reset() } else { // Write complete UTF-8 character bytes to cache w.cache.Write(p[i : i+size]) } // Write complete UTF-8 character bytes to output _, err := w.w.Write(p[i : i+size]) if err != nil { return 0, fmt.Errorf("glamour: error writing bytes: %w", err) } i += size } return len(p), nil } // Close closes the [PaddingWriter]. func (w *PaddingWriter) Close() error { return w.w.Close() //nolint:wrapcheck } // IndentFunc is a function that applies indentation around whatever you write to // it. type IndentFunc = func(w io.Writer) // IndentWriter is a writer that applies indentation around whatever you write to // it. type IndentWriter struct { Indent int IndentFunc PaddingFunc w io.Writer pw *lipgloss.WrapWriter skipIndent bool } // NewIndentWriter returns a new IndentWriter. func NewIndentWriter(w io.Writer, indent int, indentFunc IndentFunc) *IndentWriter { return &IndentWriter{ Indent: indent, IndentFunc: indentFunc, pw: lipgloss.NewWrapWriter(w), w: w, } } func (w *IndentWriter) resetPen() { style := w.pw.Style() link := w.pw.Link() if !style.IsZero() { _, _ = io.WriteString(w.w, ansi.ResetStyle) } if !link.IsZero() { _, _ = io.WriteString(w.w, ansi.ResetHyperlink()) } } func (w *IndentWriter) restorePen() { style := w.pw.Style() link := w.pw.Link() if !style.IsZero() { _, _ = io.WriteString(w.w, style.String()) } if !link.IsZero() { _, _ = io.WriteString(w.w, ansi.SetHyperlink(link.URL, link.Params)) } } // Write writes to the indentation writer. func (w *IndentWriter) Write(p []byte) (int, error) { // Use UTF-8 aware iteration to properly handle multi-byte characters (e.g., CJK) for i := 0; i < len(p); { r, size := utf8.DecodeRune(p[i:]) if !w.skipIndent { w.resetPen() if w.IndentFunc != nil { for j := 0; j < w.Indent; j++ { w.IndentFunc(w.pw) } } else { _, err := io.WriteString(w.pw, strings.Repeat(" ", w.Indent)) if err != nil { return 0, fmt.Errorf("glamour: error writing indentation: %w", err) } } w.skipIndent = true w.restorePen() } if r == '\n' { w.skipIndent = false } // Write complete UTF-8 character bytes to output _, err := w.pw.Write(p[i : i+size]) if err != nil { return 0, fmt.Errorf("glamour: error writing bytes: %w", err) } i += size } return len(p), nil } // Close closes the [IndentWriter]. func (w *IndentWriter) Close() error { // Close the wrap writer (w.pw) before the downstream writer (w.w). w.pw // wraps w.w, so its Close flushes a trailing style/link reset back through // w.w. Closing w.w first would return its parser to the pool and nil it // out, turning that flush into a write on a closed writer. werr := w.pw.Close() if c, ok := w.w.(io.WriteCloser); ok { werr = errors.Join(werr, c.Close()) } return werr }