commit 502e0d5bb61287accc4380aa23b380a8929faf1b from: ale date: Sun Aug 30 01:07:34 2026 UTC Add slide directive A slide with '' on its own line gets every text line horizontally centered within the content width after rendering; Kitty graphics placeholders keep their existing PadLeft centering. The marker is stripped before glamour sees it, like . For closer / section / figure-caption slides. commit - 17219495f34a017ba2918517de54d8c7bf15a888 commit + 502e0d5bb61287accc4380aa23b380a8929faf1b blob - dad863b30ac32f2fd1a3392e2953e7f5b9cf1aaa blob + df3e4fec2c678e864b42e6ab4ee50929f564c3b0 --- internal/model/model.go +++ internal/model/model.go @@ -25,6 +25,7 @@ import ( "charm.land/lipgloss/v2" "github.com/charmbracelet/glamour" + "github.com/charmbracelet/x/ansi/kitty" "github.com/maaslalani/slides/internal/bib" "github.com/maaslalani/slides/internal/code" "github.com/maaslalani/slides/internal/image" @@ -332,6 +333,29 @@ func juliaEnv(maxCols, maxRows int) map[string]string } } +// centerMarker on its own line makes renderSlide horizontally center the +// slide's text. +const centerMarker = "" + +// centerLines horizontally centers every non-blank text line of s within +// width. Lines carrying a Kitty graphics placeholder are left untouched -- +// their own PadLeft already centers the image. +func centerLines(s string, width int) string { + lines := strings.Split(s, "\n") + for i, line := range lines { + if strings.ContainsRune(line, kitty.Placeholder) { + continue + } + trimmed := strings.TrimSpace(line) + w := lipgloss.Width(trimmed) + if w == 0 || w >= width { + continue + } + lines[i] = strings.Repeat(" ", (width-w)/2) + trimmed + } + return strings.Join(lines, "\n") +} + func fileWatchCmd() tea.Cmd { return tea.Every(time.Second, func(t time.Time) tea.Msg { return fileWatchMsg{} @@ -579,6 +603,15 @@ func (m Model) renderSlide(md string, includeVirtualTe r, _ := glamour.NewTermRenderer(m.Theme, glamour.WithWordWrap(contentWidth)) slide := code.HideComments(md) + // A slide carrying `` on its own line has every text + // line horizontally centered after rendering (images keep their own + // centering). Strip the marker before glamour sees it. + centered := strings.Contains(slide, centerMarker) + if centered { + slide = strings.ReplaceAll(slide, centerMarker+"\n", "") + slide = strings.ReplaceAll(slide, centerMarker, "") + } + maxCols, maxRows := m.mediaSize() spin := m.spinner.View() @@ -633,6 +666,9 @@ func (m Model) renderSlide(md string, includeVirtualTe if len(latexPlacements) > 0 { slide = latex.Expand(slide, latexPlacements) } + if centered { + slide = centerLines(slide, contentWidth) + } if margin := m.leftMargin(); margin > 0 { pad := strings.Repeat(" ", margin) lines := strings.Split(slide, "\n") blob - /dev/null blob + 3f4d280872cee04739c4627f646e73179d20fb07 (mode 644) --- /dev/null +++ internal/model/center_test.go @@ -0,0 +1,43 @@ +package model + +import ( + "strings" + "testing" + + "github.com/charmbracelet/x/ansi/kitty" +) + +func TestCenterLines(t *testing.T) { + const width = 20 + + got := centerLines("hello\n\n spaced ", width) + lines := strings.Split(got, "\n") + if lines[0] != strings.Repeat(" ", 7)+"hello" { // (20-5)/2 = 7 + t.Errorf("line 0 not centered: %q", lines[0]) + } + if lines[1] != "" { + t.Errorf("blank line changed: %q", lines[1]) + } + if lines[2] != strings.Repeat(" ", 7)+"spaced" { // trimmed len 6, (20-6)/2 = 7 + t.Errorf("line 2 not centered/trimmed: %q", lines[2]) + } +} + +func TestCenterLinesLeavesPlaceholdersAndAnsi(t *testing.T) { + ph := string(kitty.Placeholder) + "xxxx" + got := centerLines(ph, 40) + if got != ph { + t.Errorf("placeholder line was modified: %q", got) + } + + ansi := "\x1b[1mbold\x1b[0m" // visible width 4 + got = centerLines(ansi, 10) + if got != strings.Repeat(" ", 3)+ansi { // (10-4)/2 = 3 + t.Errorf("ansi width miscounted: %q", got) + } + + long := "this line is wider than the width" + if centerLines(long, 10) != long { + t.Errorf("over-wide line should be untouched") + } +}