1package e2e23import (4 "cmp"5 "fmt"6 "html"7 "strings"89 ghostty "go.mitchellh.com/libghostty"10)1112// renderHTML draws the screen as HTML from libghostty's cell grid, one13// span per run of identically styled cells. Colors are palette CSS14// variables so a gallery can show the same snapshot under any theme.15//16// We don't use libghostty's HTML formatter because it paints erased cells17// with the style of the text before them, which misrepresents what a18// terminal shows.19func renderHTML(vt *ghostty.Terminal) (string, error) {20 cols, err := vt.Cols()21 if err != nil {22 return "", err23 }24 rows, err := vt.Rows()25 if err != nil {26 return "", err27 }28 var b strings.Builder29 for y := range uint32(rows) {30 var line spanWriter31 for x := range cols {32 text, css, ok, err := cellHTML(vt, ghostty.Point{Tag: ghostty.PointTagActive, X: x, Y: y})33 if err != nil {34 return "", err35 }36 if ok {37 line.add(text, css)38 }39 }40 b.WriteString(strings.TrimRight(line.String(), " "))41 b.WriteByte('\n')42 }43 return `<pre class="screen">` + strings.TrimRight(b.String(), "\n") + "</pre>", nil44}4546// spanWriter writes a line of cells, one span per run of cells with the47// same CSS; unstyled runs are bare text.48type spanWriter struct {49 out, run strings.Builder50 style string51}5253// add appends a cell's text with its style.54func (w *spanWriter) add(text, css string) {55 if css != w.style {56 w.flush()57 w.style = css58 }59 w.run.WriteString(html.EscapeString(text))60}6162func (w *spanWriter) flush() {63 if w.run.Len() == 0 {64 return65 }66 if w.style == "" {67 w.out.WriteString(w.run.String())68 } else {69 fmt.Fprintf(&w.out, `<span style="%s">%s</span>`, w.style, w.run.String())70 }71 w.run.Reset()72}7374// String is the line so far.75func (w *spanWriter) String() string {76 w.flush()77 return w.out.String()78}7980// cellHTML returns the text of the cell at p and its CSS; ok is false81// for the second half of a wide character, which draws nothing.82func cellHTML(vt *ghostty.Terminal, p ghostty.Point) (text, css string, ok bool, err error) {83 ref, err := vt.GridRef(p)84 if err != nil {85 return "", "", false, err86 }87 cell, err := ref.Cell()88 if err != nil {89 return "", "", false, err90 }91 if wide, _ := cell.Wide(); wide == ghostty.CellWideSpacerTail {92 return "", "", false, nil93 }94 text = " "95 if has, _ := cell.HasText(); has {96 cps, _ := ref.Graphemes()97 var sb strings.Builder98 for _, cp := range cps {99 sb.WriteRune(rune(cp))100 }101 text = sb.String()102 }103 if st, err := ref.Style(); err == nil && !st.IsDefault() {104 css = styleCSS(st, strings.TrimSpace(text) == "")105 }106 // Cells erased with a background color hold just that color.107 switch tag, _ := cell.ContentTag(); tag {108 case ghostty.CellContentBgColorPalette:109 p, _ := cell.ColorPalette()110 css = fmt.Sprintf("background:var(--vt-palette-%d)", p)111 case ghostty.CellContentBgColorRGB:112 c, _ := cell.ColorRGB()113 css = fmt.Sprintf("background:#%02x%02x%02x", c.R, c.G, c.B)114 }115 return text, css, true, nil116}117118// styleCSS converts a cell style to CSS. For blank cells only what shows119// is kept (background, lines), so snapshots don't depend on whether the120// renderer wrote spaces or erased cells.121func styleCSS(st *ghostty.Style, blank bool) string {122 fg, bg := cssColor(st.FgColor(), ""), cssColor(st.BgColor(), "")123 if st.Inverse() {124 fg, bg = cmp.Or(bg, "var(--bg)"), cmp.Or(fg, "var(--fg)")125 }126 var parts []string127 if fg != "" && !blank {128 parts = append(parts, "color:"+fg)129 }130 if bg != "" {131 parts = append(parts, "background:"+bg)132 }133 if st.Bold() && !blank {134 parts = append(parts, "font-weight:bold")135 }136 if st.Italic() && !blank {137 parts = append(parts, "font-style:italic")138 }139 if st.Faint() && !blank {140 parts = append(parts, "opacity:.6")141 }142 if u := st.Underline(); u != ghostty.UnderlineNone {143 line := map[ghostty.SGRUnderline]string{144 ghostty.UnderlineDouble: " double", ghostty.UnderlineCurly: " wavy",145 ghostty.UnderlineDotted: " dotted", ghostty.UnderlineDashed: " dashed",146 }[u]147 if c := cssColor(st.UnderlineColor(), ""); c != "" {148 line += " " + c149 }150 parts = append(parts, "text-decoration:underline"+line)151 }152 if st.Strikethrough() {153 parts = append(parts, "text-decoration:line-through")154 }155 return strings.Join(parts, ";")156}157158func cssColor(c ghostty.StyleColor, none string) string {159 switch c.Tag {160 case ghostty.StyleColorPalette:161 return fmt.Sprintf("var(--vt-palette-%d)", c.Palette)162 case ghostty.StyleColorRGB:163 return fmt.Sprintf("#%02x%02x%02x", c.RGB.R, c.RGB.G, c.RGB.B)164 }165 return none166}